青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】七、函数


阅读数: 0次    2025-10-16
字数:9.3k字 | 预计阅读时长:37分钟

1. function函数的介绍与作用

function的中文翻译一般为函数,但代码中的函数与数学中的函数不同,数学中的函数强调映射,而代码中的函数其实是一个功能块。因此function的比较准确的翻译应该是类似于Java中的“方法”。

函数的功能在于,当许多处代码中都会用到相同的功能时,可以将功能块提取出来,打包成一个函数,这样只需要在合适的时候调用函数即可。

因此可以总结一下,函数的作用就是模块化地将一些功能抽离打包出来,在其他地方复用。

2. 函数声明与定义的规则

为提高程序的可读性,程序中的函数一般先声明再定义。

即main函数前面只做函数的声明,告诉程序这个函数是存在的。

所有函数的实现都放在main函数的后面实现。

声明和定义的函数一一对应。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

// 先声明
void greet();

// C语言案例
int main(void) {
// 调用
greet();

return 0;
}

// 后定义
void greet() {
printf("Hello!\n");
}

3. 函数的参数

函数声明和定义时,可以在括号中添加函数期望收到的参数,也是希望传递给函数的参数。

括号里的参数定义和常规的参数声明是一样的,都应该是变量类型+变量名。

同一个函数,声明和定义应该是一一对应的,声明的函数是什么样,定义时的函数也应该是什么样。

例如一个将输入的年龄打印出来的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 声明
void printAge(uint32_t age);

// C语言案例
int main(void) {
uint32_t age = 18;
// 调用
printAge(age);

// 可以多次调用
printAge(20);

return 0;
}

// 定义
void printAge(uint32_t age) {
printf("age = %" PRIu32 "\n", age);
}

很明显,一个函数就是一个功能,在主函数中可以多次调用,传入的参数可以任意改变,只要符合声明和定义时的参数类型即可。

函数中传入的参数可以有很多个,参数的类型也没有限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

// 形式参数
void printInfo(uint32_t age, bool gender);

// C语言案例
int main(void) {
// 实际参数
uint32_t age = 18;
bool gender = true;

printInfo(age, gender);
printInfo(20, false);

return 0;
}

// 形式参数
void printInfo(uint32_t age, bool gender) {
printf("age = %" PRIu32 ", gender = %s\n", age, gender ? "男" : "女");
}

函数声明和定义时的参数是形式参数,函数在被调用时传入的参数是实际参数。

形式参数只能在函数定义的内部使用。

函数中还有一个比较重要的地方是函数的返回值。可以看到,前面写的函数中,函数名前面的返回值类型都是void,代表返回空类型,也就是没有参数被返回。

这里的返回值类型也可以是其他任何参数类型,对应函数return返回的参数类型,例如一个返回值为整型加和函数:

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

int add(uint32_t num1, uint32_t num2);

// C语言案例
int main(void) {
uint32_t num1 = 7;
uint32_t num2 = 8;
uint32_t sum = 0;

sum = add(num1, num2);
printf("%" PRIu32 " + %" PRIu32 " = %" PRIu32 "\n", num1, num2, sum);

// 可以直接在printf中调用函数
printf("%" PRIu32 " + %" PRIu32 " = %" PRIu32 "\n", 2, 3, add(2, 3));

return 0;
}

int add(uint32_t num1, uint32_t num2) {
// 返回两个数的和
return num1 + num2;
}

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

#define PI 3.14

double square_area(double side);
double rectangle_area(double length, double width);
double circle_area(double radius);

// C语言案例
int main(void) {
double side = 5.0; // 正方形边长
double length = 9.0, width = 6.0; // 长方形长和宽
double radius = 3.5; // 圆形半径

printf("Square area: %.2f\n", square_area(side));
printf("Rectangle area: %.2f\n", rectangle_area(length, width));
printf("Circle area: %.2f\n", circle_area(radius));

return 0;
}

double square_area(double side) {
return side * side;
}

double rectangle_area(double length, double width) {
return length * width;
}

double circle_area(double radius) {
return PI * radius * radius;
}

5. 函数编写要领

编写函数的一大关键就是要确定形式参数和返回值,它们应该根据此函数的任务来确定。

形式参数就是这个函数完成这个功能需要的输入值,返回值就是函数完成功能后需要给出的输出值。

以最常见的main函数为例,int main(void)代表main函数不需要输入值,返回值为int类型,因此一般main函数的最后都会写一个return 0,以满足main函数的返回值要求。

