函数最重要的概念就是模块化编程。模块化程序设计。
函数结构:
一个函数的基本结构:
1、函数声明:声明函数的返回类型、名称和参数类型;
2、函数定义:编写函数的实际代码实现;
3、函数调用,早需要的地方调用函数。
声明与定义:
程序中的声明可以理解为预先告诉编译器实体的存在,如变量,函数等;
程序中的定义是明确指示编译器实体的意义。
函数示例:
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int sum;
// 函数调用
sum = add(5, 3);
printf("Sum: %d\n", sum);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
函数参数:
函数参数在本质上与局部变量相同,都是子啊栈上分配空间;
函数参数的初始值是函数调用时的实参值;
顺序点概念:
顺序点(Sequence Point) ,它定义了表达式中的某些时刻,在这些时刻之前的所有副作用(如修改变量、IO操作等)都已完成,且这些副作用的结果已经生效。
- 每条语句结束时是一个顺序点。在这之前的所有副作用都已生效。
- 逻辑与(&&)和逻辑或(||)运算符的左侧表达式求值完成后是一个顺序点。如果左侧表达式决定了逻辑结果,右侧表达式可能不再求值(短路求值)
- 条件表达式(
?:
)的条件部分求值完成后是一个顺序点; - 在函数调用中,所有参数的求值和副作用完成后是一个顺序点,然后才开始执行函数体。
- 逗号运算符的左侧表达式求值完成并应用副作用后是一个顺序点,然后才开始右侧表达式的求值。
函数概念小结:
- c语言是一种面向过程的语言;
- 函数可以理解为解决问题的步骤;
- 函数的实参并没有固定的计算次序;
- 顺序点是c语言中变量改变的最晚时机;
- 函数定义是参数个返回值的缺省类型为int;
可变参数列表:
在c语言中可以定义参数可变的函数;参数可变函数的实现依赖于stdarg.h头文件;va_list变量与va_start、va_arg和va_end配合使用能够访问参数值。
示例:
#include <stdio.h>
#include <stdarg.h>
// 定义一个可变参数函数,计算参数的总和
int sum(int count, ...) {
va_list args;
int total = 0;
// 初始化参数列表
va_start(args, count);
// 逐个获取参数并累加
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
// 结束参数处理
va_end(args);
return total;
}
int main() {
// 调用可变参数函数
int result1 = sum(3, 1, 2, 3); // 传递3个参数
int result2 = sum(5, 10, 20, 30, 40, 50); // 传递5个参数
printf("Sum of 3 numbers: %d\n", result1);
printf("Sum of 5 numbers: %d\n", result2);
return 0;
}
- 包含
<stdarg.h>
头文件。 - 使用省略号表示可变参数。
- 使用
va_list
类型变量来存储参数列表。 - 使用
va_start
宏初始化参数列表。 - 使用
va_arg
宏获取参数列表中的参数。 - 使用
va_end
宏结束参数处理。
在示例代码中:va_list
:声明一个变量,用于存储参数列表;va_start
:初始化参数列表。第一个参数是va_list
类型的变量,第二个参数是最后一个固定参数,即可变参数之前的那个参数。va_arg
:获取参数列表中的下一个参数。第一个参数是va_list
类型的变量,第二个参数是要获取的参数类型。
va_end:结束参数处理,通常在函数返回前调用。
函数 VS 宏
1、宏是由预处理器直接在程序中替换展开的,编译器不知道宏的存在;
2、函数是由编译器直接编译的实体,调用行为由编译器决定;
3、多次使用宏会导致程序代码量增加;
4、函数是跳转执行的,因此代码量不会增加;
5、宏的效率比函数要高,因为是直接展开,无调用开销;
6、函数调用时会创建活动记录,效率不如宏。
宏定义的详细作用请参考前面章节。总之宏的好处是代码替换,但是缺点是不安全且难以调试。
函数的调用行为:
在程序执行过程中,调用函数会用活动记录,也称为栈帧,这是一个基于栈的数据结构,用于存储函数调用期间所需的所有信息。这些信息包括:
返回地址、参数、局部变量、保存的寄存器、静态链、帧指针。
参考上一节中栈的学习记录,给出一个示例:
在示例中主函数调用函数A,函数A中调用函数B:
#include <stdio.h>
void functionB(int b) {
int y = b * 2;
printf("functionB: y = %d\n", y);
}
void functionA(int a) {
int x = a + 5;
functionB(x);
}
int main() {
int n = 10;
functionA(n);
return 0;
}
则有:
main
函数调用 functionA
+-----------------+ <--- 栈顶(Stack Top)
| Return Address | <--- `main`的返回地址
| Saved FP | <--- `main`的帧指针
| n = 10 | <--- `main`的局部变量
+-----------------+
functionA
调用 functionB
+-----------------+ <--- 栈顶(Stack Top)
| Return Address | <--- `functionA`的返回地址
| Saved FP | <--- `functionA`的帧指针
| x = 15 | <--- `functionA`的局部变量
+-----------------+
| Return Address | <--- `functionB`的返回地址
| Saved FP | <--- `functionB`的帧指针
| b = 15 | <--- `functionB`的参数
| y = 30 | <--- `functionB`的局部变量
+-----------------+
递归函数:
递归函数是指在函数定义过程中直接或者间接调用自身的函数。
递归函数的重要思想是分而治之。
递归必须有一个基准条件,确保递归不会无限进行。当满足基准条件时,递归停止。
函数设计技巧:
1、不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块;
2、函数名要能够体现参数的意义;
3、不要省略返回值的类型,如果函数没有返回值,那么应该声明为void类型;
4、在函数体的入口处,对参数的有效性进行检查,对指针的检查尤为重要;
5、语句不可范围指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁;这也就是内存管理中的一个错误;