云计算百科
云计算领域专业知识百科平台

C语言14章零基础干货 第六章

第6章 函数

6.1 函数概述

函数是C语言中模块化编程的基本单位,它是一个完成特定功能的代码块,可以被重复调用。函数可以提高代码的复用性、可读性和可维护性。

函数的特点:

  • 函数是一个独立的代码块,完成特定的功能
  • 函数可以被多次调用,提高代码的复用性
  • 函数可以接收输入参数,并返回输出结果
  • 函数可以隐藏内部实现细节,只暴露接口

C语言中的函数分类:

  • 库函数:由C语言标准库提供的函数,如printf、scanf等
  • 用户自定义函数:由用户自己编写的函数,用于实现特定的功能

6.2 函数的定义和声明

6.2.1 函数的定义

函数定义是指编写函数的具体实现代码。

函数定义的语法:

返回值类型 函数名(参数列表) {
// 函数体:实现函数功能的代码
return 返回值; // 可选,取决于返回值类型
}

其中:

  • 返回值类型:函数返回值的数据类型,如果函数不返回值,使用void
  • 函数名:函数的名称,遵循变量命名规则
  • 参数列表:函数接收的输入参数,每个参数由数据类型和参数名组成,多个参数之间用逗号分隔
  • 函数体:实现函数功能的代码块
  • return语句:用于返回函数的结果,结束函数的执行

示例:

// 定义一个计算两个整数和的函数
int add(int a, int b) {
int sum = a + b;
return sum; // 返回两个数的和
}

// 定义一个打印问候语的函数,无返回值
void greet() {
printf("Hello, World!\\n");
// 没有return语句,或者使用return;
}

6.2.2 函数的声明

函数声明是指告诉编译器函数的存在,包括函数名、返回值类型和参数列表,而不包含函数体。函数声明的目的是为了让编译器在编译时能够识别函数调用。

函数声明的语法:

返回值类型 函数名(参数列表);

示例:

// 声明一个计算两个整数和的函数
int add(int a, int b);

// 声明一个打印问候语的函数
void greet();

注意事项

  • 函数声明可以省略参数名,只保留参数类型,如int add(int, int);
  • 函数声明必须在函数调用之前
  • 如果函数定义在函数调用之前,可以省略函数声明

6.2.3 函数定义和声明的关系

函数声明是函数定义的“原型”,它告诉编译器函数的接口信息,而函数定义则提供了函数的具体实现。

示例:

#include <stdio.h>

// 函数声明
int add(int a, int b);
void greet();

int main() {
greet(); // 调用greet函数

int num1 = 5, num2 = 3;
int result = add(num1, num2); // 调用add函数
printf("%d + %d = %d\\n", num1, num2, result);

return 0;
}

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

// 函数定义
void greet() {
printf("Hello, World!\\n");
}

运行结果:

Hello, World!
5 + 3 = 8

章节练习:函数的定义和调用

编写一个程序,定义一个函数来计算两个整数的乘积,然后在main函数中调用该函数,输入两个整数,输出它们的乘积。

疑难解析:函数声明和函数定义的区别是什么?

函数声明和函数定义是C语言中两个不同的概念:

  • 函数声明:告诉编译器函数的存在,包括函数名、返回值类型和参数列表,但不包含函数体。函数声明的目的是为了让编译器在编译时能够识别函数调用。
  • 函数定义:包含函数的完整实现,包括函数体。函数定义是函数的具体实现代码。

一个函数可以有多个声明,但只能有一个定义(单一定义规则)。

易错部分:函数定义顺序错误

在C语言中,如果函数定义在函数调用之后,必须先声明该函数,否则编译器会报错。例如:

#include <stdio.h>

int main() {
int result = add(5, 3); // 错误:add函数尚未声明
printf("%d\\n", result);
return 0;
}

// 函数定义在调用之后
int add(int a, int b) {
return a + b;
}

