标准C不完全声明,及头文件
本文主要介绍标准C的不完全声明及头文件。
1. One-pass complier
Question 1.0. 什么是 ‘one-pass complier’ ?
Answers 1.0. ‘One-pass complier’ [4] 是一种和 ‘multi-pass complier’ [3] 相对的编译方式。顾名思义,基于 ‘one-pass complier’ 的编译器在编译的时候,通常仅仅顺序地遍历一次源代码;相反地,基于 ‘multi-pass complier’ 的编译器在编译的时候,通常需要遍历多次源代码。
显然,’one-pass compliers’ 比 ‘multi-pass compliers’ 需要更少的代码链接时间 [6],因为’one-pass compliers’ 默认用户已经根据 ‘先声明后使用’ 的原则 [7] 顺序地组织好源代码。一旦用户没有按照该原则 [7] 进行代码的组织,编译器立即返回相关错误,编译失败;而在相同情况下,基于 ‘multi-pass compliers’ 的编译器并不会因为首次找不到声明代码段而抛出编译错误,而是顺序地遍历多次源代码,每一次遍历的结果作为下一次遍历的输入,不断优化迭代指令,直至最终编译完成,避免声明代码后置导致单次顺序编译失败的情况的发生。
举个例子,标准C及其衍生语言,Python 等是典型的 ‘one-pass compliers’ 类型的编译器,Java,Go 是典型的 ‘multi-pass compliers’ 类型的编译器,下面我们以C,Go 代码例子来看看两者的区别,
标准C 实现的
add.c
的源代码如下,#include <stdio.h> void main() { int a = 1; int b = 2; printf("%d", add(a, b)); } int add(int a, int b) { return a + b; }
编译失败:”add.c:8:15: warning: implicit declaration of function ‘add’ [-Wimplicit-function-declaration]” – gcc version 5.4.0 20160609
Go 实现的
add.go
的源代码如下,package main import "fmt" func main() { fmt.Println(add(42, 13)) } func add(x int, y int) int { return x + y }
编译成功:
add.go
在 go version go1.10.1 linux/amd64 下成功编译~$go build add.go
~$./add
55
Question 1.1. 什么是不完全声明? 为什么不完全声明能解决 声明代码后置导致单次顺序编译失败情况?
Answer 1.1. 不完全声明又常被称为前置声明 [1, 2],通常是 one-pass compliers
应对 “声明代码后置导致单次顺序编译失败情况” 的一种解决方案。本质上,不完全声明是一种不完整的 class
或者说是 struct
,因为它只告诉编译器某个 class
/ struct
存在的事实,并没告诉编译器该 class
/ struct
完整的内部信息,比如函数内部运算等——所以它叫不完全声明。另一方面,不完全声明的代码段通常在预编译的时候被编译器处理——所以不完全声明也常被叫作前置声明。
不完全声明是如何解决 声明代码后置导致单次顺序编译失败情况 的呢?以标准C来举个例子,
方法一: 在
.c
文件中进行不完全声明,add.c
的代码如下,#include <stdio.h> int add(int a, int b); // forward declaration void main() { int a = 1; int b = 2; printf("%d", add(a, b)); } int add(int a, int b) { return a + b; }
编译成功,gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu~16.04.10)
~$ gcc add.c -o add
~$ ls
add.c add
~$ ./add
3
方法二:在
.h
文件中进行不完全声明,add.h
的代码如下,#ifndef add_h #define add_h #include <stdio.h> int add(int a, int b); // forward declaration #endif
add.c
的代码如下,#include "add.h" void main() { int a = 1; int b = 2; printf("%d", add(a, b)); } int add(int a, int b) { return a + b; }
编译成功,gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu~16.04.10)
~$ gcc add.c -o add
~$ ls
add.c add
~$ ./add
3
方法一 和 方法二 哪个好?为什么好?好在哪里?
通常来讲,我们推荐将不完全声明写在
.h
文件而非.c
文件 [2]。这样做的好处有以下几点:其一,降低程序的耦合度,特别是在项目规模较大的时候。换句话说,将成千上万行代码全部放到一个.c
文件,显然不能说是一个好的做法,这样不便于程序的阅读和修改。其二,方便模块化编程。换句话说,将不同代码段 “封装” 成不同的模块,需要即调用,增加编程效率。因此,我们推荐将不完全声明写在.h
头文件中。
Question 1.2. 为什么不完全声明提供的是不完整的信息 (比如函数名,函数返回类型), 而不是完整的信息?
Answer 1.2. 这个问题存在一个逻辑矛盾:不完全声明的定义是:在class
或 struct
的声明代码 (注:声明代码是名词,表示正式实现的class
或者struct
代码;前置声明是动词,表示在声明代码之前提前告知编译器声明代码的存在的事实) 之前提前告知编译器声明代码的存在,避免编译器顺序编译的时候找不到后置的声明代码。
因此,如果不完全声明提供的是完整的class
或 struct
的实现,就会出现两个问题:其一,和不完全声明的定义相矛盾,即自我矛盾。其二,编译器无法正常编译,出现class
或struct
重定义的编译错误。其三,C编译器设计时期内存消耗是考虑在内的,在one-pass compliers
的技术框架内,满足一定编程灵活性的前提下,代码应该尽可能精简。举个例子,
add.h
的实现如下,
#ifndef add_h
#define add_h
#include <stdio.h>
// int add(int a, int b); // forward declaration
int add(int a, int b)
{
return a + b;
}
#endif
add.c
的实现如下,
#include "add.h"
void main()
{
int a = 1;
int b = 2;
printf("%d", add(a, b));
}
int add(int a, int b)
{
return a + b;
}
编译失败,gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu~16.04.10)
~$ gcc add.c -o add
add.c:12:5: error: redefinition of ‘add’
References
[1. Include diractive] https://en.wikipedia.org/wiki/Include_directive
[2. 不完全声明] http://umich.edu/~eecs381/handouts/IncompleteDeclarations.pdf
[3. multi-time compliers] https://en.wikipedia.org/wiki/Multi-pass_compiler
[4. one-pass compliers] https://en.wikipedia.org/wiki/One-pass_compiler
[5. one-pass compliers problem] https://en.wikipedia.org/wiki/One-pass_compiler
[6. link time] https://en.wikipedia.org/wiki/Link_time
[7. forward declaration] https://en.wikipedia.org/wiki/Forward_declaration