第5章 函数

函数的基本概念

函数是 C 语言中组织代码的基本单位,它是一个完成特定任务的代码块,具有名称、参数和返回值。函数将相关的代码组织在一起,形成一个独立的、可重用的模块。

函数的优点

  1. 代码重用 - 可以在多个地方调用同一个函数,避免代码重复
  2. 模块化 - 将复杂问题分解为更小的部分,提高代码的可维护性
  3. 可读性 - 使代码结构更加清晰,便于理解和调试
  4. 抽象 - 隐藏实现细节,只暴露接口,降低代码复杂度
  5. 可测试性 - 便于对单个功能进行测试和验证
  6. 协作开发 - 多人可以同时开发不同的函数,提高开发效率

函数的设计原则

  1. 单一职责 - 每个函数应该只负责一个特定的任务
  2. 函数名清晰 - 函数名应该准确反映其功能
  3. 参数适量 - 函数参数不宜过多,一般不超过 5-6 个
  4. 返回值明确 - 函数的返回值应该有明确的含义
  5. 避免副作用 - 函数应该尽量避免修改全局变量或产生其他副作用
  6. 错误处理 - 函数应该适当处理错误情况

函数的声明和定义

函数声明

函数声明告诉编译器函数的名称、返回类型和参数列表,也称为函数原型。

1
2
3
4
5
6
7
// 函数声明
return_type function_name(parameter_list);

// 示例
int add(int a, int b);
void print_hello(void);
float calculate_average(int numbers[], int size);

函数定义

函数定义包含函数的实现代码,包括函数头和函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 函数定义
return_type function_name(parameter_list)
{
// 函数体
// 执行任务的代码
return return_value; // 对于非 void 函数
}

// 示例
int add(int a, int b)
{
int sum = a + b;
return sum;
}

void print_hello(void)
{
printf("Hello, World!\n");
}

函数定义的组成部分

  1. 返回类型 - 函数返回值的类型,可以是任何有效的 C 数据类型
  2. 函数名 - 函数的标识符,应遵循 C 语言的命名规则
  3. 参数列表 - 函数接收的参数,每个参数包含类型和名称
  4. 函数体 - 包含函数实现的代码块,用大括号包围
  5. return 语句 - 用于返回函数值,结束函数执行

函数的调用

要使用函数,需要调用它并提供必要的参数。

函数调用的语法

1
2
3
4
5
6
7
// 调用函数
return_value = function_name(arguments);

// 示例
int result = add(5, 3);
print_hello();
float avg = calculate_average(numbers, 10);

函数调用的执行过程

  1. 参数传递 - 将实际参数的值传递给形式参数
  2. 执行函数体 - 执行函数内部的代码
  3. 返回值 - 将函数的返回值传递给调用者
  4. 继续执行 - 从函数调用处继续执行后续代码

函数调用的栈帧

当函数被调用时,系统会在栈上为其分配一个栈帧,用于存储:

  • 函数的返回地址
  • 函数的参数
  • 函数的局部变量
  • 函数的返回值(如果需要)

函数执行完毕后,栈帧会被自动释放。

函数的参数

形式参数和实际参数

  • 形式参数 (形参) - 函数定义中声明的参数,是函数的局部变量
  • 实际参数 (实参) - 函数调用时提供的参数,用于初始化形参
1
2
3
4
5
6
7
8
// 形式参数:a 和 b
int add(int a, int b)
{
return a + b;
}

// 实际参数:5 和 3
int result = add(5, 3);

参数传递方式

C 语言中,函数参数的传递方式是值传递,即传递的是参数的副本。

值传递示例

1
2
3
4
5
6
7
8
9
10
11
12
13
void increment(int x)
{
x++; // 修改的是副本
printf("函数内 x = %d\n", x); // 输出 6
}

int main(void)
{
int x = 5;
increment(x); // 传递 x 的副本
printf("函数外 x = %d\n", x); // 输出 5,原值不变
return 0;
}

指针参数

要在函数中修改实参的值,可以使用指针参数。指针参数传递的是变量的地址,通过指针可以修改原变量的值。

指针参数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
void increment(int *x)
{
(*x)++; // 通过指针修改原值
printf("函数内 *x = %d\n", *x); // 输出 6
}