正确的做法是在main函数之前声明add函数,或者将add函数的定义放在main函数之前。

章节趣事:函数的历史

函数的概念最早可以追溯到数学中的函数,它表示一种输入输出关系。在编程语言中,函数的概念是由早期的编程语言如FORTRAN和ALGOL 60引入的。C语言继承和发展了函数的概念,使其成为模块化编程的基本单位。现代编程语言如C++、Java、Python等都保留了函数的概念,并在此基础上进行了扩展,如面向对象编程中的方法。

6.3 函数参数和返回值

6.3.1 函数参数

函数参数是函数接收的输入值,用于传递数据给函数。函数参数分为形参和实参。

1. 形参

形参(形式参数)是函数定义时括号中的参数,它是函数内部的局部变量,用于接收实参传递的值。

2. 实参

实参(实际参数)是函数调用时传递给函数的值,它可以是常量、变量或表达式。

示例:

int add(int a, int b) { // a和b是形参
return a + b;
}

int main() {
int x = 5, y = 3;
int result = add(x, y); // x和y是实参
return 0;
}

6.3.2 参数传递方式

C语言中,函数参数的传递方式是值传递,即实参的值被复制到形参中,函数内部对形参的修改不会影响实参的值。

示例:

#include <stdio.h>

void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("swap函数中: a = %d, b = %d\\n", a, b);
}

int main() {
int x = 5, y = 3;
printf("交换前: x = %d, y = %d\\n", x, y);
swap(x, y); // 值传递
printf("交换后: x = %d, y = %d\\n", x, y); // x和y的值没有改变

return 0;
}

运行结果:

交换前: x = 5, y = 3
swap函数中: a = 3, b = 5
交换后: x = 5, y = 3

注意事项

值传递的特点是:函数内部对形参的修改不会影响实参的值。如果需要在函数内部修改实参的值,需要使用指针传递(后续章节会详细讲解)。

6.3.3 返回值

返回值是函数执行后返回给调用者的结果。

使用return语句返回值:

  • 如果函数有返回值,return语句必须返回一个与返回值类型匹配的值
  • 如果函数的返回值类型是void,return语句可以省略,或者使用return;结束函数
  • return语句会立即结束函数的执行,返回到调用位置

示例:

#include <stdio.h>

// 有返回值的函数
int max(int a, int b) {
if (a > b) {
return a; // 返回a的值,结束函数
} else {
return b; // 返回b的值,结束函数
}
}

// 无返回值的函数
void print_number(int num) {
if (num < 0) {
printf("负数\\n");
return; // 提前结束函数
}
printf("正数或零: %d\\n", num);
}

int main() {
int x = 5, y = 3;
int m = max(x, y);
printf("最大值: %d\\n", m);

print_number(-5);
print_number(10);

return 0;
}

运行结果:

最大值: 5
负数
正数或零: 10

6.4 函数调用

函数调用是指执行函数体中的代码,获取函数的返回值(如果有)。

6.4.1 函数调用的语法

// 如果函数有返回值,可以将返回值赋给变量
返回值类型 变量名 = 函数名(实参列表);

// 或者直接调用,忽略返回值
函数名(实参列表);

示例:

#include <stdio.h>

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

void greet() {
printf("Hello!\\n");
}

int main() {
// 调用有返回值的函数,并将返回值赋给变量
int result = add(5, 3);
printf("5 + 3 = %d\\n", result);

// 直接调用有返回值的函数,忽略返回值
add(10, 20);

// 调用无返回值的函数
greet();

return 0;
}

运行结果:

5 + 3 = 8
Hello!

6.4.2 函数的嵌套调用

函数的嵌套调用是指在一个函数内部调用另一个函数。

示例:

#include <stdio.h>

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

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

int calculate(int x, int y) {
int sum = add(x, y); // 在calculate函数中调用add函数
int product = multiply(x, y); // 在calculate函数中调用multiply函数
return sum + product;
}

