【C语言进阶】函数栈帧与可变参数列表(函数调用,函数栈帧,可变参数列表,递归调用)

一、函数栈帧

1.认识相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值

ebx:通用寄存器,保留临时数据

ebp:栈底寄存器

esp:栈顶寄存器

eip:指令寄存器,保存当前指令的下一条指令的地址


2.认识相关汇编命令

mov:数据转移指令

push:数据入栈,同时esp栈顶寄存器也要发生改变

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

sub:减法命令

add:加法命令

call:函数调用,1. 压入返回地址 2. 转入目标函数

jump:通过修改eip,转入目标函数,进行调用

ret:恢复返回地址,将返回地址pop至eip寄存器


3.起步(执行call指令之前)

1.局部变量空间的开辟,是在对应函数栈帧内部开辟的。

2.临时变量具有临时性的本质是:栈帧具有临时性

3.函数形参实例化形成临时拷贝是在函数被正式调用(执行call指令)之前。

4.形参实例化(函数形参压栈)的顺序是从右向左

5.函数形参(实参的临时拷贝)的内存空间是相邻的,也就数说可以通过其中一个形参的地址和形参变量的类型访问其他形参变量(可变参数列表的工作原理)。

6.main函数也要被调用,main函数的调用也会形成栈帧结构。


4.开始调用(执行call指令)

call指令:

1.压入返回地址(函数调用完毕后,需要返回调用处的下一条命令)

2.转入目标函数


5.形成栈帧

1.函数的栈帧空间由esp保存的栈顶指针和ebp保存栈底指针所标定。

2.调用函数会形成函数栈帧,esp寄存器保存的栈顶指针减多少栈区空间就有多大。而这个sub的数值由编译器决定。编译器会在程序的编译阶段通过函数体中定义的变量即其类型确定栈区空间的大小


6.执行MyAdd函数

1. 如果函数的返回值是较小的内置类型(可以存放在寄存器中),就会通过寄存器返回给函数调用方。(即使不接受返回值,也会保存在寄存器中)

2. 如果函数的返回值是较大的内置类型或是复杂类型(结构体,C++类),就会存放在函数调用方的栈帧空间中。


7.释放栈帧

1.释放栈帧空间的本质是,函数的栈帧空间不在栈顶,栈底指针标定的范围内,下一次调用函数可以覆盖对应的空间。

2.释放栈帧空间,仅仅是将该空间设置为无效(使可以被覆盖),而并不会清空其中的数据。


8.释放临时拷贝,彻底释放空间

总结:调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧的成本。

二、可变参数列表

1.使用

#include <stdio.h>
#include <windows.h>
//num:表示传入参数的个数
int FindMax(int num, ...)
{
	va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型
	va_start(arg, num); //使arg指向可变参数部分
	int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
	for (int i = 0; i < num - 1; i++){//获取并比较其他的
		int curr = va_arg(arg, int);
		if (max < curr){
			max = curr;
		}
		
	}
	va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
	return max;
}
int main()
{
	int max = FindMax(5, 11, 22, 33, 44, 55);
	printf("max = %d\n", max);
	system("pause");
	return 0;
}

初步了解相关宏:

1. va_list    typedef char *  va_list;

//定义可以访问可变参数部分的变量,其实是一个char*类型

2. va_start(ap,v)    

//使ap指针跳过第一个变量v,指向可变参数部分

3. va_arg(ap,t)    

//根据类型依次获取可变参数列表中的数据

4. va_end(ap)    #define _crt_va_end(ap)      ( ap = (va_list)0 )

//将指针设为NULL


2.注意事项

1.可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。

2.参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。

3.使用可变参数定义函数时,可变参数列表前可以有若干个命名参数(至少一个),但可变参数列表之后不能有命名参数。

 3.这些宏是无法直接判断实际存在参数的数量。以上的例子中FindMax函数是通过num变量确定参数数量的。

4.这些宏无法判断每个参数的是类型。参数的类型需要在va_arg宏中指定。

5.如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。会发生多读取或少读取内存的错误。

6.标准库函数printf();就是使用可变参数的典型代表,当中通过格式控制符(%d,%f,%s)向函数传递参数的类型及数量。第一个参数const char*是唯一存在的命名参数。


3.原理

1.使用可变参数列表定义的函数,最终调用也是函数调用,也要形成栈帧