int main(void)
{
int x = 5;
increment(&x); // 传递 x 的地址
printf("函数外 x = %d\n", x); // 输出 6,原值已修改
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
void print_array(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

void modify_array(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
arr[i] *= 2; // 修改数组元素
}
}

int main(void)
{
int numbers[] = {1, 2, 3, 4, 5};

printf("修改前:");
print_array(numbers, 5); // 传递数组名(数组首地址)

modify_array(numbers, 5); // 修改数组元素

printf("修改后:");
print_array(numbers, 5); // 数组元素已被修改

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
struct Point {
int x;
int y;
};

// 值传递
void print_point(struct Point p)
{
printf("Point: (%d, %d)\n", p.x, p.y);
}

// 指针传递
void modify_point(struct Point *p)
{
p->x += 10;
p->y += 10;
}

int main(void)
{
struct Point p = {5, 10};

printf("修改前:");
print_point(p);

modify_point(&p);

printf("修改后:");
print_point(p);

return 0;
}

可变参数

C 语言支持可变参数函数,即函数的参数数量可以变化。这需要使用 stdarg.h 头文件中的宏。

可变参数示例

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
#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(void)
{
printf("sum(3, 1, 2, 3) = %d\n", sum(3, 1, 2, 3));
printf("sum(5, 10, 20, 30, 40, 50) = %d\n", sum(5, 10, 20, 30, 40, 50));
return 0;
}

函数的返回值

返回值的类型

函数可以返回各种类型的值,包括基本类型、指针类型和结构体类型。

返回基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b)
{
return a + b;
}

float divide(int a, int b)
{
if (b == 0)
{
printf("错误:除数不能为零\n");
return 0.0f;
}
return (float)a / b;
}

返回指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int* create_array(int size)
{
int* arr = (int*)malloc(size * sizeof(int));
if (arr == NULL)
{
printf("错误:内存分配失败\n");
return NULL;
}
// 初始化数组
for (int i = 0; i < size; i++)
{
arr[i] = i;
}
return arr;
}

void free_array(int* arr)
{
if (arr != NULL)
{
free(arr);
arr = NULL;
}
}

返回结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Point get_midpoint(struct Point p1, struct Point p2)
{
struct Point mid;
mid.x = (p1.x + p2.x) / 2;
mid.y = (p1.y + p2.y) / 2;
return mid;
}

// 返回结构体指针
struct Point* create_point(int x, int y)
{
struct Point* p = (struct Point*)malloc(sizeof(struct Point));
if (p != NULL)
{
p->x = x;
p->y = y;
}
return p;
}

void 函数

void 函数不返回任何值。

1
2
3
4
5
6
7
8
9
10
11
void print_message(const char* message)
{
printf("%s\n", message);
// 没有 return 语句
}

void clear_screen(void)
{
system("clear"); // 或 system("cls") 在 Windows 上
return; // 可选的 return 语句
}

返回值的注意事项

  1. 返回局部变量的地址 - 不要返回局部变量的地址,因为局部变量在函数执行完毕后会被释放
  2. 返回值的类型转换 - 确保返回值的类型与函数声明的返回类型一致
  3. 错误处理 - 函数应该适当处理错误情况,并返回有意义的错误值
  4. 返回大型结构体 - 对于大型结构体,考虑返回指针而不是值,以提高性能

函数原型

函数原型是函数声明的另一种形式,它告诉编译器函数的签名(返回类型、名称和参数类型)。

为什么需要函数原型?

  1. 允许函数在定义之前被调用 - 函数可以在定义之前被调用,提高代码的灵活性
  2. 帮助编译器检查函数调用是否正确 - 编译器可以检查参数的类型和数量是否匹配
  3. 提高代码可读性 - 函数原型可以作为函数的文档,说明函数的接口
  4. 支持分离编译 - 函数可以在不同的文件中定义,通过头文件中的函数原型进行声明

函数原型的语法

1
2
3
4
5
6
7
8
9
// 完整的函数原型(包含参数名)
return_type function_name(type1 param1, type2 param2, ...);

// 简化的函数原型(省略参数名)
return_type function_name(type1, type2, ...);

// 示例
int add(int a, int b); // 完整形式
int add(int, int); // 简化形式

函数原型的位置

函数原型通常放在以下位置:

  1. 头文件中 - 对于需要被多个文件使用的函数,函数原型应该放在头文件中
  2. 源文件的顶部 - 对于只在当前文件中使用的函数,函数原型可以放在源文件的顶部

头文件示例

1
2
3
4
5
6
7
8
9
10
11
// math_functions.h
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H

// 函数原型
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
float divide(int a, int b);

#endif // MATH_FUNCTIONS_H
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
// math_functions.c
#include "math_functions.h"

int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

int multiply(int a, int b)
{
return a * b;
}

float divide(int a, int b)
{
if (b == 0)
{
return 0.0f;
}
return (float)a / b;
}
1
2
3
4
5
6
7
8
9
10
11
12
// main.c
#include <stdio.h>
#include "math_functions.h"

int main(void)
{
printf("5 + 3 = %d\n", add(5, 3));
printf("5 - 3 = %d\n", subtract(5, 3));
printf("5 * 3 = %d\n", multiply(5, 3));
printf("5 / 3 = %.2f\n", divide(5, 3));
return 0;
}

递归函数

递归函数是调用自身的函数。递归是一种强大的编程技术,适合解决可以分解为相同子问题的问题。

递归的基本结构

1
2
3
4
5
6
7
8
9
10
11
return_type recursive_function(parameters)
{
if (base_case) // 基本情况,终止递归
{
return base_value;
}
else // 递归情况,调用自身
{
return recursive_function(modified_parameters);
}
}

示例:计算阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int factorial(int n)
{
if (n <= 1) // 基本情况
{
return 1;
}
else // 递归情况
{
return n * factorial(n - 1);
}
}

int main(void)
{
int result = factorial(5); // 5! = 120
printf("5! = %d\n", result);
return 0;
}

示例:斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int fibonacci(int n)
{
if (n <= 1) // 基本情况
{
return n;
}
else // 递归情况
{
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

int main(void)
{
for (int i = 0; i < 10; i++)
{
printf("%d ", fibonacci(i)); // 输出:0 1 1 2 3 5 8 13 21 34
}
printf("\n");
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
int binary_search(int arr[], int low, int high, int target)
{
if (low > high) // 基本情况:未找到目标
{
return -1;
}

int mid = (low + high) / 2;

if (arr[mid] == target) // 基本情况:找到目标
{
return mid;
}
else if (arr[mid] > target) // 递归情况:在左半部分查找
{
return binary_search(arr, low, mid - 1, target);
}
else // 递归情况:在右半部分查找
{
return binary_search(arr, mid + 1, high, target);
}
}

int main(void)
{
int arr[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int size = sizeof(arr) / sizeof(arr[0]);
int target = 7;
int result = binary_search(arr, 0, size - 1, target);

if (result != -1)
{
printf("元素 %d 在数组中的索引为 %d\n", target, result);
}
else
{
printf("元素 %d 不在数组中\n", target);
}

return 0;
}

递归的优缺点

优点

  1. 代码简洁 - 递归代码通常比迭代代码更简洁、更易理解
  2. 逻辑清晰 - 递归能够直接反映问题的结构
  3. 适合分治问题 - 递归特别适合解决分治类型的问题,如排序、搜索等

缺点

  1. 可能导致栈溢出 - 递归深度过大时,可能导致栈溢出
  2. 执行效率可能低于迭代 - 递归调用需要额外的栈空间和函数调用开销
  3. 可能产生重复计算 - 如斐波那契数列的递归实现会产生大量重复计算

递归的优化

  1. 尾递归优化 - 将递归调用放在函数的最后,编译器可以优化为迭代
  2. 记忆化 - 存储已计算的结果,避免重复计算
  3. 限制递归深度 - 对于可能深度过大的递归,设置最大深度限制
  4. 考虑迭代实现 - 对于性能要求高的场景,考虑使用迭代代替递归

尾递归优化示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 普通递归
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1); // 递归调用后还有乘法操作
}

// 尾递归
int factorial_tail(int n, int accumulator)
{
if (n <= 1)
return accumulator;
return factorial_tail(n - 1, n * accumulator); // 递归调用是最后一个操作
}

// 包装函数
int factorial(int n)
{
return factorial_tail(n, 1);
}

记忆化优化示例

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
#include <stdio.h>

#define MAX_N 100

// 存储已计算的结果
int memo[MAX_N];

// 初始化记忆数组
void init_memo(void)
{
for (int i = 0; i < MAX_N; i++)
{
memo[i] = -1;
}
}

// 使用记忆化的斐波那契函数
int fibonacci(int n)
{
if (n <= 1)
{
return n;
}

// 如果已经计算过,直接返回结果
if (memo[n] != -1)
{
return memo[n];
}

// 计算并存储结果
memo[n] = fibonacci(n - 1) + fibonacci(n - 2);
return memo[n];
}

int main(void)
{
init_memo();

for (int i = 0; i < 40; i++)
{
printf("fib(%d) = %d\n", i, fibonacci(i));
}

return 0;
}

内联函数

内联函数是一种特殊的函数,编译器会尝试将其代码直接插入到调用点,而不是进行常规的函数调用。

内联函数的语法

1
2
3
4
5
6
7
8
9
10
11
// 内联函数
inline return_type function_name(parameters)
{
// 函数体
}

// 示例
inline int max(int a, int b)
{
return (a > b) ? a : b;
}

内联函数的优缺点

优点

  1. 减少函数调用开销 - 避免了函数调用的栈操作和跳转开销
  2. 提高执行速度 - 对于短小的函数,内联可以显著提高执行速度
  3. 允许编译器进行更多优化 - 内联后,编译器可以对整个代码进行更全面的优化

缺点

  1. 增加代码大小 - 内联会导致代码膨胀,增加可执行文件的大小
  2. 可能降低缓存命中率 - 代码膨胀可能导致缓存命中率下降
  3. 不适合复杂函数 - 对于复杂的函数,内联可能反而会降低性能

内联函数的使用场景

  1. 短小的函数 - 函数体短小(通常不超过 10-15 行)
  2. 频繁调用的函数 - 被频繁调用的函数,如数学运算、类型转换等
  3. 性能关键路径 - 位于性能关键路径上的函数
  4. 模板函数 - 模板函数的实例化通常是内联的

内联函数的注意事项

  1. inline 只是建议 - inline 关键字只是向编译器提出的建议,编译器可以选择忽略
  2. 内联函数的定义通常放在头文件中 - 因为内联函数需要在调用点可见
  3. 避免递归内联 - 递归函数通常不会被内联
  4. 避免内联复杂函数 - 复杂函数的内联可能会导致代码膨胀和性能下降

函数的存储类别

C 语言中,函数有以下存储类别:

外部函数(默认)

外部函数可以被其他文件中的函数调用。默认情况下,所有函数都是外部函数。

1
2
3
4
5
6
7
8
9
10
11
// 外部函数(默认)
extern int add(int a, int b)
{
return a + b;
}

// 等同于
int add(int a, int b)
{
return a + b;
}

静态函数

静态函数只能在定义它的文件中被调用。使用 static 关键字声明静态函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 静态函数
static void helper_function(void)
{
printf("这是一个静态函数\n");
}

// 只能在同一个文件中调用
int main(void)
{
helper_function();
return 0;
}

存储类别的选择

  1. 外部函数 - 当函数需要被其他文件调用时使用
  2. 静态函数 - 当函数只在当前文件中使用时使用,提高代码的封装性和安全性

函数指针

函数指针是指向函数的指针变量,它可以存储函数的地址并用于调用函数。

函数指针的声明

1
2
3
4
5
6
7
// 声明函数指针
return_type (*pointer_name)(parameter_list);

// 示例
int (*add_ptr)(int, int);
void (*print_ptr)(void);
float (*calculate_ptr)(float, float);

函数指针的初始化

1
2
3
4
5
6
7
8
9
10
// 定义函数
int add(int a, int b)
{
return a + b;
}

// 初始化函数指针
int (*add_ptr)(int, int) = add;
// 或者使用取地址运算符
int (*add_ptr)(int, int) = &add;

函数指针的使用

1
2
3
4
5
6
// 使用函数指针调用函数
int result = add_ptr(5, 3);
printf("5 + 3 = %d\n", result);

// 也可以使用 (*add_ptr) 语法
int result = (*add_ptr)(5, 3);

函数指针的应用

函数指针常用于以下场景:

  1. 回调函数 - 函数作为参数传递给其他函数,在适当的时候被调用
  2. 函数表 - 使用数组存储函数指针,实现类似开关语句的功能
  3. 策略模式 - 通过函数指针选择不同的算法或行为
  4. 事件处理 - 用于处理事件,如按钮点击、定时器触发等

回调函数示例

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
#include <stdio.h>

// 函数类型定义
typedef int (*CompareFunction)(int, int);

// 比较函数
int ascending(int a, int b)
{
return a - b;
}

int descending(int a, int b)
{
return b - a;
}

// 排序函数(使用回调)
void sort(int arr[], int size, CompareFunction compare)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (compare(arr[j], arr[j + 1]) > 0)
{
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

// 打印数组
void print_array(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

int main(void)
{
int arr[] = {5, 2, 8, 1, 9};
int size = sizeof(arr) / sizeof(arr[0]);

printf("原始数组:");
print_array(arr, size);

// 升序排序
sort(arr, size, ascending);
printf("升序排序:");
print_array(arr, size);

// 降序排序
sort(arr, size, descending);
printf("降序排序:");
print_array(arr, size);

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
68
69
70
71
#include <stdio.h>

// 操作函数
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

int multiply(int a, int b)
{
return a * b;
}

int divide(int a, int b)
{
if (b == 0)
{
printf("错误:除数不能为零\n");
return 0;
}
return a / b;
}

// 函数表
int (*operations[])(int, int) = {
add,
subtract,
multiply,
divide
};

// 操作名称
const char* operation_names[] = {
"加法",
"减法",
"乘法",
"除法"
};

int main(void)
{
int choice, a, b, result;

printf("简单计算器\n");
printf("0. 加法\n");
printf("1. 减法\n");
printf("2. 乘法\n");
printf("3. 除法\n");
printf("请选择操作:");
scanf("%d", &choice);

if (choice < 0 || choice >= 4)
{
printf("无效选择\n");
return 1;
}

printf("请输入两个数字:");
scanf("%d %d", &a, &b);

// 使用函数表调用函数
result = operations[choice](a, b);
printf("%d %s %d = %d\n", a, operation_names[choice], b, result);

return 0;
}

函数的作用域和生命周期

函数的作用域

函数的作用域是指函数可以被访问的范围:

  1. 外部函数 - 作用域是整个程序,可以被其他文件中的函数调用
  2. 静态函数 - 作用域是定义它的文件,只能被该文件中的函数调用

函数的生命周期

函数的生命周期是指函数存在的时间:

  1. 外部函数 - 生命周期是整个程序的运行时间
  2. 静态函数 - 生命周期也是整个程序的运行时间

函数内变量的作用域和生命周期

  1. 局部变量 - 作用域是函数内部,生命周期是函数的执行时间
  2. 静态局部变量 - 作用域是函数内部,生命周期是整个程序的运行时间
  3. 全局变量 - 作用域是整个文件,生命周期是整个程序的运行时间
  4. 静态全局变量 - 作用域是定义它的文件,生命周期是整个程序的运行时间

变量存储类别示例

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
#include <stdio.h>

// 全局变量(外部链接)
int global_var = 100;

// 静态全局变量(内部链接)
static int static_global_var = 200;

void func(void)
{
// 局部变量(自动存储)
int local_var = 10;

// 静态局部变量(静态存储)
static int static_local_var = 20;

printf("local_var = %d\n", local_var);
printf("static_local_var = %d\n", static_local_var);

local_var++;
static_local_var++;
}

int main(void)
{
printf("第一次调用 func():\n");
func();

printf("第二次调用 func():\n");
func();

printf("global_var = %d\n", global_var);
printf("static_global_var = %d\n", static_global_var);

return 0;
}

函数设计的最佳实践

命名约定

  1. 函数名 - 使用动词或动词短语,如 calculate_averageprint_message
  2. 参数名 - 使用有意义的名称,如 user_namearray_size
  3. 返回值 - 函数的返回值应该有明确的含义,如 0 表示成功,非零表示错误

代码风格

  1. 缩进 - 使用一致的缩进风格,如 4 个空格
  2. 空行 - 在函数定义之间和逻辑块之间使用空行
  3. 注释 - 为复杂的函数添加注释,说明函数的功能、参数和返回值
  4. 大括号 - 使用一致的大括号风格,如 K&R 风格或 Allman 风格

错误处理

  1. 参数验证 - 在函数开始时验证参数的有效性
  2. 错误返回值 - 使用适当的返回值表示错误情况
  3. 错误消息 - 在发生错误时提供清晰的错误消息
  4. 资源清理 - 确保在错误情况下正确清理资源

性能考虑

  1. 避免不必要的参数传递 - 对于大型数据结构,考虑使用指针传递
  2. 减少函数调用开销 - 对于频繁调用的小函数,考虑使用内联
  3. 避免递归过深 - 对于可能深度过大的递归,考虑使用迭代
  4. 缓存计算结果 - 对于昂贵的计算,考虑缓存结果

可维护性

  1. 单一职责 - 每个函数只负责一个特定的任务
  2. 函数长度 - 函数长度不宜过长,一般不超过 50-100 行
  3. 代码重用 - 提取重复的代码为函数
  4. 模块化 - 将相关的函数组织在一起,形成模块

示例:使用函数

示例 1:计算器

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
68
69
70
71
#include <stdio.h>

// 函数声明
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);

int main(void)
{
int choice, num1, num2, result;

printf("简单计算器\n");
printf("1. 加法\n");
printf("2. 减法\n");
printf("3. 乘法\n");
printf("4. 除法\n");
printf("请选择操作:");
scanf("%d", &choice);

printf("请输入两个数字:");
scanf("%d %d", &num1, &num2);

switch (choice)
{
case 1:
result = add(num1, num2);
break;
case 2:
result = subtract(num1, num2);
break;
case 3:
result = multiply(num1, num2);
break;
case 4:
result = divide(num1, num2);
break;
default:
printf("无效选择\n");
return 1;
}

printf("结果:%d\n", result);
return 0;
}

// 函数定义
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

int multiply(int a, int b)
{
return a * b;
}

int divide(int a, int b)
{
if (b == 0)
{
printf("错误:除数不能为零\n");
return 0;
}
return a / b;
}

示例 2:使用函数指针实现回调

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
#include <stdio.h>

// 函数类型定义
typedef int (*Operation)(int, int);

// 操作函数
int add(int a, int b)
{
return a + b;
}

int multiply(int a, int b)
{
return a * b;
}

// 回调函数
void process_numbers(int a, int b, Operation op, const char* op_name)
{
int result = op(a, b);
printf("%d %s %d = %d\n", a, op_name, b, result);
}

int main(void)
{
int x = 5, y = 3;

process_numbers(x, y, add, "+" );
process_numbers(x, y, multiply, "*");

return 0;
}

示例 3:递归实现快速排序

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
68
69
#include <stdio.h>

// 交换两个元素
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}

// 分区函数
int partition(int arr[], int low, int high)
{
int pivot = arr[high]; // 选择最后一个元素作为 pivot
int i = (low - 1); // 小于 pivot 的元素的索引

for (int j = low; j <= high - 1; j++)
{
// 如果当前元素小于或等于 pivot
if (arr[j] <= pivot)
{
i++;
swap(&arr[i], &arr[j]);
}
}

swap(&arr[i + 1], &arr[high]);
return (i + 1);
}

// 快速排序函数
void quick_sort(int arr[], int low, int high)
{
if (low < high)
{
// 获取分区点
int pi = partition(arr, low, high);

// 递归排序分区点左侧和右侧的子数组
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}

// 打印数组
void print_array(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

int main(void)
{
int arr[] = {10, 7, 8, 9, 1, 5};
int size = sizeof(arr) / sizeof(arr[0]);

printf("原始数组:");
print_array(arr, size);

quick_sort(arr, 0, size - 1);

printf("排序后数组:");
print_array(arr, size);

return 0;
}

小结

本章详细介绍了 C 语言的函数,包括:

  • 函数的基本概念:函数的定义、优点和设计原则
  • 函数的声明和定义:函数声明、函数定义的语法和组成部分
  • 函数的调用:函数调用的语法和执行过程
  • 函数的参数:形式参数和实际参数、值传递、指针参数、数组参数、结构体参数和可变参数
  • 函数的返回值:返回值的类型、void 函数和返回值的注意事项
  • 函数原型:函数原型的作用、语法和位置
  • 递归函数:递归的基本结构、示例、优缺点和优化
  • 内联函数:内联函数的语法、优缺点和使用场景
  • 函数的存储类别:外部函数和静态函数
  • 函数指针:函数指针的声明、初始化、使用和应用
  • 函数的作用域和生命周期:函数和函数内变量的作用域和生命周期
  • 函数设计的最佳实践:命名约定、代码风格、错误处理、性能考虑和可维护性

函数是 C 语言中组织代码的基本单位,掌握函数的使用是编写高质量 C 程序的关键。通过合理使用函数,你可以将复杂的程序分解为更小、更易于管理的部分,提高代码的可读性、可维护性和可重用性。

在后续章节中,我们将学习数组和指针等概念,它们与函数密切相关,是 C 语言的核心特性。