int main() {
int x = 5, y = 3;
int result = calculate(x, y); // 在main函数中调用calculate函数
printf("计算结果: %d\\n", result);

return 0;
}

运行结果:

计算结果: 23 // 5+3 + 5*3 = 8 + 15 = 23

6.4.3 函数的递归调用

递归调用是指函数调用自身的过程。递归函数通常用于解决可以分解为相似子问题的问题,如阶乘、斐波那契数列等。

递归函数的特点:

  • 有一个或多个基本情况(终止条件),不需要递归就能直接求解
  • 有一个或多个递归情况,将问题分解为更小的子问题,通过递归调用解决

示例1:计算n的阶乘(n!)

#include <stdio.h>

int factorial(int n) {
// 基本情况:n=0或n=1时,阶乘为1
if (n == 0 || n == 1) {
return 1;
}
// 递归情况:n! = n * (n-1)!
return n * factorial(n – 1);
}

int main() {
int n = 5;
int result = factorial(n);
printf("%d! = %d\\n", n, result);

return 0;
}

运行结果:

5! = 120

示例2:计算斐波那契数列的第n项

#include <stdio.h>

int fibonacci(int n) {
// 基本情况:第1项和第2项都是1
if (n == 1 || n == 2) {
return 1;
}
// 递归情况:第n项 = 第n-1项 + 第n-2项
return fibonacci(n – 1) + fibonacci(n – 2);
}

int main() {
int n = 10;
printf("斐波那契数列第%d项: %d\\n", n, fibonacci(n));

// 输出前10项
printf("前10项: ");
for (int i = 1; i <= 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\\n");

return 0;
}

运行结果:

斐波那契数列第10项: 55
前10项: 1 1 2 3 5 8 13 21 34 55

注意事项

  • 递归函数必须有明确的终止条件,否则会导致无限递归,栈溢出
  • 递归函数的执行效率通常比迭代(循环)低,因为每次递归调用都会产生额外的栈开销
  • 对于复杂问题,递归可以使代码更简洁、更易理解
章节练习:递归函数

编写一个递归函数,计算斐波那契数列的第n项,然后在main函数中调用该函数,输出前20项的斐波那契数列。

疑难解析:递归函数与迭代函数如何选择?

递归函数和迭代函数都可以用于解决循环问题,但它们各有优缺点:

  • 递归函数:
    • 优点:代码更简洁、更易理解,适合解决可以分解为相似子问题的问题
    • 缺点:执行效率较低,容易导致栈溢出,不适合处理大规模数据
  • 迭代函数:
    • 优点:执行效率较高,不会导致栈溢出,适合处理大规模数据
    • 缺点:代码可能更复杂,不如递归函数直观

选择哪种方式取决于具体的问题和需求。对于小规模数据和复杂问题,递归函数可能更合适;对于大规模数据和简单问题,迭代函数可能更合适。

易错部分:忘记终止条件

在编写递归函数时,最常见的错误之一是忘记添加终止条件,导致无限递归,最终栈溢出。例如:

// 错误:没有终止条件
int factorial(int n) {
return n * factorial(n – 1);
}

// 正确:有终止条件
int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n – 1);
}

为了避免这种情况,编写递归函数时,首先应该考虑终止条件,然后再考虑递归逻辑。

章节趣事:递归的历史

递归的概念最早可以追溯到数学中的递归定义,如阶乘、斐波那契数列等。在编程语言中,递归的概念是由早期的编程语言如LISP引入的,LISP是一种函数式编程语言,非常适合使用递归。C语言支持递归,但它是一种命令式编程语言,递归的执行效率相对较低。现代编程语言如Python、Java、C++等都支持递归,并对其进行了优化。

6.5 变量的作用域和存储类别

6.5.1 变量的作用域

变量的作用域是指变量在程序中可以被访问的范围。C语言中的变量作用域分为以下几种:

1. 局部变量

