深入理解C语言的函数参数

1、一个简单的函数

int Add(int x, int y)
{return x + y;
}int main()
{printf("%d", Add(2, 3, 4, 5, 6));return 0;
}

        这一段足够简单的代码,闭眼都能知道运行结果会在屏幕上打印 5 。那编译器是怎么处理后面的 4、5、6 ?

        我们再看看这个函数。

void MyTest(int a, int b, int c, int d)
{printf("%d", a);
}

         似乎参数 b、c、d 的设定是多余的。不论这三个参数传入什么值,都不影响结果。那上述的 Add 函数是不是也能看作后续 4、5、6 对应的参数没有用到,所以没有表现出任何现象?

        带着这个问题,再看一个函数:

void MyTest_2(int num)
{int* ptr = &num;for (int i = 0; i <= num; i++){printf("%d ", *ptr);ptr++;}
}

        是不是有点懵?对形参取地址是个什么操作?还要对形参的指针进行移动再打印出来又是什么鬼?预感上,大概率会报非法访问。

        那不妨,我们在 main 函数里调用一下?

MyTest_2(0);
MyTest_2(1);
MyTest_2(10);

        然而结果是:

        虽然返回了一堆不明所以的值,但返回代码 0 说明程序压根就没有报错。而参数 0 、 1 、 10 都被完整打印出来了,简直是毁三观。

        知道你很急,但是你先别急,再在 main 函数中试试这个足以让人懵逼的例子:

MyTest_2(5, 100, 20, 35, 40, 114514);

        结果更毁三观了: 

        什么玩意?明明 MyTest 创建时只设定了一个参数,为什么传入六个参数能全部打印出来?是不是说明,一开始的 Add 调用,后面的 4、5、6 也一并进行了传参,只是没有在函数内部进行使用?

        要弄懂这个问题,首先得了解编译器对函数调用时的参数是怎么处理,传参过程又是怎么样的。

2、函数传参过程

2.1、栈帧建立之前

        调用函数时,系统会在内存中创建对应函数的栈帧。关于栈帧建立及销毁这部分内容可以看这一篇开头部分:函数栈帧简述。

        而在进行栈帧建立之前,程序还会执行一系列的操作。以这段代码为例,直接在汇编中看看 ret 赋值时的 Add 调用,汇编指令到底做了什么:

int Add(int x, int y)
{return x + y;
}int main()
{int ret = Add(0xAB, 0xCD, 0xEF, 0xAA, 0xDD);return 0;
}

        当前栈帧是 main 函数,根据以上汇编指令,在调用 Add 函数之前,程序将 0xAB、0xCD、0xEF、0xAA、 0xDD 这五个参数逆序放入 main 函数栈顶( ESP 是栈顶寄存器)。

         这一步实际上就是函数传参。通过这一步得出结论,不论函数在创建时定义了多少个形参,甚至不定义形参,只要在调用时,函数名后的括号内写入参数,就一定会进行传参。

2.2、参数调用

        上述两句汇编代码首先是将 ebp+8 位置的值存入 eax 寄存器,再让 eax 寄存器中的值 +=  ebp+12 位置的值。而 ebp+8 的地址与 ebp+12 的地址分别储存了 0xAB 和 0xCD 。

        至此就是一次完整的传参及参数调用。 

        也就是说,只需要知道第一个参数的地址,那么剩下的参数即使不在创建函数时定义,也可以通过第一个参数的地址进行访问。就此,最开始的 MyTest_2 函数产生的现象也就解释完毕。

3、可变参数列表

3.1、定义阐述

        严格来说 C 语言的函数参数数量并不是固定的,那么在应用上根据传入的各个参数类型及第一个参数的地址,对函数传入任意参数个数,只需要通过某种方式在函数内部进行调用,那么函数的灵活性和扩展性就大大提高了。

        很好, printf 也是这么想的。在使用 printf 时,第一个参数中有几个占位符,后续就带几个参数,各位对这规则应该已经形成肌肉记忆了。而对于之前的 MyTest_2 函数,唯一定义的参数便是后续传入有效参数的个数。而为了语义上更加直观,像这类可对后续参数进行操作的函数在创建时,一般会加上三个点。当然,也是为了语义,将变量名改为 argc (argument count):

