三. 函数 与 宏
1. 函数 与 宏 对比案例
(1) 函数 和 宏 的案例
代码示例 : 分别使用 函数 和 宏 将数组数据清零;
1.代码 :
#include <stdio.h> /* 定义宏 : 这个宏的作用是将 p 目前是 void* 类型, 转为 char* 类型, 将后将每个字节的内容都设置为 0 */ #define RESET(p, len) while(len > 0) ((char*)p)[--len] = 0; /* 定义函数 : 也是将 p 指向的 len 字节的内存置空 */ void reset(void* p, int len) { while(len > 0) { ((char*)p)[--len] = 0; } } int main() { //1. 定义两个数组, 用函数 和 宏 不同的方式重置数据 int array1[] = {1, 2, 3}; int array2[] = {4, 5, 6, 7}; //2. 获取两个数组大小 int len1 = sizeof(array1); int len2 = sizeof(array2); //3. 定义循环控制变量 int i = 0; //4. 打印两个数组处理前的数据 printf("打印array1 : \n"); for( i = 0; i < 3; i ++) { printf("array1[%d] = %d \n", i, array1[i]); } printf("打印array2 : \n"); for( i = 0; i < 4; i ++) { printf("array2[%d] = %d \n", i, array2[i]); } //5. 使用宏的方式处理数组1 RESET(array1, len1); //6. 使用函数的方式处理数组2 reset(array2, len2); //7. 打印处理后的数组 printf("打印处理后的array1 : \n"); for( i = 0; i < 3; i ++) { printf("array1[%d] = %d \n", i, array1[i]); } printf("打印处理后的array2 : \n"); for( i = 0; i < 4; i ++) { printf("array2[%d] = %d \n", i, array2[i]); } return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
2.编译运行结果 :
虽然看起来 函数 和 宏实现了相同的功能, 但是它们有很大的区别;
2. 函数 和 宏 的分析
(1) 函数 和 宏 分析
函数 和 宏 分析 :
1.宏处理 : 宏定义是在预处理阶段直接进行宏替换, 代码直接复制到宏调用的位置, 由于宏在预处理阶段就被处理了, 编译器是不知道宏的存在的;
2.函数处理 : 函数是需要编译器进行编译的, 编译器有决定函数调用行为的义务;
3.宏的弊端 ( 代码量 ) : 每调用一次宏, 在预处理阶段都要进行一次宏替换, 会造成代码量的增加;
4.函数优势 ( 代码量 ) : 函数执行是通过跳转来实现的, 代码量不会增加;
5.宏的优势 ( 效率 ) : 宏 的执行效率 高于 函数, 宏定义是在预编译阶段直接进行代码替换, 没有调用开销;
6.函数的弊端 ( 效率 ) : 函数执行的时候需要跳转, 以及创建对应的活动记录( 栈 ), 效率要低于宏;
3. 函数 与 宏 的 利弊
(1) 宏 优势 和 弊端
宏的优势和弊端 : 宏的执行效率要高于函数, 但是使用宏会有很大的副作用, 非常容易出错, 下面的例子说明这种弊端;
代码示例 :
1.代码 :
#include <stdio.h> #define ADD(a, b) a + b #define MUL(a, b) a * b #define _MIN_(a, b) ((a) < (b) ? (a) : b) int main() { int a = 1, b = 10; //宏替换的结果是 : 2 + 3 * 4 + 5, 最终打印结果是 19 printf("%d\n", MUL(ADD(2, 3), ADD(4, 5))); //宏替换的结果是 ((a++) < (b) ? (a++) : b), 打印结果是 2 printf("%d\n", _MIN_(a++, b)); return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
2.编译运行结果 :
3.查看预编译文件 : 使用 gcc -E test_1.c -o test_1.i 指令, 将预编译文件输出到 test_1.i 目录中; 下面是预编译文件的一部分 ;
int main() { int a = 1, b = 10; printf("%d\n", 2 + 3 * 4 + 5); printf("%d\n", ((a++) < (b) ? (a++) : b)); return 0; } 1 2 3 4 5 6 7 8 9 10
(2) 函数 的 优势 和 弊端
函数的优缺点 :
1.函数优势 : 函数调用需要将实参传递给形参, 没有宏替换这样的副作用;
2.弊端 ( 效率低 ) : 函数执行需要跳转, 同时也需要建立活动对象对象 ( 如 函数栈 ) 来存储相关的信息, 需要牺牲一些性能;
(3) 宏的无可替代性
宏 定义 优势 :
1.宏参数不限定类型 : 宏参数 可以是 任何 C 语言 的实体类型, 如 int, float, char, double 等;
2.宏参数可以使类型名称 : 类型的名称也可以作为宏的参数;
//宏定义 : 实现分配 n 个 type 类型空间, 并返回 type 类型指针
#define MALLOC(type, n) (type*)malloc(n * sizeof(type))
//分配 5 个 float 大小的动态空间, 并将首地址存放在 指针 p 中;
float *p = MALLOC(int, 5);
1
2
3
4
5
4. 总结
(1) 宏 定义 和 函数 总结
宏定义 和 函数 小结 :
1.宏定义 : 宏 的 参数 可以 是 C 语言中 的 任何类型的 ( 优势 ) , 宏的执行效率 高 ( 优势 ), 但是容易出错 ( 弊端 ); 2.函数 : 函数 参数 的 类型是固定的, 其 执行效率低于宏, 但是不容易出错; 3.宏定义 和 函数之间的关系 : 这两者不是竞争对手, 宏定义可以实现一些函数无法实现的功能;
四. 函数的调用约定
1. 函数的活动记录 分析
(1) 函数的活动记录
活动记录概述 : 函数调用时 将 下面一系列的信息 记录在 活动记录中 ;
1.临时变量域 : 存放一些运算的临时变量的值, 如自增运算, 在到顺序点之前的数值是存在临时变量域中的;
后置操作 自增 原理 : i++ 自增运算 进行的操作 :
( 1 ) 生成临时变量 : 在内存中生成临时变量 tmp ;
( 2 ) 临时变量赋值 : 将 i 的值赋值给临时变量, tmp = i ;
( 3 ) 进行加 1 操作 : 将 i + 1 并赋值给 i;
示例 : 定义函数 fun(int a, int b), 传入 fun(i, i++), 传入后 获取的实参值分别是 2 和 1;
在函数传入参数达到顺序点之后开始取值, 函数到达顺序点之后, 上面的三个步骤就执行完毕, 形参 a 从内存中取值, i 的值是2, 形参 b 从临时变量域中取值, 即 tmp 的值, 取值是 1;
2.局部变量域 : 用于存放 函数 中定义 的局部变量, 该变量的生命周期是局部变量执行完毕;
3.机器状态域 : 保存 函数调用 之前 机器状态 相关信息, 包括 寄存器值 和 返回地址, 如 esp 指针, ebp 指针;
4.实参数域 : 保存 函数的实参信息 ;
5.返回值域 : 存放 函数的返回值 ;
2. 函数的调用约定概述
(1) 参数入栈 问题描述
参数入栈问题 : 函数参数的计算次序是不固定的, 严重依赖于编译器的实现, 编译器中函数参数入栈次序;
1.参数传递顺序 : 函数的参数 实参传递给形参 是从左到右传递 还是 从右到左传递;
2.堆栈清理 : 是函数的调用者清理 还是 由 函数本身清理 ;
参数入栈 栈维护 问题示例 :
1.多参数函数定义 : 定义一个函数 fun(int a, int b, int c) , 其中有 3 个参数;
2.函数调用 : 当发生函数调用时 fun(1, 2, 3), 传入三个 int 类型的参数, 这三个参数肯定有一个传递顺序, 这个传递顺序可以约定;
( 1 ) 从左向右入栈 : 将 1, 2, 3 依次 传入 函数中 ;
( 2 ) 从右向左入栈 : 将 3, 2, 1 依次 传入 函数中 ;
3.栈维护 : 在 fun1() 函数中 调用 fun2() 函数, 会创建 fun2() 函数的 活动记录 (栈), 当 fun2() 函数执行完毕 返回的时候, 该 fun2 函数的栈空间是由谁 ( fun1 或 fun2 函数 ) 负责释放的;
函数参数计算次序依赖于编辑器实现, 函数参数入栈的顺序可以自己设置;
(2) 参数传递顺序的调用约定
函数参数调用约定 :
1.函数调用行为 : 函数调用时 参数 传递给 被调用的 函数, 返回值被返回给 调用函数 ;
2.调用约定作用 : 调用约定 是 用来规定 ① 参数 是通过什么方式 传递到 栈空间 ( 活动记录 ) 中, ② 栈 由谁来 清理 ;
3.参数传递顺序 ( 右到左 ) : 从右到左入栈使用 __stdcall, __cdecl, __thiscall 关键字, 放在 函数返回值之前;
4.参数传递顺序 ( 左到右 ) : 从左到右入栈使用 __pascal, __fastcall 关键字, 放在 函数返回值之前;
5.调用堆栈的清理工作 : ① 调用者负责清理调用堆栈; ② 被调用的函数返回之前清理堆栈;
五. 函数设计技巧
函数设计技巧 :
1.避免使用全局变量 : 在函数中尽量避免使用全局变量, 让函数形成一个独立功能模块;
2.参数传递全局变量 : 如果必须使用到全局变量, 那么多设计一个参数, 用于传入全局变量;
3.参数名称可读性 : 尽量不要使用无意义的字符串作为参数变量名;
4.参数常量 : 如果参数是一个指针, 该指针仅用于输入作用, 尽量使用 const 修饰该指针参数, 防止该指针在函数体内被修改;
//这里第二个参数仅用于输入, 不需要修改该指针, 那么就将该参数设置成常量参数
void fun(char *dst, const char* src);
1
2
5.返回类型不能省略 : 函数的返回类型不能省略, 如果省略了返回值, 那么返回值默认 int;
6.参数检测 : 在函数开始位置, 需要检测函数参数的合法性, 避免不必要的错误, 尤其是指针类型的参数;
7.栈内存指针 : 返回值 绝对不能是 局部变量指针, 即 指针指向的位置是 栈内存位置, 栈内存在返回时会销毁, 不能再函数运行结束后使用 ;
8.代码量 : 函数的代码量尽量控制在一定数目, 50 ~ 80 行, 符合模块化设计规则;
9.输入输出固定 : 函数在输入相同的参数, 其输出也要相同, 尽量不要在函数体内使用 static 局部变量, 这样函数带记忆功能, 增加函数的复杂度;
10.参数控制 : 编写函数的时候, 函数的参数尽量控制在 4 个以内, 方便使用;
11.函数返回值设计 : 有时候函数不需要返回值, 或者返回值使用指针参数设置, 但是为了增加灵活性, 可以附加返回值; 如 支持 链式表达式 功能;