局部变量是在函数内部或代码块内部定义的变量,它的作用域仅限于定义它的函数或代码块内部。

特点:

  • 局部变量在函数调用时被创建,函数返回时被销毁
  • 局部变量在不同的函数中可以重名,它们是不同的变量
  • 局部变量如果没有初始化,其值是不确定的
2. 全局变量

全局变量是在函数外部定义的变量,它的作用域是整个程序。

特点:

  • 全局变量在程序开始时被创建,程序结束时被销毁
  • 全局变量在所有函数中都可以访问和修改
  • 全局变量如果没有初始化,会被自动初始化为0

示例:

#include <stdio.h>

int global_var = 10; // 全局变量

void func1() {
int local_var = 5; // 局部变量
printf("func1: global_var = %d, local_var = %d\\n", global_var, local_var);
}

void func2() {
int local_var = 8; // 与func1中的local_var重名,是不同的变量
printf("func2: global_var = %d, local_var = %d\\n", global_var, local_var);

// 修改全局变量
global_var = 20;
}

int main() {
printf("main: global_var = %d\\n", global_var);
func1();
func2();
printf("main: global_var = %d\\n", global_var); // 全局变量已被修改

return 0;
}

运行结果:

main: global_var = 10
func1: global_var = 10, local_var = 5
func2: global_var = 10, local_var = 8
main: global_var = 20

6.5.2 变量的存储类别

变量的存储类别决定了变量的存储位置、生命周期和可见性。C语言中的变量存储类别有以下几种:

1. auto存储类别

auto存储类别是局部变量的默认存储类别,它表示变量存储在栈内存中。

示例:

void func() {
auto int x = 5; // 等同于 int x = 5;
}

2. static存储类别

static存储类别可以用于局部变量和全局变量:

  • 对于局部变量:static局部变量存储在静态内存中,它的生命周期是整个程序,而作用域仍然是定义它的函数或代码块内部。static局部变量只初始化一次,每次函数调用时保留上次的值。
  • 对于全局变量:static全局变量的作用域仅限于定义它的源文件,其他源文件无法访问。

示例:

#include <stdio.h>

void func() {
int auto_var = 0; // auto局部变量,每次调用重新初始化
static int static_var = 0; // static局部变量,只初始化一次

auto_var++;
static_var++;

printf("auto_var = %d, static_var = %d\\n", auto_var, static_var);
}

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

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

printf("第三次调用func():");
func();

return 0;
}

运行结果:

第一次调用func():auto_var = 1, static_var = 1
第二次调用func():auto_var = 1, static_var = 2
第三次调用func():auto_var = 1, static_var = 3

3. extern存储类别

extern存储类别用于声明外部变量,即声明在其他源文件中定义的全局变量。

示例:

// file1.c
extern int global_var; // 声明在其他文件中定义的全局变量

void func() {
printf("global_var = %d\\n", global_var);
}

// file2.c
int global_var = 10; // 定义全局变量

4. register存储类别

register存储类别用于声明寄存器变量,它建议编译器将变量存储在CPU寄存器中,以提高访问速度。

示例:

void func() {
register int count = 0; // 建议将count存储在寄存器中
// …
}

注意事项

  • 由于寄存器数量有限,register关键字只是一个建议,编译器可能会忽略
  • 现代编译器的优化能力很强,通常会自动将频繁使用的变量存储在寄存器中

6.6 函数设计模式

6.6.1 函数设计原则

良好的函数设计有助于提高代码的可读性、可维护性和可复用性。以下是一些常用的函数设计原则:

  • 单一职责原则:一个函数应该只完成一个特定的功能,避免函数过于复杂
  • 函数名应该清晰表达其功能:函数名应该能够直观地反映函数的用途
  • 函数参数应该尽量少:过多的参数会增加函数调用的复杂度,建议不超过5个
  • 函数应该有明确的返回值:返回值应该能够清晰地表达函数执行的结果
  • 避免副作用:函数应该尽量避免修改全局变量或外部状态
  • 函数应该有适当的注释:注释应该解释函数的功能、参数、返回值和使用方法

6.6.2 常见函数设计模式

1. 包装器模式

包装器模式(Wrapper Pattern)是指创建一个函数来包装另一个函数,为其添加额外的功能,如日志记录、错误处理、性能监控等。

示例:日志包装器

#include <stdio.h>
#include <time.h>

// 原始函数
int add(int a, int b) {
return a + b;
}

// 包装器函数,添加日志功能
int add_with_log(int a, int b) {
// 记录开始时间
time_t start_time = time(NULL);

// 调用原始函数
int result = add(a, b);

// 记录结束时间和结果
time_t end_time = time(NULL);
printf("[LOG] add(%d, %d) = %d, 耗时:%ld秒\\n", a, b, result, end_time – start_time);

return result;
}

int main() {
int result = add_with_log(5, 3);
printf("最终结果:%d\\n", result);

return 0;
}

运行结果:

[LOG] add(5, 3) = 8, 耗时:0秒
最终结果:8

2. 策略模式

策略模式(Strategy Pattern)是指定义一系列算法,将它们封装起来,并使它们可以相互替换。在C语言中,策略模式通常通过函数指针实现。

示例:计算器策略模式

#include <stdio.h>

// 策略函数类型定义
typedef int (*CalculationStrategy)(int, int);

// 具体策略:加法
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("错误:除数不能为0\\n");
return 0;
}
return a / b;
}

// 上下文函数,使用策略
int calculate(int a, int b, CalculationStrategy strategy) {
return strategy(a, b);
}

int main() {
int a = 10, b = 5;

printf("%d + %d = %d\\n", a, b, calculate(a, b, add));
printf("%d – %d = %d\\n", a, b, calculate(a, b, subtract));
printf("%d * %d = %d\\n", a, b, calculate(a, b, multiply));
printf("%d / %d = %d\\n", a, b, calculate(a, b, divide));

return 0;
}

运行结果:

10 + 5 = 15
10 – 5 = 5
10 * 5 = 50
10 / 5 = 2

3. 工厂模式

工厂模式(Factory Pattern)是指创建一个函数来根据不同的条件创建并返回不同的函数指针,实现动态函数选择。

示例:排序算法工厂

#include <stdio.h>

// 排序函数类型定义
typedef void (*SortAlgorithm)(int[], int);

// 冒泡排序
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n – 1; i++) {
for (int j = 0; j < n – 1 – i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

// 选择排序
void selection_sort(int arr[], int n) {
for (int i = 0; i < n – 1; i++) {
int min_idx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}

// 插入排序
void insertion_sort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j–;
}
arr[j + 1] = key;
}
}

// 排序算法工厂函数
SortAlgorithm get_sort_algorithm(const char* name) {
if (strcmp(name, "bubble") == 0) {
return bubble_sort;
} else if (strcmp(name, "selection") == 0) {
return selection_sort;
} else if (strcmp(name, "insertion") == 0) {
return insertion_sort;
} else {
printf("未知的排序算法:%s\\n", name);
return NULL;
}
}

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

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

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

// 使用冒泡排序
SortAlgorithm sort_func = get_sort_algorithm("bubble");
if (sort_func != NULL) {
sort_func(arr, n);
printf("冒泡排序后:");
print_array(arr, n);
}

// 重置数组
int arr2[] = {5, 2, 8, 1, 9, 3};

// 使用插入排序
sort_func = get_sort_algorithm("insertion");
if (sort_func != NULL) {
sort_func(arr2, n);
printf("插入排序后:");
print_array(arr2, n);
}

return 0;
}

运行结果:

原始数组:5 2 8 1 9 3
冒泡排序后:1 2 3 5 8 9
插入排序后:1 2 3 5 8 9

6.7 递归深度解析

6.7.1 递归的基本概念

递归(Recursion)是指函数调用自身的过程。递归函数必须具备两个条件:

  • 基本情况(Base Case):递归终止的条件,不需要进一步递归就能直接求解
  • 递归情况(Recursive Case):将问题分解为更小的子问题,通过递归调用解决

6.7.2 递归的工作原理

递归函数的执行过程可以分为两个阶段:

  • 递推阶段:函数不断调用自身,将问题分解为更小的子问题,直到达到基本情况
  • 回归阶段:从基本情况开始,逐步返回结果,直到得到原问题的解
递归调用栈

每次递归调用都会在内存中创建一个新的栈帧,用于存储函数的局部变量、参数和返回地址。当递归深度过大时,可能会导致栈溢出(Stack Overflow)。

6.7.3 递归算法的优化

1. 尾递归优化

尾递归(Tail Recursion)是指递归调用是函数的最后一个操作,这样编译器可以优化递归调用,避免创建新的栈帧。

示例:尾递归阶乘

#include <stdio.h>

// 普通递归阶乘
int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n – 1); // 递归调用不是最后一个操作
}

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

// 包装函数,提供更友好的接口
int factorial_wrapper(int n) {
return factorial_tail(n, 1);
}

int main() {
int n = 5;
printf("普通递归:%d! = %d\\n", n, factorial(n));
printf("尾递归:%d! = %d\\n", n, factorial_wrapper(n));

return 0;
}

运行结果:

普通递归:5! = 120
尾递归:5! = 120

2. 记忆化优化

记忆化(Memoization)是指将已经计算过的结果存储起来,避免重复计算,提高递归效率。

示例:记忆化斐波那契数列

#include <stdio.h>
#include <stdlib.h>

// 记忆化数组,用于存储已经计算过的结果
long long* memo;

// 记忆化斐波那契数列
long long fibonacci_memo(int n) {
// 基本情况
if (n == 0) {
return 0;
}
if (n == 1 || n == 2) {
return 1;
}

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

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

int main() {
int n = 50;

// 初始化记忆化数组
memo = (long long*)malloc((n + 1) * sizeof(long long));
for (int i = 0; i <= n; i++) {
memo[i] = -1;
}

printf("斐波那契数列第%d项: %lld\\n", n, fibonacci_memo(n));

// 释放内存
free(memo);

return 0;
}

运行结果:

斐波那契数列第50项: 12586269025

6.7.4 递归与迭代的转换

任何递归算法都可以转换为迭代算法,迭代算法通常具有更高的效率,因为它避免了递归调用的栈开销。

示例:斐波那契数列的迭代实现

#include <stdio.h>

// 迭代实现斐波那契数列
long long fibonacci_iterative(int n) {
if (n == 0) {
return 0;
}
if (n == 1 || n == 2) {
return 1;
}

long long a = 1, b = 1, c;
for (int i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}

int main() {
int n = 50;
printf("斐波那契数列第%d项: %lld\\n", n, fibonacci_iterative(n));

return 0;
}

运行结果:

斐波那契数列第50项: 12586269025

6.7.5 递归的应用场景

递归算法适用于以下场景:

  • 数据结构是递归的,如链表、树、图等
  • 问题可以自然地分解为相似的子问题,如分治算法
  • 需要遍历或搜索非线性数据结构,如树的遍历、图的搜索
  • 需要生成排列、组合或子集等

6.8 函数指针高级应用

6.8.1 函数指针数组

函数指针数组是指数组的元素是函数指针,可以用于存储一组类型相同的函数指针,实现函数的动态选择和调用。

示例:计算器函数指针数组

#include <stdio.h>

// 定义函数指针类型
typedef double (*CalcFunc)(double, double);

// 算术运算函数
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) { return b != 0 ? a / b : 0; }

int main() {
// 函数指针数组
CalcFunc calc_funcs[] = {add, subtract, multiply, divide};
char operators[] = {'+', '-', '*', '/'};
int num_ops = sizeof(calc_funcs) / sizeof(calc_funcs[0]);

double a = 10.5, b = 2.5;

// 使用函数指针数组调用不同的函数
for (int i = 0; i < num_ops; i++) {
double result = calc_funcs[i](a, b);
printf("%.2f %c %.2f = %.2f\\n", a, operators[i], b, result);
}

return 0;
}

运行结果:

10.50 + 2.50 = 13.00
10.50 – 2.50 = 8.00
10.50 * 2.50 = 26.25
10.50 / 2.50 = 4.20

6.8.2 指向函数指针的指针

指向函数指针的指针(Pointer to Function Pointer)是指一个指针,它指向一个函数指针变量。这种结构可以用于实现更复杂的函数动态调用。

示例:指向函数指针的指针

#include <stdio.h>

// 函数定义
void func1() { printf("调用了func1\\n"); }
void func2() { printf("调用了func2\\n"); }
void func3() { printf("调用了func3\\n"); }

int main() {
// 函数指针变量
void (*func_ptr)();

// 指向函数指针的指针
void (**func_ptr_ptr)() = &func_ptr;

// 通过指向函数指针的指针调用函数
*func_ptr_ptr = func1;
(**func_ptr_ptr)(); // 调用func1

*func_ptr_ptr = func2;
(**func_ptr_ptr)(); // 调用func2

*func_ptr_ptr = func3;
(**func_ptr_ptr)(); // 调用func3

return 0;
}

运行结果:

调用了func1
调用了func2
调用了func3

6.8.3 回调函数的高级应用

回调函数可以用于实现事件驱动编程、异步编程和插件系统等高级应用。

示例:事件处理器

#include <stdio.h>
#include <string.h>

// 事件类型定义
typedef enum {
EVENT_CLICK,
EVENT_KEYDOWN,
EVENT_MOUSEMOVE,
EVENT_QUIT
} EventType;

// 事件结构体
typedef struct {
EventType type;
int x, y; // 鼠标坐标
char key; // 按键
} Event;

// 回调函数类型定义
typedef void (*EventHandler)(const Event* event);

// 事件处理器结构体
typedef struct {
EventHandler handlers[4]; // 每个事件类型对应一个处理器
} EventDispatcher;

// 初始化事件分发器
void init_event_dispatcher(EventDispatcher* dispatcher) {
memset(dispatcher->handlers, 0, sizeof(dispatcher->handlers));
}

// 注册事件处理器
void register_event_handler(EventDispatcher* dispatcher, EventType type, EventHandler handler) {
dispatcher->handlers[type] = handler;
}

// 触发事件
void dispatch_event(EventDispatcher* dispatcher, const Event* event) {
EventHandler handler = dispatcher->handlers[event->type];
if (handler != NULL) {
handler(event);
}
}

// 具体事件处理函数
void on_click(const Event* event) {
printf("点击事件:x=%d, y=%d\\n", event->x, event->y);
}

void on_keydown(const Event* event) {
printf("按键事件:key=%c\\n", event->key);
}

void on_quit(const Event* event) {
printf("退出事件\\n");
}

int main() {
EventDispatcher dispatcher;
init_event_dispatcher(&dispatcher);

// 注册事件处理器
register_event_handler(&dispatcher, EVENT_CLICK, on_click);
register_event_handler(&dispatcher, EVENT_KEYDOWN, on_keydown);
register_event_handler(&dispatcher, EVENT_QUIT, on_quit);

// 模拟事件
Event click_event = {EVENT_CLICK, 100, 200, 0};
Event keydown_event = {EVENT_KEYDOWN, 0, 0, 'A'};
Event quit_event = {EVENT_QUIT, 0, 0, 0};

// 触发事件
dispatch_event(&dispatcher, &click_event);
dispatch_event(&dispatcher, &keydown_event);
dispatch_event(&dispatcher, &quit_event);

return 0;
}

运行结果:

点击事件:x=100, y=200
按键事件:key=A
退出事件

6.8.4 函数指针与typedef

使用typedef可以为函数指针类型定义一个别名,使代码更加简洁和易读。

示例:typedef函数指针

#include <stdio.h>

// 使用typedef定义函数指针类型
typedef int (*MathFunc)(int, int);
typedef void (*PrintFunc)(const char*);

// 算术运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a – b; }

// 打印函数
void print_info(const char* msg) { printf("[INFO] %s\\n", msg); }
void print_error(const char* msg) { printf("[ERROR] %s\\n", msg); }

int main() {
// 使用typedef定义的函数指针类型
MathFunc math_func;
PrintFunc print_func;

math_func = add;
printf("5 + 3 = %d\\n", math_func(5, 3));

math_func = subtract;
printf("5 – 3 = %d\\n", math_func(5, 3));

print_func = print_info;
print_func("这是一条信息");

print_func = print_error;
print_func("这是一条错误");

return 0;
}

运行结果:

5 + 3 = 8
5 – 3 = 2
[INFO] 这是一条信息
[ERROR] 这是一条错误

6.9 内联函数和可变参数函数

6.9.1 内联函数

内联函数(Inline Function)是指在编译时将函数调用替换为函数体的函数,用于减少函数调用的开销,提高程序的执行效率。

内联函数的定义语法:

inline 返回值类型 函数名(参数列表) {
// 函数体
return 返回值;
}

内联函数的注意事项
  • 内联函数适用于函数体短小、调用频繁的函数
  • 内联函数的定义通常放在头文件中
  • inline关键字只是一个建议,编译器可以选择忽略
  • 递归函数、包含循环或复杂控制流的函数不适合作为内联函数
示例:内联函数

#include <stdio.h>

// 内联函数,计算两个数的最大值
inline int max(int a, int b) {
return a > b ? a : b;
}

int main() {
int x = 10, y = 20;
int z = max(x, y); // 编译时会替换为:z = x > y ? x : y;
printf("最大值:%d\\n", z);

return 0;
}

运行结果:

最大值:20

6.9.2 可变参数函数

可变参数函数(Variadic Function)是指参数数量可变的函数,如printf和scanf函数。

可变参数函数的定义需要使用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++) {
int num = va_arg(args, int);
total += num;
}

