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

news2024/9/24 11:26:33

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 = #
	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 是栈底寄存器。由于此时已经调用 Add 函数成功,此时的 ebp 指向 Add 函数栈帧的栈底。上述两句汇编代码首先是将 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.coloradmin.cn/o/1310959.html

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

相关文章

【系统架构】集群、分布式概念及系统架构演进过程

集群、分布式概念&#xff1a; 对食物没有太高要求的人在肚子饿的时候一般都会选择去兰州拉面、沙县小吃等小饭馆&#xff0c;这类小饭馆有个很显著的特点&#xff1a;洗菜、切菜、炒菜都是同一个人完成&#xff0c;如果厨子不舒服可能饭馆还会歇业。而一些人流量较大的饭馆的分…

【Qt之QNetworkAccessManager】概述及示例

概述 QNetworkAccessManager类允许应用程序发送网络请求和接收应答 网络访问API是围绕一个QNetworkAccessManager对象构建的&#xff0c;该对象为它发送的请求保存通用配置和设置。它包含代理和缓存配置&#xff0c;以及与此类问题相关的信号&#xff0c;以及可用于监视网络操…

【Linux API 揭秘】container_of函数详解

我的圈子&#xff1a; 高级工程师聚集地 我是董哥&#xff0c;高级嵌入式软件开发工程师&#xff0c;从事嵌入式Linux驱动开发和系统开发&#xff0c;曾就职于世界500强企业&#xff01; 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; …

leetcode-21-合并两个有序链表(C语言实现)

题目&#xff1a; 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输出…

不用再找,这是大模型 LLM 微调经验最全总结

大家好&#xff0c;今天对大模型微调项目代码进行了重构&#xff0c;支持ChatGLM和ChatGLM2模型微调的切换&#xff0c;增加了代码的可读性&#xff0c;并且支持Freeze方法、Lora方法、P-Tuning方法、「全量参数方法」 微调。 PS&#xff1a;在对Chat类模型进行SFT时&#xff…

一款计算机顶会爬取解析系统 paper info

一款计算机顶会爬取解析系统 paper info 背景项目实现的功能 技术方案架构设计项目使用的技术选型 使用方法本地项目部署使用ChatGPT等大模型创建一个ChatGPT助手使用阿里云 顶会数据量 百度网盘pfd文件json文件 Q&A github链接 &#xff1a;https://github.com/codebricki…

PyTorch 模型训练性能大揭秘:从基础到高级技巧一网打尽!

PyTorch 是一个开源的 Python 机器学习库&#xff0c;基于Torch&#xff0c;用于自然语言处理等应用程序。 PyTorch既可以看作加入了GPU支持的numpy&#xff0c;也可以看成一个拥有自动求导功能的强大的深度神经网络&#xff0c;同时它也是大模型开发的首选工具。 《PyTorch模…

每日分享,以元旦为题的诗词

元旦佳节即将来临&#xff0c;相信大家都会在朋友圈表达一下自己的情感&#xff0c;不管大家以前是怎么表达的&#xff0c;今天小编给你分享几首以元旦为题的几首诗&#xff0c;喜欢的朋友可以自取&#xff0c;想要更多免费的诗词&#xff0c;请自行百度或小程序搜索&#xff1…

ES中根据主键_id查询记录

一、需求 es中_type&#xff1a;_doc&#xff0c;想要根据主键_id查询记录 二、实现 复合查询中使用语句查询http://192.168.1.1/_doc/1

ArcGIS for Android开发引入arcgis100.15.2

最后再点击同步即可&#xff01;&#xff01;&#xff01;

Stable diffusion 简介

Stable diffusion 是 CompVis、Stability AI、LAION、Runway 等公司研发的一个文生图模型&#xff0c;将 AI 图像生成提高到了全新高度&#xff0c;其效果和影响不亚于 Open AI 发布 ChatGPT。Stable diffusion 没有单独发布论文&#xff0c;而是基于 CVPR 2022 Oral —— 潜扩…

