函数
# 06.函数
# 目录介绍
# 6.1 概述和定义
在 C 语言中,函数是程序的基本构建块,用于将代码组织成可重用的模块。函数可以接收输入参数,执行特定任务,并返回结果。通过使用函数,可以提高代码的可读性、可维护性和复用性。
# 6.1.1 函数概念
函数的基本概念
- 函数定义:实现函数功能的代码块。
- 函数声明:告诉编译器函数的名称、返回类型和参数列表。
- 函数调用:执行函数中的代码。
# 6.1.2 函数的定义
语法
返回类型 函数名(参数列表) {
// 函数体
return 返回值; // 如果返回类型不是 void
}
2
3
4
示例
int add(int a, int b) {
return a + b;
}
2
3
# 6.1.3 函数声明
函数声明(也称为函数原型)告诉编译器函数的名称、返回类型和参数列表。
语法
返回类型 函数名(参数列表);
示例
int add(int a, int b);
原理说明:函数声明(原型)的作用是让编译器在调用函数之前就知道函数的签名信息。C语言采用单遍编译模型,编译器从上到下处理源文件。如果没有提前声明,编译器遇到未知函数时会隐式假设其返回 int,这在C99之后已被废弃。函数原型使编译器能够在编译期检查参数类型和数量是否匹配,是类型安全的重要保障。
# 6.1.4 综合案例与思考
综合案例:函数定义、声明与使用的完整流程
#include <stdio.h>
// 函数声明(原型)——告诉编译器函数签名
int factorial(int n);
double power(double base, int exp);
void print_separator(int length);
int main() {
// 案例1:阶乘函数
for (int i = 0; i <= 10; i++) {
printf("%2d! = %d\n", i, factorial(i));
}
// 案例2:幂运算
print_separator(30);
printf("2^10 = %.0f\n", power(2.0, 10));
printf("3.14^2 = %.4f\n", power(3.14, 2));
// 案例3:函数的嵌套调用
print_separator(30);
// 组合数 C(n,k) = n! / (k! * (n-k)!)
int n = 10, k = 3;
int combination = factorial(n) / (factorial(k) * factorial(n - k));
printf("C(%d,%d) = %d\n", n, k, combination);
return 0;
}
// 函数定义
int factorial(int n) {
if (n <= 1) return 1;
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
double power(double base, int exp) {
double result = 1.0;
for (int i = 0; i < exp; i++) {
result *= base;
}
return result;
}
void print_separator(int length) {
for (int i = 0; i < length; i++) {
printf("-");
}
printf("\n");
}
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
原理说明:函数是C语言程序的基本构建块,每个C程序至少有一个函数(main)。函数的三要素是:返回类型、函数名、参数列表。编译器处理函数时会生成一个符号表条目,链接器通过符号表将函数调用与函数定义关联起来。将声明放在文件头部、定义放在 main 之后是一种常见的代码组织方式,可以让 main 函数更早出现,便于阅读。
思考题:
- 如果不写函数声明,直接在
main下方定义函数,会发生什么?C89和C99对此行为有何不同? - 函数声明中参数名可以省略(如
int add(int, int);),为什么允许这样做?什么时候需要保留参数名? - 递归版本的
factorial和循环版本相比,各有什么优缺点?
# 6.2.1 调用函数语法
通过函数名和参数列表调用函数。
语法
函数名(参数列表);
示例
int result = add(3, 5);
# 6.2.2 理解栈帧
当函数被调用时,系统会在 stack 空间上申请一块内存,用来给函数调用提供空间。存储 形参 和 局部变量(定义在函数内部的变量)。
函数调用结束时,这块内存空间,会被自动释放 (消失) 。
原理说明:栈帧(Stack Frame)是函数调用的核心机制。每次函数调用时,系统会在栈上创建一个栈帧,包含:返回地址(调用完成后跳转回哪里)、函数参数(按照调用约定压栈)、局部变量、保存的寄存器值。函数返回时,栈帧被弹出,栈指针恢复到调用前的位置。这就是为什么局部变量在函数返回后就"消失"了——它们所在的内存已被回收。栈的大小是有限的(通常几MB),递归调用过深会导致栈溢出(Stack Overflow)。
# 6.2.3 综合案例与思考
综合案例:理解函数调用过程和栈帧
#include <stdio.h>
void func_c() {
int c = 30;
printf(" func_c: c的地址 = %p\n", (void *)&c);
printf(" func_c: 栈帧最深处\n");
}
void func_b() {
int b = 20;
printf(" func_b: b的地址 = %p\n", (void *)&b);
func_c();
printf(" func_b: func_c返回后继续执行\n");
}
void func_a() {
int a = 10;
printf("func_a: a的地址 = %p\n", (void *)&a);
func_b();
printf("func_a: func_b返回后继续执行\n");
}
// 递归演示栈帧增长
void recursive(int depth) {
int local_var = depth;
printf("递归深度 %d: local_var地址 = %p\n", depth, (void *)&local_var);
if (depth < 5) {
recursive(depth + 1);
}
}
int main() {
printf("=== 函数调用链演示 ===\n");
int m = 0;
printf("main: m的地址 = %p\n\n", (void *)&m);
func_a();
printf("\n=== 递归栈帧演示 ===\n");
recursive(1);
// 观察地址:每深入一层,地址向低地址方向增长(栈向下生长)
return 0;
}
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
原理说明:通过观察局部变量的地址可以直观看到栈的增长方向——在大多数平台上,栈从高地址向低地址增长。每次函数调用都会消耗一定的栈空间,因此递归深度受栈大小限制。通过 ulimit -s 可查看/修改栈大小(Linux系统)。
思考题:
- 运行上面的程序,观察变量地址的变化规律,能得出栈增长方向的结论吗?
- 如果一个函数的局部数组很大(如
int arr[1000000]),可能会发生什么?如何解决? - 函数调用有开销(保存/恢复寄存器、压栈弹栈),
inline关键字如何优化这个问题?
# 6.3.1 参数
- 形式参数:函数定义中的参数。
- 实际参数:函数调用时传递的参数。
示例
int add(int a, int b) { // a 和 b 是形式参数
return a + b;
}
int result = add(3, 5); // 3 和 5 是实际参数
2
3
4
5
# 6.3.2 默认参数
C语言不支持默认参数(这是C++的特性)。但可以通过以下技巧模拟:
方法1:使用宏定义
#define print_msg(msg, times) _print_msg(msg, times)
#define print_msg_default(msg) _print_msg(msg, 1)
void _print_msg(const char *msg, int times) {
for (int i = 0; i < times; i++) {
printf("%s\n", msg);
}
}
2
3
4
5
6
7
8
方法2:使用特殊值标记
void draw_circle(int x, int y, int radius) {
if (radius <= 0) radius = 10; // 默认半径为10
printf("画圆: (%d,%d) 半径=%d\n", x, y, radius);
}
2
3
4
# 6.3.3 传值和传址
C语言中函数参数默认是值传递,即函数接收的是参数的副本,修改副本不影响原值。要修改原值,需要传递指针(传址)。
值传递示例
void try_modify(int x) {
x = 100; // 只修改了副本
printf("函数内 x = %d\n", x);
}
int main() {
int a = 10;
try_modify(a);
printf("函数外 a = %d\n", a); // a仍然是10
return 0;
}
2
3
4
5
6
7
8
9
10
11
传址(指针)示例
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap(&x, &y);
printf("x=%d, y=%d\n", x, y); // x=20, y=10
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 6.3.4 综合案例与思考
综合案例:传值与传址的深入理解
#include <stdio.h>
#include <stdlib.h>
// 值传递:无法修改外部变量
void bad_alloc(int *p) {
p = (int *)malloc(sizeof(int)); // 只修改了指针的副本!
if (p) *p = 42;
}
// 传址:通过二级指针修改指针本身
void good_alloc(int **pp) {
*pp = (int *)malloc(sizeof(int));
if (*pp) **pp = 42;
}
// 通过返回值传递结果
int *return_alloc() {
int *p = (int *)malloc(sizeof(int));
if (p) *p = 42;
return p;
}
// 数组作为参数:天然是传址
void double_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // 直接修改原数组
}
}
int main() {
// 案例1:错误的动态分配
int *p1 = NULL;
bad_alloc(p1);
printf("bad_alloc后: p1 = %p (仍为NULL!)\n", (void *)p1);
// 案例2:正确的动态分配
int *p2 = NULL;
good_alloc(&p2);
printf("good_alloc后: p2 = %p, *p2 = %d\n", (void *)p2, *p2);
free(p2);
// 案例3:通过返回值
int *p3 = return_alloc();
printf("return_alloc后: *p3 = %d\n", *p3);
free(p3);
// 案例4:数组天然传址
int arr[] = {1, 2, 3, 4, 5};
printf("\n原数组: ");
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
double_array(arr, 5);
printf("\n翻倍后: ");
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
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
原理说明:C语言只有值传递这一种参数传递方式。所谓"传址"其实是传递了指针的值(即地址的副本)。要修改指针本身(而非指针指向的内容),需要传递指针的指针(二级指针)。数组作为参数时会退化为指针,所以看起来像传址——函数内可以直接修改原数组。这是C语言中最容易混淆的概念之一。
思考题:
- 为什么
bad_alloc函数无法正确分配内存给外部指针?画出函数调用时内存的变化过程。 - 结构体作为函数参数是值传递还是传址?传递大结构体时有什么性能问题?如何优化?
const int *p和int *const p作为函数参数时各有什么含义?
- 使用
return语句返回函数的结果。 - 如果返回类型是
void,则不需要return语句。
# 6.4.1 void返回
void printHello() {
printf("Hello, World!\n");
}
2
3
# 6.4.2 return返回
int add(int a, int b) {
return a + b;
}
2
3
# 6.5 函数分类
(1) 无参数无返回值函数
void printHello() {
printf("Hello, World!\n");
}
2
3
(2) 有参数无返回值函数
void printSum(int a, int b) {
printf("Sum: %d\n", a + b);
}
2
3
(3) 无参数有返回值函数
int getRandomNumber() {
return rand();
}
2
3
(4) 有参数有返回值函数
int add(int a, int b) {
return a + b;
}
2
3
# 6.6 函数与数组
# 6.6.1 数组作为函数参数
数组可以作为函数参数传递,实际传递的是数组的首地址。
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 6.6.2 多维数组作为函数参数
void printMatrix(int mat[3][3], int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
}
2
3
4
5
6
7
8
# 6.6.3 综合案例与思考
综合案例:函数与数组的综合运用
#include <stdio.h>
// 数组求和
int array_sum(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
// 数组求平均值
double array_avg(int arr[], int size) {
return (double)array_sum(arr, size) / size;
}
// 查找最大值及其索引
int array_max_index(int arr[], int size) {
int max_idx = 0;
for (int i = 1; i < size; i++) {
if (arr[i] > arr[max_idx]) {
max_idx = i;
}
}
return max_idx;
}
// 数组反转(原地操作)
void array_reverse(int arr[], int size) {
for (int i = 0; i < size / 2; i++) {
int temp = arr[i];
arr[i] = arr[size - 1 - i];
arr[size - 1 - i] = temp;
}
}
// 打印数组
void array_print(const char *label, int arr[], int size) {
printf("%s: ", label);
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int scores[] = {85, 92, 78, 96, 88, 73, 91};
int n = sizeof(scores) / sizeof(scores[0]);
array_print("成绩", scores, n);
printf("总分: %d\n", array_sum(scores, n));
printf("平均分: %.1f\n", array_avg(scores, n));
int max_i = array_max_index(scores, n);
printf("最高分: %d (索引=%d)\n", scores[max_i], max_i);
array_reverse(scores, n);
array_print("反转后", scores, n);
return 0;
}
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
原理说明:数组作为函数参数时,会退化为指向首元素的指针,因此函数内部无法通过 sizeof 获得数组长度,必须额外传递 size 参数。使用 const 修饰数组参数(如 const int arr[])可以防止函数意外修改数组内容。将常用的数组操作封装为函数是良好的编程习惯,可以提高代码复用性。
思考题:
- 为什么数组作为函数参数时,
sizeof(arr)返回的是指针大小而非数组大小? - 如何在函数中"创建"一个数组并返回给调用者?直接返回局部数组有什么问题?
void func(int arr[10])中的10有实际约束作用吗?编译器会检查吗?
C 语言的安全函数通过引入边界检查机制,可以有效避免缓冲区溢出等安全问题。常见的函数包括:
- 字符串操作:
strcpy_s、strcat_s、strncpy_s。 - 输入输出:
gets_s、scanf_s。 - 内存操作:
memcpy_s、memset_s。 - 文件操作:
fopen_s。
# 6.7.1 字符串安全函数
# 1.1 strcpy_s
- 功能:安全地复制字符串。
- 原型:
errno_t strcpy_s(char *dest, rsize_t dest_size, const char *src);1 - 参数:
dest:目标字符串。dest_size:目标字符串的缓冲区大小。src:源字符串。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
char dest[10]; strcpy_s(dest, sizeof(dest), "Hello");1
2
# 1.2 strcat_s
- 功能:安全地连接字符串。
- 原型:
errno_t strcat_s(char *dest, rsize_t dest_size, const char *src);1 - 参数:
dest:目标字符串。dest_size:目标字符串的缓冲区大小。src:源字符串。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
char dest[10] = "Hello"; strcat_s(dest, sizeof(dest), "World");1
2
# 1.3 strncpy_s
- 功能:安全地复制指定长度的字符串。
- 原型:
errno_t strncpy_s(char *dest, rsize_t dest_size, const char *src, rsize_t count);1 - 参数:
dest:目标字符串。dest_size:目标字符串的缓冲区大小。src:源字符串。count:要复制的最大字符数。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
char dest[10]; strncpy_s(dest, sizeof(dest), "HelloWorld", 5);1
2
# 6.7.2 内存操作安全函数
# 3.1 memcpy_s
- 功能:安全地复制内存块。
- 原型:
errno_t memcpy_s(void *dest, rsize_t dest_size, const void *src, rsize_t count);1 - 参数:
dest:目标内存块。dest_size:目标内存块的大小。src:源内存块。count:要复制的字节数。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
char dest[10]; char src[] = "Hello"; memcpy_s(dest, sizeof(dest), src, sizeof(src));1
2
3
# 3.2 memset_s
- 功能:安全地设置内存块的值。
- 原型:
errno_t memset_s(void *dest, rsize_t dest_size, int value, rsize_t count);1 - 参数:
dest:目标内存块。dest_size:目标内存块的大小。value:要设置的值。count:要设置的字节数。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
char buffer[10]; memset_s(buffer, sizeof(buffer), 0, sizeof(buffer));1
2
# 6.7.3 文件操作安全函数
# 4.1 fopen_s
- 功能:安全地打开文件。
- 原型:
errno_t fopen_s(FILE **file, const char *filename, const char *mode);1 - 参数:
file:指向文件指针的指针。filename:文件名。mode:打开模式(如 "r"、"w" 等)。
- 返回值:成功返回 0,失败返回非 0 值。
- 示例:
FILE *file; fopen_s(&file, "test.txt", "r");1
2
# 6.7.4 输入输出安全函数
# 2.1 gets_s
- 功能:安全地从标准输入读取字符串。
- 原型:
char *gets_s(char *buffer, rsize_t size);1 - 参数:
buffer:存储输入字符串的缓冲区。size:缓冲区的大小。
- 返回值:成功返回
buffer,失败返回NULL。 - 示例:
char buffer[10]; gets_s(buffer, sizeof(buffer));1
2
# 2.2 scanf_s
- 功能:安全地从标准输入读取格式化数据。
- 原型:
int scanf_s(const char *format, ...);1 - 参数:
format:格式化字符串。...:可变参数列表。
- 返回值:成功返回读取的项数,失败返回
EOF。 - 示例:
char buffer[10]; scanf_s("%9s", buffer, sizeof(buffer));1
2
# 6.8 标准库函数
C 语言提供了丰富的标准库函数,如:
- 输入输出:
printf、scanf - 字符串操作:
strcpy、strlen - 数学函数:
sqrt、pow - 内存管理:
malloc、free
示例
#include <stdio.h>
#include <string.h>
#include <math.h>
int main() {
char str[50];
strcpy(str, "Hello, World!");
printf("%s\n", str);
double result = sqrt(16.0);
printf("Square root: %f\n", result);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.9 函数注意事项
函数声明:
- 如果函数定义在调用之后,需要先声明函数。
参数传递:
- C 语言中,参数是按值传递的(指针除外)。
返回值:
- 如果函数有返回值,必须使用
return语句。
- 如果函数有返回值,必须使用
递归:
- 递归函数必须有终止条件,否则会导致栈溢出。
函数命名:
- 函数名应具有描述性,遵循命名规范。
# 6.9.1 综合案例与思考
综合案例:函数的综合应用——简易计算器
#include <stdio.h>
// 函数声明
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
// 函数指针类型定义
typedef double (*operation)(double, double);
// 获取操作函数
operation get_operation(char op) {
switch (op) {
case '+': return add;
case '-': return subtract;
case '*': return multiply;
case '/': return divide;
default: return NULL;
}
}
int main() {
// 测试数据
double a = 10.0, b = 3.0;
char operators[] = {'+', '-', '*', '/'};
printf("=== 简易计算器 ===\n");
for (int i = 0; i < 4; i++) {
operation op = get_operation(operators[i]);
if (op) {
printf("%.1f %c %.1f = %.2f\n", a, operators[i], b, op(a, b));
}
}
// 函数指针数组
printf("\n=== 使用函数指针数组 ===\n");
operation ops[] = {add, subtract, multiply, divide};
const char *names[] = {"加法", "减法", "乘法", "除法"};
for (int i = 0; i < 4; i++) {
printf("%s: %.2f\n", names[i], ops[i](a, b));
}
return 0;
}
double add(double a, double b) { return a + b; }
double subtract(double a, double b) { return a - b; }
double multiply(double a, double b) { return a * b; }
double divide(double a, double b) {
if (b == 0) {
printf("错误: 除数不能为0!\n");
return 0;
}
return a / b;
}
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
原理说明:函数指针是C语言实现回调机制和策略模式的核心手段。函数名本身就是指向函数代码起始地址的指针。typedef 可以简化函数指针的类型声明。函数指针数组常用于实现命令分发、事件处理等场景,比大量的 if-else 或 switch-case 更优雅、更易扩展。标准库中的 qsort、bsearch 都使用函数指针作为参数。
思考题:
- 函数指针和普通指针有什么区别?
sizeof一个函数指针的结果是什么? qsort函数要求传入一个比较函数指针,如何用它对整数数组和字符串数组分别进行排序?static函数有什么特点?在多文件项目中如何合理使用static限制函数的可见性?