// 结束可变参数的获取
va_end(args);

return total;
}

int main() {
printf("sum(2, 10, 20) = %d\\n", sum(2, 10, 20));
printf("sum(3, 10, 20, 30) = %d\\n", sum(3, 10, 20, 30));
printf("sum(5, 1, 2, 3, 4, 5) = %d\\n", sum(5, 1, 2, 3, 4, 5));

return 0;
}

运行结果:

sum(2, 10, 20) = 30
sum(3, 10, 20, 30) = 60
sum(5, 1, 2, 3, 4, 5) = 15

可变参数函数的注意事项
  • 可变参数函数必须有至少一个固定参数
  • 可变参数的类型必须通过固定参数或其他方式确定
  • 使用va_arg时必须指定正确的参数类型
  • 可变参数函数不类型安全,使用时要格外小心

练习题

  • 编写一个函数,计算两个数的最大公约数(GCD)。
  • 编写一个函数,计算两个数的最小公倍数(LCM)。
  • 编写一个递归函数,计算斐波那契数列的第n项,并与迭代实现比较效率。
  • 编写一个函数,将一个数组中的元素逆序排列。
  • 编写一个函数,判断一个数是否为素数。
  • 编写一个回调函数示例,实现不同的排序方式。
  • 学习建议

    • 理解函数的基本概念和作用
    • 掌握函数的定义、声明和调用方法
    • 理解参数传递方式和返回值的使用
    • 学习递归函数的设计和实现
    • 理解局部变量和全局变量的区别
    • 掌握变量的存储类别
    • 学习函数指针和回调函数的使用
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C语言14章零基础干货 第六章
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!