【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ](三)

简介: 【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ](三)

三. 函数 与 宏





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.编译运行结果 :

image.png

虽然看起来 函数 和 宏实现了相同的功能, 但是它们有很大的区别;






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.堆栈清理 : 是函数的调用者清理 还是 由 函数本身清理 ;


image.png



参数入栈 栈维护 问题示例 :


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.函数返回值设计 : 有时候函数不需要返回值, 或者返回值使用指针参数设置, 但是为了增加灵活性, 可以附加返回值; 如 支持 链式表达式 功能;


目录
相关文章
|
13天前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
52 23
|
13天前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
44 15
|
13天前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
53 24
|
9天前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
46 16
|
8天前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
19 3
|
8天前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
11 2
|
12天前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
41 1
|
1月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
81 10
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
62 9
|
1月前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
55 8

热门文章

最新文章