2.栈帧形成前,临时变量是要先入栈的,根据之前所学,参数之间位置关系是固定的。(函数形参压栈(形参实例化)的顺序是从右向左依次入栈的,形参与形参之间是紧挨着的。)

3.通过查看汇编,我们看到,在可变参数场景下:

>>>实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行4字节提升(4的最小倍数):char,short向int提升;float向double提升。函数形参是按找4字节提升后的内存大小进行压栈的。

>>>函数内部使用的时候,根据类型提取数据,就要考虑提升之后的值,如果不加考虑,获取数据可能会报错或者结果不正确。

>>>函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行

#pragma warning(disable:4996)
#include <stdio.h>
#include <Windows.h>
//FindMax1:不考虑float类型的4字节提升
float FindMax1(int num, ...)
{
	va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型
	va_start(arg, num); //使arg指向可变参数部分
	float max = va_arg(arg, float); //根据类型,获取可变参数列表中的第一个数据
	for (int i = 0; i < num - 1; i++){//获取并比较其他的
		float curr = va_arg(arg, float);
		if (max < curr){
			max = curr;
		}
	}
	va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
	return max;
}
//FindMax2:float类型4字节提升为double类型
double FindMax2(int num, ...)
{
	va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型
	va_start(arg, num); //使arg指向可变参数部分
	double max = va_arg(arg, double); //根据类型,获取可变参数列表中的第一个数据
	for (int i = 0; i < num - 1; i++){//获取并比较其他的
		double curr = va_arg(arg, double);
		if (max < curr){
			max = curr;
		}
	}
	va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
	return max;
}

int main()
{
	float a = 3.14f;
	float b = 6.28f;
	float c = 9.37f;
	float d = 2.56f;
	float e = 1.1f;

	float max1 = FindMax1(5, a, b, c, d, e);
	double max2 = FindMax2(5, a, b, c, d, e);
	
	printf("max1 = %f\n", max1);
	printf("max2 = %lf\n", max2);
	
	system("pause");
	return 0;
}

执行结果: 


 4.相关宏的底层源码:

1. va_list    typedef char *  va_list;

//定义可以访问可变参数部分的变量,其实是一个char*类型

2. va_start    #define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

//使ap指针跳过第一个变量v,指向可变参数部分

>>>宏参ap是va_list指针

>>>宏参v是可变参数列表之前的被命名参数,紧邻可变参数列表。

将上面的宏定义稍加翻译:( ap = (char*)&v + _INTSIZEOF(v) )

3. va_arg    #define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

//根据类型依次获取可变参数列表中的数据

>>>宏参t是可变参数4字节提升后的类型

1. (ap += _INTSIZEOF(t))

使ap指向当前可变参数的下一个参数