具体来说:

  1. 明确函数目的,明确它的功能将帮助你决定哪些输入是必须的(形式参数,模板),以及如何报告它的工作结果。
  2. 识别所需要的输入
  3. 确定函数的输出
  4. 使用适当的数据类型
  5. 函数命名清晰,并且先声明后定义

例如一个判断输入的年份是否为闰年的程序:

  1. 函数需要拿到一个输入年份,然后判断这个年份是否是闰年
  2. 函数功能是判断输入年份是否为闰年
  3. 函数判断结束后,需要返回一个是否为闰年的bool类型的值
  4. 函数名应该标准,命名为is_leap_year

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdbool.h>

bool is_leap_year(int year);

// C语言案例
int main(void) {
int year = 2025;

printf("%d 年 %s\n", year, is_leap_year(year) ? "是闰年" : "不是闰年");

return 0;
}

bool is_leap_year(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

编写函数时,不应该将很多功能都扔给一个函数进行,而应尽可能分模块化,鼓励一个函数只做一件事情,这样函数的复用性会更高。

6. 初步认识全局变量、局部变量

每个变量都有属于它的作用域,就是这些变量在什么范围内有效。

我们按照变量的作用域范围可以把变量划分为局部变量和全局变量。

局部变量出现在三种地方:

  • 在函数的开头定义的变量:在一个函数内部定义的变量只在本函数范围内有效,也就是只有本函数内才能引用它们,在此函数外不能使用这些变量;
  • 在函数内的复合语句内定义的变量:在复合语句内定义的变量只能在本复合语句范围内有效,只有本复合语句内才能引用他们,在该复合语句外不能使用这些变量。例如在for循环中定义的局部变量i;
  • 形式参数:函数的形参,只在该函数内有效。

全局变量:

  • 一个源文件中可以包含若干个函数,在函数外部定义的变量就是全局变量或外部变量;
  • 全局变量为该源文件中所有函数所共有,它的作用范围是从变量定义的位置到源文件结束;
  • 任何一处对全局变量的修改会影响全局的这个变量。

例如:

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

// 全局变量
int global_year = 2019;

// 局部变量(形式参数)
bool is_leap_year(int year);

// C语言案例
int main(void) {
// 局部变量(函数内定义的变量)
int local_year = 2025;
printf("%d 年 %s\n", local_year, is_leap_year(local_year) ? "是闰年" : "不是闰年");

// 修改全局变量
global_year++;
printf("%d 年 %s\n", global_year, is_leap_year(global_year) ? "是闰年" : "不是闰年");

global_year++;

// 复合语句(代码块)内的局部变量
if (true) {
// 局部变量(复合语句内定义的变量)
bool flag = is_leap_year(global_year);
printf("%d 年 %s\n", global_year, flag ? "是闰年" : "不是闰年");
}
//flag = false; // 错误,flag变量的作用域仅限于上面的if语句块内

return 0;
}

// 局部变量(形式参数)
bool is_leap_year(int year) {
//local_year++; // 错误,local_year变量的作用域仅限于main函数内
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

7. 游戏案例:石头剪刀布与软件工程的“规则映射”设计技巧

要求:

  • 程序模拟一个玩家和电脑之间的剪刀石头布的游戏;
  • 玩家从键盘输入一种选择,程序随机选择一种作为电脑的选择;
  • 分别打印两者的选择;
  • 对选择进行比较,判断出胜利的一方;
  • 打印游戏最终的结果。

代码实现:

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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h> // 标准库函数
#include <time.h> // 时间函数

// 游戏选项
#define ROCK 1 // 石头
#define SCISSORS 2 // 剪刀
#define PAPER 3 // 布

// C语言案例
int main(void) {
// 用当前的时间初始化随机数种子
srand(time(NULL));

// 打印游戏说明
puts("欢迎来到剪刀石头布游戏!");
puts("请输入你的选择:1(石头)、2(剪刀)、3(布)");

// 获取玩家的选择
uint32_t player_choice;
scanf_s("%" SCNu32, &player_choice);
while (player_choice < ROCK || player_choice > PAPER) {
puts("输入无效,请重新输入:1(石头)、2(剪刀)、3(布)");
scanf_s("%" SCNu32, &player_choice);
}

// 生成电脑的选择
uint32_t computer_choice = rand() % (PAPER - ROCK + 1) + ROCK; // 生成1~3之间的随机数

// 打印玩家和电脑的选择
const char* choices[] = { 0, "石头", "剪刀", "布" }; // 索引0是占位符
printf("玩家选择了:%s\n", choices[player_choice]);
printf("电脑选择了:%s\n", choices[computer_choice]);

// 判断胜负
if (player_choice == computer_choice) {
puts("平局!");
}
else if ((player_choice == ROCK && computer_choice == SCISSORS) ||
(player_choice == SCISSORS && computer_choice == PAPER) ||
(player_choice == PAPER && computer_choice == ROCK)) {
puts("玩家获胜!");
}
else {
puts("电脑获胜!");
}

return 0;
}

很明显,这种实现方式是将所有逻辑都放在main函数中,导致main函数比较臃肿,不好维护。

我们可以把单一的功能封装成函数,在main函数中调用函数即可。

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h> // 标准库函数
#include <time.h> // 时间函数

// 游戏选项
#define ROCK 1 // 石头
#define SCISSORS 2 // 剪刀
#define PAPER 3 // 布

// =====函数声明=====
// 打印游戏说明
void print_instructions(void);
// 获取玩家的选择
uint32_t get_player_move(void);
// 生成电脑的选择
uint32_t get_computer_move(void);
// 打印选择结果
void print_move(uint32_t move);
// 判断胜负
void determine_winner(uint32_t player_move, uint32_t computer_move);

// C语言案例
int main(void) {
// 用当前的时间初始化随机数种子
srand(time(NULL));

// 打印游戏说明
print_instructions();

// 获取玩家的选择
uint32_t player_move = get_player_move();

// 生成电脑的选择
uint32_t computer_move = get_computer_move();

// 打印玩家和电脑的选择
puts("玩家选择:");
print_move(player_move);
puts("电脑选择:");
print_move(computer_move);

// 判断胜负
determine_winner(player_move, computer_move);

return 0;
}

// =====函数定义=====
// 打印游戏说明
void print_instructions(void) {
puts("欢迎来到剪刀石头布游戏!");
puts("请输入你的选择:1(石头)、2(剪刀)、3(布)");
}

// 获取玩家的选择
uint32_t get_player_move(void) {
uint32_t player_move;
while (1) {
puts("你的选择:");
// 判断输入是否有效
if (scanf_s("%" SCNu32, &player_move) != 1) {
// 输入无效,清除输入缓冲区
while (getchar() != '\n');
puts("输入无效,请输入1(石头)、2(剪刀)、3(布)");
continue;
}
if (player_move < ROCK || player_move > PAPER) {
puts("选择无效,请输入1(石头)、2(剪刀)、3(布)");
continue;
}
break;
}
return player_move;
}

// 生成电脑的选择
uint32_t get_computer_move(void) {
return (rand() % (PAPER - ROCK + 1)) + ROCK; // 生成1到3之间的随机数
}

// 打印选择结果
void print_move(uint32_t move) {
switch (move) {
case ROCK:puts("石头"); break;
case SCISSORS:puts("剪刀"); break;
case PAPER:puts("布"); break;
default:break;
}
}

// 判断胜负
void determine_winner(uint32_t player_move, uint32_t computer_move) {
if (player_move == computer_move) {
puts("平局!");
}
else if ((player_move == ROCK && computer_move == SCISSORS) ||
(player_move == SCISSORS && computer_move == PAPER) ||
(player_move == PAPER && computer_move == ROCK)) {
puts("玩家获胜!");
}
else {
puts("电脑获胜!");
}
}

将每一个小功能封装成函数之后,文件结构就非常清晰了,大大提高的代码的可读性,易于维护。

但是可以注意到,判断胜负的determine_winner函数中,else if中的条件比较多,逻辑较为混乱,我们在前面将if的时候就说过,应该尽量避免一个if语句中的条件过于冗长,有没有一种办法可以避免这种复杂的逻辑判断呢?

答案是有的,应该使用一个软件工程中非常巧妙的设计技巧:规则映射

在判断胜负函数中,可以定义一个玩家胜利的规则映射,本质上是一个数组。这个规则映射(数组)的键(下标)是玩家的移动,对应的值(元素)是玩家可以打败电脑的那种选择。

当电脑的选择与玩家选择的下标对应的规则映射中的值相同时,判定玩家胜利,否则电脑胜利。

同时,可以使用卫语句节省资源,当平局时直接return,结束函数。

判断胜负函数的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断胜负
void determine_winner(uint32_t player_move, uint32_t computer_move) {
if (player_move == computer_move) {
puts("平局!");
return;
}

uint32_t winning_moves[4] = { 0, SCISSORS, PAPER, ROCK }; // 索引0是占位符
if (winning_moves[player_move] == computer_move) {
puts("玩家获胜!");
} else {
puts("电脑获胜!");
}
}

这样设计大大减少了逻辑的复杂度,且计算机访问数组的速度非常快,这种设计降低了资源消耗,提升了程序性能。

8. 案例:软件工程设计技巧:表驱动法之再探成绩评分系统、再探闰年返回月份天数

第四章中有一个例子,可以使用if-else语句来根据成绩给出评分等级,90分以上是A,80分到90分是B,70分到80分是C,60分到70分是D,60分以下是F。

可以将这个功能封装成函数来实现:

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

char get_cradit_level(uint32_t grade);

// C语言案例
int main(void) {
uint32_t grade;
puts("请输入成绩:");
scanf_s("%" SCNu32, &grade);

printf("成绩等级为:%c\n", get_cradit_level(grade));

return 0;
}

char get_cradit_level(uint32_t grade) {
if (grade >= 90) {
return 'A';
} else if (grade >= 80) {
return 'B';
} else if (grade >= 70) {
return 'C';
} else if (grade >= 60) {
return 'D';
} else {
return 'F';
}
}

但这个等级判断函数中包含了很多个if-else语句,会使程序浪费更多的资源来完成判断操作。

上一节中提到,软件工程中有一种设计方法叫“规则映射“,程序访问数组的速度会比做判断的速度快很多。

这里的等级判断函数中也可以采用映射的方法:

1
2
3
4
5
6
char get_credit_level(uint32_t grade) {
// 非显式数组
char level[] = { 'F', 'F', 'F', 'F', 'F', 'F', 'D', 'C', 'B', 'A' };

return level[grade / 10];
}

这样设计的程序会比原来一堆判断条件的性能高不少。

这样的设计方法被称为表驱动法。

又例如:给出一个年月,程序返回这一年的这一个月有多少天。

比较低级的设计方法会用到switch-case语句,一共12个case,每一个返回一个天数,同时二月份还要做一个是否为闰年的判断,这样设计就会比较繁琐,且性能得不到保证。

这时候就又可以用到表驱动法来设计函数,判断闰年的功能可以单独放在一个函数中,在输出天数的函数中直接调用。

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 <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

uint32_t return_days(uint32_t year, uint32_t month);
bool is_leap_year(uint32_t year);

// C语言案例
int main(void) {
uint32_t year, month;
puts("请输入年份和月份(以空格分隔):");
scanf_s("%" SCNu32 " %" SCNu32, &year, &month);
if (month < 1 || month > 12) {
puts("月份输入错误!");
return 1;
}
printf("%" PRIu32 "年%" PRIu32 "月有%" PRIu32 "天。\n", year, month, return_days(year, month));

return 0;
}

uint32_t return_days(uint32_t year, uint32_t month) {
uint32_t days[] = { 31, is_leap_year(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
return days[month - 1];
}

bool is_leap_year(uint32_t year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

9. 递归

递归是指在函数的定义中,函数直接或间接调用自身。它也是一种通过将问题分解成与自身结构相似的子问题来解决问题的方法。

为了防止无限循环,递归必须有一个终止条件(也称为递归出口)。

不过递归操作会导致程序来回“套娃”,性能很差,在实际开发过程中应该尽量避免递归。

递归常用于解决数学问题和计算机科学中的复杂问题,最典型的一种就是计算一个数的阶乘。

例如写一个函数,计算传入的数的阶乘,将计算结果返回。

1
2
3
4
5
6
uint32_t factorial(uint32_t n) {
if (n == 0) {
return 1;
}
return n * factorial(n - 1);
}

这个函数会返回函数本身,使得函数的返回值一层一层向下递归,直到传入函数的值变为0,递归终止。

# 10. 尾递归

每一次递归都会调用一次函数,导致当递归层数很多的时候,函数的性能就会比较差。

为解决这个问题,可以采用尾递归的方式,在函数的尾部调用这个函数本身,通过多定义一个参数将每一级的计算结果传递下去,从而使运行效率大大提高。

1
2
3
4
5
6
7
uint32_t factorial(uint32_t n, uint32_t accumulator) {
if (n == 0) {
return accumulator;
} else {
return factorial(n - 1, n * accumulator);
}
}

需要注意的是,计算阶乘时,参数accumulator传入的值必须为1,这样在使用函数的时候可能会造成困惑。

为解决这个问题,可以在尾递归函数之外,再写一个正常形式的函数:

1
2
3
4
5
6
7
8
9
10
11
uint32_t factorial(uint32_t n, uint32_t accumulator) {
if (n == 0) {
return accumulator;
} else {
return factorial(n - 1, n * accumulator);
}
}

uint32_t factorial_wrapper(uint32_t n) {
return factorial(n, 1);
}

在函数中调用的时候,直接调用封装出来的函数即可。

对于一些更高级的封装方法与尾递归优化原理解析,可以参考下一节或这篇文章:尾调用优化 - 阮一峰的网络日志

11. 为什么不建议使用递归以及递归和尾递归用途区别

常规递归中,每次递归都需要在栈上保存信息,对于深层的递归,需要同时保存成千上百个调用记录,会浪费大量内存,性能很低且可能有栈溢出的风险。

尾递归是在函数尾部调用自身,可以优化传递调用,对于尾递归来说,可以复用栈中的调用记录,由于只存在一个调用记录,所以一般不会发生“栈溢出”错误。

需要注意的是,不同编译器可能对于尾递归和类似的方法的编译支持是不同的,Visual Studio中运行的编译器是MSVC,其他跨平台编译器可能是Clang、GCC等。

因此不一定所有的编译器都对尾递归有专门的优化。

传统的递归对于处理树的结构、图搜索算法、分治策略等比较适合。

尾递归一般在递归深度较大,用传统递归可能导致栈溢出的情况下使用(编译器支持的情况下)。

递归可能导致浪费性能和内存,可能有栈溢出的风险,且较为难以理解和维护,因此企业中对递归的使用非常谨慎。

12. 企业中使用"迭代方法"来替代递归

例如:写出一个函数求斐波那契数列的第n项。

我们可以使用前面学过的递归方法来实现这个函数:

1
2
3
4
5
6
uint32_t fibonacci(uint32_t n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}

不过前面说过,递归深度较大时,递归的性能比较低。

可以使用前面学过的尾递归的方法来改善性能问题,同样的,可以单独写一个函数封装这个尾递归函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint32_t fibonacci_tail(uint32_t n, uint32_t a, uint32_t b) {
if (n == 0) {
return a;
}
else if (n == 1) {
return b;
}
else {
return fibonacci_tail(n - 1, b, a + b);
}
}
uint32_t fibonacci_tail_wrapper(uint32_t n) {
return fibonacci_tail(n, 0, 1);
}

不过,不管是递归还是尾递归,逻辑比较难以理解,不好维护,因此企业中一般使用"迭代方法"来替代递归。所有循环计算逻辑都在一个函数中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint32_t fibonacci_iterative(uint32_t n) {
if (n <= 1) {
return n;
}
uint32_t fib_n_minus_2 = 0;
uint32_t fib_n_minus_1 = 1;

uint32_t fib_n = 0;

for (uint32_t i = 2; i <= n; ++i) {
fib_n = fib_n_minus_1 + fib_n_minus_2;
fib_n_minus_2 = fib_n_minus_1;
fib_n_minus_1 = fib_n;
}

return fib_n;
}

在for循环中,++i和i++得到的结果是一样的。

不过有资料说,由于i++是先赋值再加和,会在内存中申请一个临时变量来储存加和前的值,而++i不需要,因此使用++i的循环耗时更短。

也有资料说i++的逻辑已经被优化,运行时不会再产生额外的临时变量。

总而言之,对于一般的程序来说,使用++i和i++并无区别,随意使用即可。

这种“循环迭代”的方法可以更好地控制程序占用的内存和性能,且较易于维护。

13. 企业规范之void作为函数参数的必要性

当一个函数中不需要任何参数,有的教材中是这样写的,例如:

1
2
3
void greet() {
printf("Hello, World!\n");
}

括号中不写任何参数。

但这样写是不对的,在企业中被严格禁止。因为这样写时,表示这个函数可以接收任意数量、类型的参数,而不是不接收参数。

如果这个函数在调用时向其中传递一些参数,编译器不会出现任何报错。这在一些项目中是非常致命的,如果出现了一些错误,没有报错时根本无法找到问题所在。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void greet();

// C语言案例
int main(void) {
int age = 18;
greet(age);

return 0;
}

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

此时greet函数的本意是不需要任何参数,但调用时传入了一个int类型的参数,此时编译运行程序,程序没有出现任何警告和报错,正常运行。这是不符合设计预期的。

因此,当函数不接收任何参数时,括号中应该写一个void,限制此函数不接受参数。

1
2
3
void greet(void) {
printf("Hello, World!\n");
}

程序变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void greet(void);

// C语言案例
int main(void) {
int age = 18;
greet(age);

return 0;
}

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

此时编译运行程序时会出现报错:“void greet(void)”: 用于调用的参数太多。

当调用这个函数,且括号内不传入参数时,程序才能正常运行。

这样就避免了不写void导致的当错误向函数中传递参数时,编译器没有任何警告或报错的情况。

这在规范的代码设计中是非常重要的。

14. 作用域Scope

C语言中,每个变量都有其对应的作用域,变量只有在它自己的作用域中才可以被访问,在其他地方访问会直接报错。

比较常见的作用域有三种:一个函数内(局部作用域)、一个文件内(全局作用域)、整个程序内(配置文件)。

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

void demoFunction(void);

// 全局变量:在全局作用域(global scope)内定义的变量
uint32_t globalVar = 100;

// C语言案例
int main(void) {
demoFunction();

// 在main函数中无法访问demoFunction中的局部变量
// printf("Local Variable in main: %" PRIu32 "\n", localVar);

// 任何函数中都可以访问并修改全局变量,修改会影响全局变量的值
globalVar++;
printf("Global Variable in main: %" PRIu32 "\n", globalVar);
return 0;
}

void demoFunction(void) {
// 局部变量:在函数或代码块(局部作用域,local scope)内定义的变量
uint32_t localVar = 10;

// 打印变量值
printf("Local Variable: %d\n", localVar);

// 在任何函数中都可以访问全局变量
printf("Global Variable in demoFunction: %" PRIu32 "\n", globalVar);
}

15. 生命周期Lifetime

生命周期(Lifetime):描述了从变量被创建(或者是初始化)到变量被销毁(或被回收)的整个过程。

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

void demoFunction(void);

// 全局变量:初始化时开始生命周期
uint32_t globalVar = 100;

// C语言案例
int main(void) {
demoFunction();

// 局部变量的生命周期已经结束,无法访问
// printf("Local Variable in main: %" PRIu32 "\n", localVar);

globalVar++;
printf("Global Variable in main: %" PRIu32 "\n", globalVar);

// main函数结束时全局变量生命周期结束
return 0;
}

void demoFunction(void) {
// 局部变量:初始化时开始生命周期
uint32_t localVar = 10;

printf("Local Variable: %d\n", localVar);
printf("Global Variable in demoFunction: %" PRIu32 "\n", globalVar);

// 局部变量生命周期在函数结束时结束
}

企业中对生命周期的管理非常严格,生命周期管理不当可能会导致内存泄漏、访问已经被销毁的变量(悬挂引用)、过度消耗资源等问题。

16. 局部变量的作用域限定、自动存储期、初始值未定义

局部变量(Local Variables):可以供我们在一个狭窄的作用域内存储和操作数据。

关于局部变量生命周期的一些词汇:

  • 作用域限定:局部变量只在他们被声明的函数或代码块中可见。
  • 自动存储期:局部变量通常具有自动存储期,例如一个函数中有一个局部变量,每一次调用这个函数,一个新的局部变量都会被自动创建,同时在这个函数运行结束之后,这个局部变量会被销毁(回收)。
  • 初始值未定义:除非显式初始化,否则局部变量的值是未定义的,此时这个未定义的局部变量将会被分配在栈上,而栈上的内存一般不会自动清零。因此,只要是定义了局部变量,尽量都要显式初始化。

17. 全局变量的跨越边界性与程序范围可见性,静态存储期,默认初始化

全局变量可以跨越函数边界,在整个文件中共享数据,具有程序范围的可见性。

静态存储期:全局变量在整一个程序开始时被创建,在这个程序执行结束时被销毁。

默认初始化:如果全局变量没有被显式初始化,则默认被初始化为0。如果这个全局变量是一个指针,则默认被初始化为NULL。注意与局部变量的初始化区分。

使用全局变量,可以减少需要传递给函数的参数数量,简化调用。

但是使用过多的全局变量可能会降低代码的可读性,使得变量在程序中的变化难以被追踪,从而增加出错的风险。

线程安全问题:在多线程环境中,未加锁的全局变量可能会导致数据竞争和不一致的问题。

18. 静态局部变量static local variables

一个静态局部变量在函数的内部声明,并且整个程序期间只初始化一次。即使这个函数执行结束,它的值也不会被销毁,而是一直被保持到下次函数调用。其生命周期会贯穿整个程序的运行期间。

有跨函数调用保持状态和计数等作用。

例如下面这个示例程序,如果函数increment_counter中的局部变量不是static类型,则每次调用这个函数,counter都被初始化为0,最后三次打印出的counter的值也都是0。

如果定义的局部变量是static类型,则counter的值会在函数调用之间保持,最后打印出的值分别为1、2、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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

void increment_counter(void);

// C语言案例:静态局部变量
int main(void) {
// 在其他函数中无法访问该静态局部变量
//printf("Counter: %" PRIu32 "\n", counter);

for (uint32_t i = 0; i < 3; i++) {
increment_counter();
}

return 0;
}

void increment_counter(void) {
// 静态局部变量
// 在函数调用之间保持其值
static uint32_t counter = 0;
counter++;
printf("Counter: %" PRIu32 "\n", counter);
}

静态局部变量和全局变量的区别在于,全局变量在一个文件中的任意函数中都可以使用,但静态局部变量的生命周期虽然是整个程序运行过程,但其作用域仍然是它被初始化的函数中,在其他函数中无法使用。

19. extern全局变量与跨文件访问

在学习静态全局变量之前,需要先了解变量是如何跨文件访问的。

例如目前main函数是在main.c文件中,其中定义了一个全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 全局变量定义并初始化
uint32_t g_val = 100;

void change_g_val();

// C语言案例:extern跨文件变量
int main(void) {
printf("g_val in main: %" PRIu32 "\n", g_val);
change_g_val();

return 0;
}

void change_g_val() {
g_val += 50; // 修改全局变量
printf("g_val in change_g_val: %" PRIu32 "\n", g_val);
}

当需要在另一个文件中使用这个文件中的全局变量,就需要在那个文件中声明extern全局变量,程序会自动使用其他文件中定义的全局变量。

例如另一个文件为helper.c:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 通过extern关键字声明对外部文件的全局变量的访问
extern uint32_t g_val;

void change_g_val_extern() {
g_val += 50; // 修改外部文件的全局变量
printf("g_val in change_g_val_extern: %" PRIu32 "\n", g_val);
}

此时,在main函数中调用helper.c文件中的change_g_val_extern函数,可以成功实现在一个文件中访问和修改另一个文件中的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 全局变量定义并初始化
uint32_t g_val = 100;

void change_g_val();

// C语言案例:extern跨文件变量
int main(void) {
printf("g_val in main: %" PRIu32 "\n", g_val);
change_g_val();

// 调用外部文件中的函数,修改全局变量
change_g_val_extern();

return 0;
}

void change_g_val() {
g_val += 50; // 修改全局变量
printf("g_val in change_g_val: %" PRIu32 "\n", g_val);
}

需要注意的是,此处在main函数中调用外部文件中函数的方式只是一个简单演示,并不规范,可能会导致很多问题。

一般正确的调用应该是创建helper.c文件对应的helper.h头文件,在其中声明函数,并在main.c中包含头文件之后,才可以调用。

20. 全局静态变量:文件限定

当全局变量加上static关键字时,变为静态全局变量,使得这个变量只在定义它的这个文件中可以被使用,在其他文件中即使加上extern关键字也无法使用。

21. register寄存器声明

寄存器(Register)是中央处理器内用来暂存指令、数据和地址的电脑存储器。寄存器的存贮容量有限,读写速度非常快。

简单来说,寄存器位于CPU内部,使用寄存器存储一些数据用于CPU计算时,由于不需要经过CPU与内存之间的数据交换,因此计算速度可以非常快。

C语言中,定义寄存器变量可以使用register关键字,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void demo_reg_var(void);

// C语言案例
int main(void) {
demo_reg_var();

return 0;
}

void demo_reg_var(void) {
// 声明一个寄存器变量
register int counter;

for (counter = 0; counter < 10; counter++) {
printf("Counter: %d\n", counter);
}
}

需要注意的是,register变量由于存储与寄存器中,因此不能使用取地址符&获取其地址。

此外,对于不同平台,不同编译器,不一定都能成功使用寄存器存储变量,因此register关键字的作用是建议编译器,尽可能将这个变量放在寄存器中。环境不支持的情况下,即使使用了register关键字,变量也可能并没有被放在寄存器中。

需要对一个变量做频繁读取和修改时,在合适的时候使用寄存器变量,可能会提升程序的运行速度。

目前的现代化编译器已经可以自动化地对一些变量进行优化,可能会自动将一些特殊的变量放在寄存器中,因此除非对程序设计有特殊要求,一般不需要手动使用register关键字声明寄存器变量。

22. 块作用域(Block Scope)与链接性(Linkage)

块作用域(Block Scope)和前面说的局部作用域类似,制定了一个变量的最小作用域,例如一个函数内:

1
2
3
4
5
6
7
8
9
10
// 函数作用域(Function Scope)
void demo(int num_1) {
// 块作用域(Block Scope)
int num_2 = 20; // “num_2”具有块作用域,仅在demo函数中可见

if (num_2 > 5)
{
int num_3 = num_1 + num_2; // “num_3”具有块作用域,仅在if语句中可见
}
}

链接性(Linkage)描述了一个标识符(变量名、函数名等)在同一个程序,多个文件中如何被共享地访问。

最简单的一个内部链接就是在main函数中调用本文件中写的其他函数,外部链接就比如调用定义在其他文件中的函数或变量。而局部变量是无链接的。

23. 函数的注释

在大型项目中,为了保证代码的可读性,与函数命名必须符合开发要求类似,为每个函数添加描述这个函数功能及接口等信息的注释是必不可少的。

一般项目为了更为国际化,函数的注释全部采用英文。

函数注释中的第一部分,介绍这个函数的功能,可以写多行;

第二部分写这个函数的参数有哪些,参数的类型与意义是什么;

第三部分写这个函数的返回值是什么,是什么数据类型,返回的值的意义是什么;

第四部分是对这个函数的一些相关说明;

最后一部分可以写开发者的个人信息、日期等信息。

例如一个将两个数加和的结果返回的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Calculates the sum of two integers.
*
* Parameters:
* - num_1: The first integer to be added.
* - num_2: The second integer to be added.
*
* Returns:
* - The sum of num_1 and num_2 as an int32_t.
*
* Notes:
* - The function uses int32_t to ensure the calculation is compatible across different platforms.
* - This is a basic example intended for beginners who are not yet familiar with pointer.
*
* Name:
* - HC
* - email: xxxxx
* - date: xxxx-xx-xx
*/
int32_t add_two_numbers(int32_t num_1, int32_t num_2) {
return num_1 + num_2;
}

这是其中一种注释的方式,也有其他的函数注释格式,比如使用@符加名字来表明注释类型,这在某些IDE中可以被自动识别并展示在函数的悬浮框中,便于回看注释,快速了解函数的功能等信息。例如:

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief Adds two numbers together.
*
* @param num_1 The first number to add.
* @param num_2 The second number to add.
*
* @return The sum of num_1 and num_2.
*/
static int32_t add_two_numbers(int32_t num_1, int32_t num_2) {
return num_1 + num_2;
}

在鼠标悬浮到函数名上时,可以展示函数的相关信息,在大型项目中可以很快捷方便地得知一个函数的功能:

1

24. 第七章结束语

至此,函数的基础用法介绍到此结束。

后面可能还会涉及到有关函数的更深入的内容,例如什么叫库函数,如何在其他文件中编写函数,头文件的使用,多文件的开发协作等内容。

这一章节最主要的部分包括,什么是函数,函数怎么声明,函数如何编写等,同时也有大量的有关函数的企业规范。

函数的中心思想就是模块化思想,需要有模块化思维,将一些可以复用的功能抽离出来,整理成一个个函数。

多思考!多练习!

本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2025/10/16/20251016_C_function/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
一种现代化的Git分支模型:Lean Branching
  1. 1. 1. function函数的介绍与作用
  2. 2. 2. 函数声明与定义的规则
  3. 3. 3. 函数的参数
  4. 4. 4. 案例:求面积函数
  5. 5. 5. 函数编写要领
  6. 6. 6. 初步认识全局变量、局部变量
  7. 7. 7. 游戏案例:石头剪刀布与软件工程的“规则映射”设计技巧
  8. 8. 8. 案例:软件工程设计技巧:表驱动法之再探成绩评分系统、再探闰年返回月份天数
  9. 9. 9. 递归
  10. 10. 11. 为什么不建议使用递归以及递归和尾递归用途区别
  11. 11. 12. 企业中使用"迭代方法"来替代递归
  12. 12. 13. 企业规范之void作为函数参数的必要性
  13. 13. 14. 作用域Scope
  14. 14. 15. 生命周期Lifetime
  15. 15. 16. 局部变量的作用域限定、自动存储期、初始值未定义
  16. 16. 17. 全局变量的跨越边界性与程序范围可见性,静态存储期,默认初始化
  17. 17. 18. 静态局部变量static local variables
  18. 18. 19. extern全局变量与跨文件访问
  19. 19. 20. 全局静态变量:文件限定
  20. 20. 21. register寄存器声明
  21. 21. 22. 块作用域(Block Scope)与链接性(Linkage)
  22. 22. 23. 函数的注释
  23. 23. 24. 第七章结束语
© 2021-2025 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • C
  • FPGA
  • Homework
  • MATLAB
  • Server
  • Vivado
  • Git

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank