C语言 定义和声明
定义和声明
在 C 语言中,定义和声明是两个极其容易混淆的概念,定义是明确告诉编译器完整的信息,而声明只是给编译器打一个“白条”,具有如下特点:
- 声明对应的定义可以暂时并不提供,定义可能在源文件的后面,也可能在其它源文件或编译单元中。
- 前置声明可以让我们在正式定义之前就使用它,这对于解决循环依赖是必要的。
- 同样的声明可以重复多次提供,但是定义至多只有一次,否则会报错符号重定义。
- 如果编译器在链接所有编译单元时,如果需要使用某个具体实现,但是没有找到声明对应的定义——对应的“白条”没有被兑现,就会报错符号未定义。
- 如果只是提供了一个声明,但是没有实际使用它,那么编译器会直接将其忽略掉,此时即使没有提供对应的定义也是允许的,因为压根不需要。
我们主要考虑变量的声明/定义和函数的声明/定义:
- 变量:
- 变量声明:变量声明只是告诉编译器变量的名称和类型,但不分配内存。
- 变量定义:变量定义是在编译过程中分配内存和值并为变量指定名称的操作。
- 函数:
- 函数声明:函数声明(也称为函数原型)包含函数的返回类型、参数列表和函数名,但没有函数体。它只是告诉编译器有这个函数存在,但是并不提供实现。
- 函数定义:函数定义包括函数的完整实现,包括函数的返回类型、参数列表和函数体。
对于涉及到自定义数据结构(例如结构体)的内容在本文中不做讨论,因为语法比较复杂,并且C和C++的处理并不相同。
定义和声明语句通常是难以区分的,因为C语言在这方面的语法比较灵活自由,有很多默认行为。
变量定义/声明
变量定义/声明的标准示例如下,必须加上extern
关键词才可能被识别为变量声明,否则会被编译器尝试改为变量定义(见下文)
1
2
3extern int x_dec; // 变量声明
int x_def = 10; // 变量定义
如果我们在声明的语句中同时提供了值,那么它就会被重新视作定义,extern
将会被忽略。(C语言标准并不推荐这种用法,但是编译器通常是这么实现的)
1
extern int x_def = 10; // 变量声明
变量的声明可以重复出现,但是定义不行 1
2
3
4
5
6
7
8
9
10
11
extern int s; // 变量声明
extern int s; // 变量声明
int s; // 变量定义
int main() {
s = 10;
return 0;
}
下面的代码则会报错符号重定义 1
2
3
4
5
6
7
8
9
10
int s; // 变量定义
int s; // 变量定义
int main() {
s = 10;
return 0;
}
在多文件编程中,下面的示例会导致符号s
重定义
1
2
3
4
5
6// func.c
int s = 10; // 变量定义
// main.c
int s = 100; // 变量定义
int main(){;}
一个合理的做法是将其中一个改为变量声明,例如 1
2
3
4
5
6// func.c
int s = 10; // 变量定义
// main.c
extern int s; // 变量声明
int main(){;}
或者在公共头文件中提供变量声明,在某一个源文件中提供变量定义,例如
1
2
3
4
5
6
7
8
9
10// head.h
extern int s; // 变量声明
// func.c
int s = 10; // 变量定义
// main.c
int main(){;}
对下面的语句是变量定义还是变量声明的判定比较复杂,这被称作试探性定义:如果在附近没有找到定义,那么语句就会被视作定义。
1
int x; // 变量定义
如果在当前作用域的前面或后面找到了定义,那么int x;
语句会退化为声明
1
2
3
4
5
6
7int x; // 变量声明
int x = 10; // 变量定义
int y = 20; // 变量定义
int y; // 变量声明
如果我们在变量定义中没有提供默认值,对全局变量会执行默认初始化,对于局部变量则不会。
函数定义/声明
与变量定义/声明不同,函数定义/声明的区分非常简单:以;
结尾不含函数体的,就是函数声明,反之就是函数定义。
标准示例如下 1
2
3extern void fn_dec(); // 函数声明
void fn_def(){} // 函数定义
与变量声明不同的是,即使忽略extern
关键词,函数声明也不会被编译器视作函数定义,因为没有提供函数体。
1
void fn_dec(); // 函数声明
通常的做法是使用前置的函数声明,例如 1
2
3
4
5
6
7
8void hello(); // 函数声明
int main(){
hello();
return 0;
}
void hello(){} // 函数定义
这里我们可以在提供函数定义之前就使用hello()
函数。
在多文件编程中,可以在其中一个源文件中定义函数,在另一个源文件中声明函数并使用,例如
1
2
3
4
5
6
7
8
9
10// func.c
void hello(){} // 函数定义
// main.c
void hello(); // 函数声明
int main(){
hello();
return 0;
}
更推荐的做法是将函数声明放置在公共头文件中,例如 1
2
3
4
5
6
7
8
9
10
11
12
13// head.h
void hello(); // 函数声明
// func.c
void hello(){} // 函数定义
// main.c
int main(){
hello();
return 0;
}
对于含有参数的函数,在函数声明时,编译器只关心几个信息:函数名称+参数个数和对应类型+返回值类型,并不关注具体的参数名称。
例如我们在函数声明中可以省略所有的参数名称(为了可读性,不建议这么做)
1
2
3
4
5int add(int, int, int);
int add(int a, int b, int c){
return a+b+c;
}
如果没有实际使用某个参数,那么即使在函数定义时也可能省略参数名称(不能省略参数类型),例如下面的函数并没有使用第三个参数
1
2
3int add2(int a, int b, int){
return a+b+c;
}
小结
通常情况下,我们主要在如下的情景中使用函数声明和变量声明:
- 单文件编程中,我们习惯在源文件的头部集中提供前置声明,这使我们在正式定义之前就可以使用它们;
- 多文件编程中,通常在公共头文件中提供声明,以供不同的编译单元使用;
- 对于库,需要在接口头文件中提供正确使用所必要的声明。