2. ((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

指向当前可变参数,此时ap任然指向下一个参数

3. ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

将指针强转成对应可变参数类型的指针,再解引用访问当前可变参数。

4.由以上解释可知,va_arg宏的工作有两个步骤:

>>>将ap指针指向下一个可变参数

>>>访问当前可变参数

4. va_end    #define _crt_va_end(ap)      ( ap = (va_list)0 )

//将指针设为NULL

5. ADDRESSOF    #define _ADDRESSOF(v)   ( &(v) )

//取变量的地址

6. _INTSIZEOF    #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

//按照4字节对齐的原则,确定变量的大小

>>>宏参n传入的是数据类型

1. 将上面的宏定义稍加翻译:( ((sizeof(n) + sizeof(int) - 1) / sizeof(int)) * sizeof(int) )

2. 将sizeof(int)替换为4:( ((sizeof(n) + 3) / 4) * 4 )

        当n是4的整数倍时(4,8):

        ((sizeof(n) + 3) / 4) <==> (sizeof(n) / 4)

        当n不是4的整数倍时(1,2,5,6):

        ((sizeof(n) + 3) / 4) <==> ((sizeof(n) / 4)+1) //完成了4字节提升

3. ( ((sizeof(n) + 3) / 4) * 4 ) <==> w / 4 * 4 

从位运算的角度来看实际上就是将w的数据右移两位再左移两位,即将w的后两位清空

4. w / 4 * 4 <==> w & ~3(位运算关闭位,将后两位清零)

最终得到定义式: ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )


三、函数的递归调用

1.递归算法的本质是:目标问题的子问题,也可以采用相同的算法解决,本质就是分治的思想。

2.递归的两个必要条件:

>>>存在限制条件,当满足限制条件时,递归便不再继续。

>>>每次递归调用之后越来越接近这个限制条件

3.Stack overflow 栈溢出

>>>每一次函数的调用都要在栈区申请分配函数栈帧

>>>函数递归调用时,上一层的递归并未结束任然占用着栈区中的内存空间,再次递归调用时又需要在栈区分配内存。如此循环,栈空间总有被耗干的时候,即出现栈溢出的错误

4.函数递归调用时栈溢出的一般原因:

>>>死递归(没有跳出条件,或者没有逼近跳出条件)

>>>递归调用层次太深

5.递归VS迭代

递归法:

1.递归代码简洁,解题思路简单

1.随着计算量的变大,递归成本越来越高。

2.具体原因是树形结构越来越大,并且里面存在大量的重复计算

3.函数调用是有成本的!递归不一定适合所有场景,尤其是对效率或者资源需求量大的场景。

 由上图可以看出,fib(6)和fib(7)虽然只相差一个数字但计算量却相差很大,这种差距会随计算数字的增大也变得越来越大,并且里面存在大量的重复计算。

 上图是用递归法求第41,42,43个斐波那契数所得的结果,计算所用的时间,和fib(3)所计算的次数

迭代法:

1.效率一般很高,迭代法效率高的根本原因是没有多余的函数调用

2.代码一般比较复杂

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第一篇 基础知识篇 实例1 数据类型转换 实例2 转义字符 实例3 关系和逻辑运算 实例4 自增自减 实例5 普通位运算 实例6 位移运算 实例7 字符译码 实例8 指针操作符 实例9 if判断语句 实例10 else-if语句 实例11 嵌套if语句 实例12 switch语句 实例13 for语句 实例14 while语句 实例15 do-while语句 实例16 break和continue语句 // 实例17 exit()语句 // 实例18 综合实例 实例19 一维数组 实例20 二维数组 实例21 字符数组 // 实例22 数组初始化 // 实例23 数组应用 实例24 函数的值调用 实例25 函数的引用调用 //swap 实例26 数组函数调用 // 实例27 命令行变元 // 实例28 函数的返回值 实例29 函数的嵌套调用 实例30 函数调用 实例31 局部和全局变量 实例32 变量的存储类别 实例33 内部和外部函数 实例34 综合实例1 实例35 综合实例2 实例36 变量的指针 实例37 一维数组指针 实例38 二维数组指针 实例39 字符串指针 实例40 函数指针 实例41 指针数组 实例42 二维指针 实例43 指针的初始化 实例44 综合实例 第二篇 深入提高篇 实例45 结构体变量 实例46 结构体数组 实例47 结构体指针变量 实例48 结构体指针数组 实例49 共用体变量 实例50 枚举类型 实例51 读写字符 实例52 读写字符串 实例53 格式化输出函数 实例54 格式化输入函数 实例55 打开和关闭文件 实例56 fputc()和fgetc() 实例57 函数rewind() 实例58 fread()和fwrite() 实例59 fprintf()和fscanf() 实例60 随机存取 实例61 错误处理 实例62 综合实例 实例63 动态分配函数 实例64 常用时间函数 实例65 转换函数 实例66 查找函数 实例67 跳转函数 实例68 排序函数 实例69 伪随机数生成 实例70 可变数目变元 第三篇 常用算法篇 实例71 链表的建立 实例72 链表的基本操作 实例73 队列的应用 实例74 堆的应用 实例75 串的应用 实例76 树的基本操作 实例77 冒泡排序法 实例78 堆排序 实例79 归并排序 实例80 磁盘文件排序 实例81 顺序查找 实例82 二分法查找 实例83 树的动态查找 实例84 二分法求解方程 实例85 牛顿迭代法求解方程 实例86 弦截法求解方程 实例87 拉格朗日插值 // 实例88 最小二乘法拟合 ?? 实例89 辛普生数值积分 实例90 改欧拉法 实例91 龙格-库塔法 实例92 高斯消去法 实例93 正定矩阵求逆 第四篇 综合应用篇 实例94 用C语言实现遗传算法 实例95 人工神经网络的C语言实现 实例96 K_均值算法 实例97 ISODATA算法 实例98 快速傅立叶变换 实例99 求解野人与传教士问题 实例100 简单专家系统
目 录 第1章 C语言 8 1.1 什么是局部程序块(local block)? 8 1.2 可以把变量保存在局部程序块中吗? 9 1.3 什么时候用一条switch语句比用多条if语句更好? 9 1.4 switch语句必须包含default分支吗? 10 1.5 switch语句的最后一个分支可以不要break语句吗? 11 1.6 除了在for语句中之外,在哪些情况下还要使用逗号运算符? 11 1.7 怎样才能知道循环是否提前结束了? 13 1.8 goto,longjmp()和setjmp()之间有什么区别? 13 1.9 什么是左值(lvaule)? 15 1.10 数组(array)可以是左值吗? 15 1.11 什么是右值(rvaule)? 16 1.12 运算符的优先级总能保证是“自左至右”或“自右至左”的顺序吗? 17 1.13 ++var和var++有什么区别? 17 1.14 取模运算符(modulus operator)“%”的作用是什么? 17 第2章 变量和数据存储 18 2.1. 变量存储在内存(memory)中的什么地方? 18 2.2. 变量必须初始化吗? 19 2.3. 什么是页抖动(pagethrashing)? 19 2.4. 什么是const指针? 20 2.5. 什么时候应该使用register修饰符?它真的有用吗? 21 2.6. 什么时候应该使用volatile修饰符? 21 2.7. 一个变量可以同时被说明为const和volatile吗? 22 2.8. 什么时候应该使用const修饰符? 23 2.9. 浮点数比较(floating-point comparisons)的可靠性如何? 23 2.10. 怎样判断一个数字型变量可以容纳的最大值? 24 2.11. 对不同类型的变量行算术运算会有问题吗? 25 2.12. 什么是运算符升级(operatorpromotion)? 25 2.13. 什么时候应该使用类型强制转换(typecast)? 26 2.14. 什么时候不应该使用类型强制转换(typecast)? 27 2.15. 可以在头文件中说明或定义变量吗? 27 2.16. 说明一个变量和定义一个变量有什么区别? 27 2.17. 可以在头文件中说明static变量吗? 28 2.18. 用const说明常量有什么好处? 28 第3章 排序与查找 28 排序 28 查找 29 排序或查找性能? 30 3.1. 哪一种排序方法最方便? 32 3.2. 哪一种排序方法最快? 33 3.3. 对外存(磁盘或磁带)中而不是内存中的数据行排序称为外部排序。 39 3.4. 1哪一种查找方法最方便? 44 3.5. 1哪一种查找方法最快? 46 3.6. 1什么是哈希查找? 51 3.7. 1怎样对链表行排序? 53 3.8. 1怎样查找链表中的数据? 53 第4章 数据文件 59 4.1. 当errno为一个非零值时,是否有错误发生? 59 4.2. 什么是流(stream)? 59 4.3. 怎样重定向一个标准流? 60 4.4. 怎样恢复一个重定向了的标准流? 60 4.5. stdout能被强制打印到非屏幕设备上吗? 61 4.6. 文本模式(textmode)和二制模式(binarymode)有什么区别? 61 4.7. 怎样判断是使用流函数还是使用低级函数? 62 4.8. 怎样列出某个目录下的文件? 62 4.9. 怎样列出一个文件的日期和时间? 63 4.10. 怎样对某个目录下的文件名行排序? 66 4.11. 怎样判断一个文件的属性? 67 4.12. 怎样查看PATH环境变量? 69 4.13. 怎样打开一个同时能被其它程序修改的文件? 69 4.14. 怎样确保只有你的程序能存取一个文件? 71 4.15. 怎样防止其它程序修改你正在修改的那部分文件内容? 71 4.16. 怎样一次打开20个以上的文件? 72 4.17. 怎样避开"Abort,Retry,Fail”消息? 72 4.18. 怎样读写以逗号分界的本? 74 第5章 编译预处理 76 5.1. 什么是宏(macro)?怎样使用宏? 76 5.2. 预处理程序(preprocessor)有什么作用? 77 5.3. 怎样避免多次包含同一个头文件? 79 5.4. 可以用#include指令包含类型名不是".h"的文件吗? 80 5.5. 用#define指令说明常量有什么好处? 80 5.6. 用enum关键字说明常量有什么好处? 81 5.7. 与用#define指令说明常量相比,用enum关键字说明常量有什么好处? 81 5.8. 如何使部分程序在

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芥末虾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值