6.1 函数声明
函数声明告诉编译器函数的名字、返回类型和形参类型。在同一个源文件中,可以先声明函数,再在后面给出定义。
1. 同一文件内的声明与定义
c
#include <stdio.h>
int add(int left, int right);
int main(void) {
printf("%d\n", add(3, 4));
return 0;
}
int add(int left, int right) {
return left + right;
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
声明使 main 中的调用点能够接受完整的类型检查,定义则提供实际执行的函数体。
2. 为什么原型重要
函数原型让编译器在调用点执行类型检查,提前发现实参类型不匹配问题。缺少原型时,历史兼容规则会引入默认提升,错误往往延迟到运行期。
3. 旧式声明的版本边界
C 早期存在未给出原型信息的旧式写法,但现代代码应避免继续使用。缺少完整原型会削弱编译期类型检查,尤其在可变参数或隐式转换路径上更容易埋下错误。
c
/* 推荐:给出完整原型 */
double norm2(double x, double y);1
2
2
无论调用点位于函数定义之前还是之后,都应保证声明先于调用,并让调用表达式接受完整的类型检查。
4. 习题
4.1 巩固
#10606
⚡1⏳2
下面的程序在 main 之后定义了两个函数,但只声明了 add。
c
#include <stdio.h>
int add(int left, int right);
int main(void) {
printf("%d\n", add(3, 4));
printf("%g\n", half(7.0));
return 0;
}
int add(int left, int right) {
return left + right;
}
double half(double value) {
return value / 2;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 原样用
gcc -std=c17 -Wall -Wextra构建。先写下你认为会出现的诊断,再编译核对:关于half出现了哪几类诊断,最后一条 error 的原文是什么。程序能构建成功吗? - 给
half补上原型double half(double value);,同时删掉add的原型,重新构建并运行。这时还有几条警告,各自指向哪个函数?输出是否仍为7和3.5?说明为什么同样是缺少原型,half会直接报 error,add只留下警告。 - 把两个函数的定义整体移到
main之前,不写任何声明,重新构建。确认没有诊断,运行输出仍为两行7与3.5。
第 1 步里,half 没有声明,调用点按旧规则把返回值当成 int;等到后面出现 double half(double) 的定义,两次声明对不上,编译器报 conflicting types for 'half'。add 有完整原型,所以没有问题。
#10607
⚡2⏳3
原型决定实参在调用点怎样转换成形参的类型。
c
#include <stdio.h>
int add(int left, int right);
double scale(double value, double factor);
int main(void) {
printf("%d\n", add(3.9, 4.1));
printf("%d\n", add('A', 1));
printf("%g\n", scale(3, 2.5));
printf("%g\n", scale(1.0f / 3.0f, 3));
return 0;
}
int add(int left, int right) {
return left + right;
}
double scale(double value, double factor) {
return value * factor;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 先写出四行输出,再编译运行核对。
- 加上
-Wconversion编译,抄下新增的警告。哪两个实参被截断?截断发生在调用点还是函数体内?add('A', 1)与scale(1.0f / 3.0f, 3)为什么没有警告? - 删掉
add的原型,重新构建运行。add(3.9, 4.1)这一行输出什么?这个结果既不是7,也不应指望换一台机器还是它:没有原型时,调用点不知道该把实参转换成int,double实参按原样传过去,函数却按int读取。记录本机的输出即可。 - 恢复
add的原型,改删scale的原型,重新构建。这一次为什么连编译都过不去?
验收:第 1 步四行为 7、66、7.5、1;第 4 步的最后一条 error 是 conflicting types for 'scale'。
4.2 编程
#10608
⚡3⏳3
空括号的声明 int f(); 在 C17 里表示「参数未给出」,不表示「没有参数」。
c
#include <stdio.h>
int f();
int main(void) {
printf("%d\n", f(1));
printf("%d\n", f(1, 2));
return 0;
}
int f(int x) {
return x;
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 用
gcc -std=c17 -Wall -Wextra构建运行。编译有没有诊断?两次调用各输出什么?本机上多出来的实参被忽略,函数只读到第一个;标准没有规定这种调用,换一个实现可能就不是这个结果。 - 加上
-Wstrict-prototypes重新构建,抄下警告。它提醒的是什么? - 把声明改成
int f(void);,重新构建,抄下 error。说明(void)与()在调用点检查上的差别。 - 换用
-std=c2x构建原程序(空括号,GCC 13.2)。结果与 C17 有什么不同?C23 起空形参列表的含义改成了什么? - 写一对相互递归的函数
bool is_even(unsigned int n)与bool is_odd(unsigned int n),不用取模,每次把n减 1 后交给对方。只在文件开头给is_odd一个完整原型,is_even直接定义在前面。构建并输出 0~4 的结果,说明这里为什么只声明一个就够。
验收:第 5 步输出五行,第一列为 n,后两列是 is_even 与 is_odd 的结果,每行形如 0 1 0;gcc -std=c17 -Wall -Wextra 无诊断。
4.3 综合与挑战
#10609
⚡3⏳3
把几个函数拆进两个源文件,用一个头文件让调用方与实现方看到同一份声明。接口如下:
c
/* rect.h */
int rect_area(int width, int height);
int rect_perimeter(int width, int height);
double rect_ratio(int width, int height);
double rect_average(int total, int count);1
2
3
4
5
2
3
4
5
main 已经写好,不要改动:
c
#include <stdio.h>
#include "rect.h"
int main(void) {
printf("%d\n", rect_area(3, 5));
printf("%d\n", rect_perimeter(3, 5));
printf("%g\n", rect_ratio(3, 5));
printf("%g\n", rect_average(10, 2));
return 0;
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 在同一目录下写
rect.h与rect.c:rect.h带#ifndef防护,rect.c第一行包含它;四个函数分别返回面积、周长、宽高比与平均值。用gcc -std=c17 -Wall -Wextra main.c rect.c -o app构建,运行,记录四行输出。 - 在
rect.c里把rect_ratio的返回类型改成int,头文件不动,重新构建。诊断出现在哪个文件、哪一行?如果rect.c不包含rect.h,同样的改动还能被发现吗? - 把第 2 步的改动改回去,然后删掉
rect.c的#include "rect.h",重新构建运行。程序还能正确工作吗?再加上-Wmissing-prototypes编译,抄下警告,说明它检查的是什么。 - 在
rect.c里把rect_average的形参顺序改成(int count, int total),函数体与头文件都不改,重新构建。编译器有没有诊断?运行后第四行变成什么?由此说明原型能检查什么、不能检查什么。
验收:第 1 步输出 15、16、0.6、5;第 4 步输出 0.2。