void MyTest_2(int argc, ...)
{int* ptr = &argc;for (int i = 0; i <= argc; i++){printf("%d ", *ptr);ptr++;}
}

        如以上函数中用其中某个参数确定后续参数的个数,那么这一系列参数就叫可变参数列表。

3.2、初步实现

        虽然在 MyTest_2 中已经初步实现了带可变参数列表的函数创建,但这个函数好像没什么用。所以这里再举一个例子,求若干浮点数的和:

double Sum(int argc, ...)
{double sum = 0.;//创建可变参数列表的头部指针,将指针指向列表第一个元素double* ptr = (double*)(&argc + 1);//遍历可变参数列表,求和for (int i = 1; i <= argc; i++){sum += *ptr;//指针指向下一个参数ptr++;}return sum;
}

        这代码貌似没问题,但传入的参数列表仅限于 double 类型,如果传入的参数是一个整型变量呢?由于内部只能通过指针访问,根本无法知晓外部传入的变量类型,而且编译器也不会对可变参数列表中的参数类型作检查。

        所以,如果列表的参数类型不一致,第一个参数除了附带参数的数量信息外,还应附带每个参数的类型。解决办法可以参照 printf 的第一个参数。在此之前,先了解一个点,函数在传参时,汇编指令会对参数进行类型提升和 4 字节对齐。也就是说,char、short 的类型会被提升为 int ,而 float 类型直接提升为 double 。

        修改后如下:

//format字符串只允许d或f,不区分大小写
double Sum(const char* format, ...)
{double sum = 0.;int count = strlen(format);//创建可变参数列表的头部指针,将指针指向列表第一个元素char* ptr = (char*)(&format) + sizeof(char*);for (int i = 0; i < count; i++){//遇到字符d或者D以整型处理if (format[i] == 'd' || format[i] == 'D'){sum += (double)*((int*)ptr);//指针指向下一个参数ptr += sizeof(int);}//遇到字符f或者F以双精度浮点型处理else if (format[i] == 'f' || format[i] == 'F'){sum += *((double*)ptr);//指针指向下一个参数ptr += sizeof(double);}}return sum;
}

        至此已经很接近 printf 的参数调用方式了。

3.3、可变参数列表宏

        调用 stdio.h 头文件便可以使用专用于处理可变参数列表的四个宏:

        va_list:用于创建读取可变参数列表的指针;

typedef char* va_list;

        __crt_va_start:将可变参数列表的指针指向列表第一个参数;

#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)

        __crt_va_arg:获取可变参数列表的指针当前指向的参数,并将指针指向下一个参数;

#define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

        __crt_va_end:用于销毁可变参数列表的指针。

#define __crt_va_end(ap) ((void)(ap = (va_list)0))

        此外对上述 _INTSIZEOF 和 _ADDRESSOF 也需要作了解:

#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

        上述的 _INTSIZEOF 比较难以理解。它的运算结果是 4 字节对齐,这个公式有点巧妙,有兴趣可以自行理解。

        接下来先将上面的代码用这几个宏改造一下:

double Sum(const char* format, ...)
{double sum = 0.0;va_list ptr;__crt_va_start(ptr, format);for (int i = 0; i < strlen(format); i++){if (format[i] == 'd' || format[i] == 'D'){sum += __crt_va_arg(ptr, int);}else if (format[i] == 'f' || format[i] == 'F'){sum += __crt_va_arg(ptr, double);}}__crt_va_end(ptr);return sum;
}

        不过这几个宏不推荐使用,因为随着编译器的不同,很可能某些编译器并不支持这些宏,可移植性大大降低。这里主要是提供宏的思路,至于宏的实现也已经展示,各位完全可以根据这些宏通过纯 C 代码实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/221879.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端已死?别低估前端,他是互联网世界的核心!【这是一篇治愈系文章】

文章目录 &#x1f4a5; AI回答&#x1f98b; 现状&#x1f989; 焦虑&#x1f409; 力量&#x1f985; 观点&#x1f423; 粗浅分析&#x1f9a5; 快乐的韭菜&#x1f3c6; 总结 &#x1f4a5; AI回答 前端已死&#xff1f; ai的答案是这样: 前端并没有死掉&#xff0c;它仍然…

【小沐学Python】Python实现语音识别(SpeechRecognition)

文章目录 1、简介2、安装和测试2.1 安装python2.2 安装SpeechRecognition2.3 安装pyaudio2.4 安装pocketsphinx&#xff08;offline&#xff09;2.5 安装Vosk &#xff08;offline&#xff09;2.6 安装Whisper&#xff08;offline&#xff09; 3 测试3.1 命令3.2 fastapi3.3 go…

WTF ‘Questions‘

WTF ‘Tech Team Lead’ As a Tech Team Lead, your role is to oversee the technical aspects of a project or team, and to provide guidance, support, and leadership to your team members. Here are some key responsibilities and aspects of the role: Leadership …

vue 中国省市区级联数据 三级联动

vue 中国省市区级联数据 三级联动 安装插件 npm install element-china-area-data5.0.2 -S 当前版本以测试&#xff0c;可用。组件中使用了 element-ui, https://element.eleme.cn/#/zh-CN/component/installation 库 请注意安装。插件文档 https://www.npmjs.com/package/ele…

Alibaba分布式事务组件Seata AT实战

1. 分布式事务简介 1.1 本地事务 大多数场景下&#xff0c;我们的应用都只需要操作单一的数据库&#xff0c;这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示&#xff1a; 在JDBC编程中&#xff0c;我…

使用SPSS的McNemar检验两种深度学习模型的差异性

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 使用SPSS的McNemar检验两种深度学习模型的差异性 前言简述&#xff1a;一、McNemar检验1.1来源1.2 两配对样本的McNemar(麦克尼马尔)变化显著性检验1.3 适用范围&#xff1a;…

卷积神经网络(含案例代码)

概述 卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;是一类专门用于处理具有网格结构数据的神经网络。它主要被设计用来识别和提取图像中的特征&#xff0c;但在许多其他领域也取得了成功&#xff0c;例如自然语言处理中的文本分类任务。 C…

Nginx快速入门

nginx准备 文本概述参考笔记 狂神&#xff1a;https://www.kuangstudy.com/bbs/1353634800149213186 前端vue打包 参考&#xff1a;https://blog.csdn.net/weixin_44813417/article/details/121329335 打包命令&#xff1a; npm run build:prod nginx 下载 网址&#x…

Java集合--Map

1、Map集合概述 在Java的集合框架中&#xff0c;Map为双列集合&#xff0c;在Map中的元素是成对以<K,V>键值对的形式存在的&#xff0c;通过键可以找对所对应的值。Map接口有许多的实现类&#xff0c;各自都具有不同的性能和用途。常用的Map接口实现类有HashMap、Hashtab…

uniapp+vue3使用canvas保存海报的使用示例,各种奇奇怪怪的问题解决办法

我们这里这里有一个需求&#xff0c;是将当前页面保存为海报分享给朋友或者保存到本地相册&#xff0c;因为是在小程序端开发的&#xff0c;所以不能使用html2canvas这个库&#xff0c;而且微信官方新推出Snapshot.takeSnapshot这个api还不是很完善&#xff0c;如果你是纯小程序…

【问题处理】—— lombok 的 @Data 大小写区分不敏感

问题描述 今天在项目本地编译的时候&#xff0c;发现有个很奇怪的问题&#xff0c;一直提示某位置找不到符号&#xff0c; 但是实际在Idea中显示确实正常的&#xff0c;一开始以为又是IDEA的故障&#xff0c;所以重启了IDEA&#xff0c;并执行了mvn clean然后重新编译。但是问…

ASF-YOLO开源 | SSFF融合+TPE编码+CPAM注意力,精度提升!

目录 摘要 1 Introduction 2 Related work 2.1 Cell instance segmentation 2.2 Improved YOLO for instance segmentation 3 The proposed ASF-YOLO model 3.1 Overall architecture 3.2 Scale sequence feature fusion module 3.3 Triple feature encoding module …

【Python网络爬虫入门教程3】成为“Spider Man”的第三课:从requests到scrapy、爬取目标网站

Python 网络爬虫入门&#xff1a;Spider man的第三课 写在最前面从requests到scrapy利用scrapy爬取目标网站更多内容 结语 写在最前面 有位粉丝希望学习网络爬虫的实战技巧&#xff0c;想尝试搭建自己的爬虫环境&#xff0c;从网上抓取数据。 前面有写一篇博客分享&#xff0…

【实用技巧】从文件夹内批量筛选指定文件并将其复制到目标文件夹

原创文章&#xff0c;转载请注明出处&#xff01; 从文件夹中批量提取指定文件。 使用DOS命令&#xff0c;根据TXT文件中列出指定文件名&#xff0c;批量实现查找指定文件夹里的文件并复制到新的文件夹。 文中给出使用DOS命令和建立批处理文件两种方法。 文件准备 工作文件…

vite(一)——基本了解和依赖预构建

文章目录 一、什么是构建工具&#xff1f;1.为什么使用构建工具&#xff1f;2.构建工具的作用&#xff1f;3.构建工具怎么用&#xff1f; 二、经典面试题&#xff1a;webpack和vite的区别1.编译方式不同2.基础概念不同3.开发效率不同4.扩展性不同5.应用场景不同6.总结&#xff…

QT- QT-lximagerEidtor图片编辑器

QT- QT-lximagerEidtor图片编辑器 一、演示效果二、关键程序三、下载链接 功能如下&#xff1a; 1、缩放、旋转、翻转和调整图像大小 2、幻灯片 3、缩略图栏&#xff08;左、上或下&#xff09;&#xff1b;不同的缩略图大小 4、Exif数据栏 5、内联图像重命名 6、自定义快捷方式…

Vue3安装使用Mock.js--解决跨域

首先使用axios发送请求到模拟服务器上&#xff0c;再将mock.js模拟服务器数据返回给客户端。打包工具使用的是vite。 1.安装 npm i axios -S npm i mockjs --save-dev npm i vite-plugin-mock --save-dev 2.在vite.config.js文件中配置vite-plugin-mock等消息 import { viteMo…

mysql中NULL值

mysql中NULL值表示“没有值”&#xff0c;它跟空字符串""是不同的 例如&#xff0c;执行下面两个插入记录的语句&#xff1a; insert into test_table (description) values (null); insert into test_table (description) values ();执行以后&#xff0c;查看表的…

harmonyOS鸿蒙内核概述

内核概述 内核简介 用户最常见到并与之交互的操作系统界面&#xff0c;其实只是操作系统最外面的一层。操作系统最重要的任务&#xff0c;包括管理硬件设备&#xff0c;分配系统资源等&#xff0c;我们称之为操作系统内在最重要的核心功能。而实现这些核心功能的操作系统模块…

【经验分享】gemini-pro和gemini-pro-vision使用体验

Gemini Gemini已经对开发者开放了Gemini Pro的使用权限&#xff0c;目前对大家都是免费的&#xff0c;每分钟限制60条&#xff0c;至少这比起CloseAI的每个账户5刀限速1min3条要香的多&#xff0c;目前已于第一时间进行了体验 一句话总结&#xff0c;google很大方&#xff0c;但…