如何用开关电源测试系统测试电源峰值电流?

一、用万用表、示波器测量峰值电流 首先将待测电路输入信号线分别连接到测试电路的输入端和地端。待测电路的电源端连接电源。然后将示波器设置为AC耦合模式&#xff0c;通道1连接待测电路输入端&#xff0c;通道2连接待测电路地端。调整数字万用表为电流测量模式。打开电源&am…

【动手学深度学习】(十三)深度学习硬件

文章目录 一、CPU和GPU二、更多的芯片1.DSP:数字信号处理2.可编程阵列(FPGA)3.AI ASIC 三、单机多卡并行 一、CPU和GPU 提升CPU利用率 在计算ab之前&#xff0c;需要准备数据 主内存->L3->L2->L1->寄存器(数据只有进入寄存器才可以参与运算) 提升空间和时间的内存…

USB2.0 Spec 中文篇

体系简介 线缆 USB 是一种支持热拔插的高速串行传输总线&#xff0c;使用一对&#xff08;两根&#xff09;差分信号来传输数据&#xff0c;半双工。要求使用屏蔽双绞线。 供电 USB 支持 “总线供电” 和 “自供电” 两种供电模式。在总线供电方式下&#xff0c;设备最多可…

C++学习笔记—— C++内存管理方式:new和delete操作符进行动态内存管理

系列文章目录 http://t.csdnimg.cn/d0MZH 目录 系列文章目录http://t.csdnimg.cn/d0MZH 比喻和理解a.比喻C语言开空间C开空间 b.理解a、C语言的内存管理的缺点1、开发效率低&#xff08;信息传递繁琐&#xff09;2、可读性低&#xff08;信息展示混乱&#xff09;3、稳定性差&…

【MATLAB】基于VMD分解的信号去噪算法(基础版)

代码的使用说明 基于VMD分解的信号去噪算法&#xff08;基础版&#xff09; 代码流程图 代码效果图 本文代码&#xff1a;阿里云盘分享 获取代码请关注MATLAB科研小白的个人公众号&#xff08;即文章下方二维码&#xff09;&#xff0c;并回复VMD去噪 本公众号致力于解决找代…

铭飞CMS list 接口 SQL注入漏洞复现

0x01 产品简介 铭飞CMS是一款基于java开发的一套轻量级开源内容管理系统,铭飞CMS简洁、安全、开源、免费,可运行在Linux、Windows、MacOSX、Solaris等各种平台上,专注为公司企业、个人站长快速建站提供解决方案 0x02 漏洞概述 铭飞CMS在5.2.10版本以前list 接口处存在sql注入…

AI抽烟识别系统研发关键

为了设计一个有效的AI抽烟识别系统&#xff0c;我们需要考虑几个关键组成部分&#xff1a;图像捕捉、数据处理、模型训练、以及实际应用场景。下面是这个方案的详细阐述&#xff1a; 1. 图像捕捉与数据收集 摄像头部署&#xff1a;首先&#xff0c;在需要监控的区域安装高分辨…

社交网络分析1:起源发展、不同领域的应用、核心概念

社交网络分析1&#xff1a;社交网络相关定义和概念 写在最前面关于课程 社交网络、社交网络分析社交网络发展阶段&#xff08;自己感兴趣&#xff09;1. 社交网络的起源2. 社交网络的演变3. 社交网络的成熟4. 发展阶段补充和展望 2023社交大变革&#xff08;自己感兴趣的点&…

带你手把手解读firejail沙盒源码(0.9.72版本) (三) fcopy

文章目录 main.c该模块的各个函数功能详解selinux_relabel_pathcopy_filemkdir_attrcopy_linkproc_pid_to_selffs_copydircheckduplicate_dirduplicate_fileduplicate_linkmain Makefile main.c 文件总结 ├── fcopy │ ├── Makefile │ └── main.cmain.c #include…