一个例子
C++遵循先定义,后使用的原则。就拿函数的使用来举例吧。
我看过有些人喜欢这样写函数。
#include<iostream> using namespace std; int add(int x, int y) //A { return x + y; } int main() { int re = add(1, 2); //B system("pause"); return 0; }
但我更偏向下面这种。
#include<iostream> using namespace std; int add(int x, int y); //A int main() { int re = add(1, 2); //B system("pause"); return 0; } int add(int x, int y) //C { return x + y; }
C++的编译是以文件为单位,在某一个特定源文件中,则是从上至下,逐行解析的。
第一种风格中,A处的代码既是函数的定义(函数的实现),也充当了函数的声明。函数的定义是函数正真的实体,和逻辑实现。而声明则是告知编译器:我这个函数存在,我这个函数外观是什么样的(即 :返回值,参数类型和参数个数的相关信息)。
当编译器分析到B处代码时,编译器已经知道了函数存在,则允许出现这个函数的调用,知道了函数的外观,编译器还会分析函数调用时是否正确使用了,如参数个数,参数类型等。
这个过程中拆解开来就是:定义 , 声明 , 使用。显然第二种风格更好的阐述了这三部分。
那么问题来了?当项目过大后,实体(函数定义,类,结构体,变量定义等)会放在不同的文件中。但是编译器又是以文件为单位处理的,它在处理main.cpp时,完全不知道lib.cpp的任何信息。
那么,如果要在main.cpp中调用lib.cpp中定义的函数,就必须手动将lib.cpp中的函数头拷贝声明到main.cpp中。那如果在100个源文件中调用了lib.cpp中的函数,岂不是要拷贝100份?
这样累不说,又容易出错,还不利于后期维护。
于是预处理器说:“你只需将lib.cpp中的需要共享,在其他源文件中使用的实体单独声明到一个头文件lib.h中吧,别的源文件需要使用你的lib.cpp中的实体,只需要在他们的源文件中加上一行预处理指令:#include"lib.h" 就OK了,剩下事交给我”
于是一切变成了这样:
/*lib.cpp*/ #include"lib.h" int add(int x,int y) { return x+y; }
/*lib.h*/ #ifndef _LIB_H__ #define _LIB_H__
int add(int x, int y);
#endif
/*main.cpp*/
#include<iostream> #include"lib.h" using namespace std; int main() { int re = add(1, 2); cout << re << endl; system("pause"); return 0; }
那么问题又来了:预处理器它到底是怎么帮你的呢,它做了什么手脚?
下面我们就来用VS2013看看预处理的结果。如何查看预处理结果--->点我
如果安装有g++编译器,则可以使用命令: g++ -E main.cpp -o main.i 来生成预处理文件
/*lib.i文件*/ int add(int x, int y); int add(int x,int y) { return x+y; }
/*main.i 前面省略87260行,都是iostream头文件里面的东西,真多! */ int add(int x, int y); using namespace std; int main() { int re = add(1, 2); cout << re << endl; system("pause"); return 0; }
总结:
1、预处理器在.cpp中遇到#include<> 或者 #include " ", 都会将#include<> 或者 #include " "指令替换为他们包含的头文件中的内容,形成 .i文件。
这就是预处理器对头文件的处理结果。当然还要考虑到预处理器也是有逻辑的,比如防止重复包含#ifndef .......#define .......#endif
2、头文件只在预处理期起作用,预处理过后生成 .i 文件,此后头文件就没有作用了。
3、预处理指令 的 作用域 为 源文件作用域,也就是每 一条 预处理指令 只在它所在的 .cpp文件有效。
4、预处理不属于任何名称空间,名称空间“管不住”预处理指令。预处理指令不受C/C++的作用域规则,它是一种编译前期的机制。
5、用户将一个源文件( .c 或者 .cpp ) 提交给编译器后,首先执行的是该文件的预处理(处理源文件中的预处理指令),预处理后的结果为编译单元,这种编译单元才是编译器真正 工作的对象!程序员眼中看见的是 源文件(.c 或者 .cpp )和头文件, 而编译器眼中只有编译单元(预处理后形成的.i文件)。但是我 们口头上说的C/C++编译器包括预处理器。
如果你不理解C/C++的编译过程,请点 击我
以下通过几个特例说明需要注意事项.
防止头文件的重复包含
当项目大了后,编译单元之间的关系变得复杂,就很可能出现头文件重复包含的情况。虽然大多是情况下,重复包含头文件是没有问题的,但是这会使.i文件膨胀 。
C/C++遵循单定义,多声明的规则。声明多次没问题,但是不必要的声明除了在利于代码阅读外的目的下使用外,其他的要尽量避免。
一般采用以下方法。
#ifndef _XXX_H__ #define _XXX_H__ //被包含内容放中间 #endif
C++提供了#pragma once 预处理指令来达到上述效果,但是很多人习惯了C中的写法,也为了获得更大的兼容性。
普通全局变量
有时为了让一个源文件中的全局变量在多个源文件中的共享。
普通全局变量是有外部链接性(多个源文件共享一个实体),也就是它可以在所有的.cpp文件中共享使用。因为它将在所有的源文件中共享,所以必须保证不会在其他源文件的任何地方出现相同的链接性且相同名称的全局变量,正所谓一山不容二虎。
下面是错误的写法,编译不通过,提示错误:error : “int sum”: 重定义
相信如果你明白了头文件和预处理的机制,你就应该知道为什么是错误的了。
/*share.cpp*/ #include"share.h" int sum = 100;
/*share.h*/ #ifndef _SHARE_H__ #define _SHARE_H__ int sum; #endif
/*main.cpp*/
#include<iostream> #include"share.h" using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
贴出代码说良心话。下面就是预处理后的结果。很明显的:全局变量sum的确重复定义了。在share.i 中 重复定义了2次,在main.i中又定义了一次,一共定义了3次!
/* share.i */ int sum; int sum = 100;
/*main.i 省略iostream 中的代码 */ int sum; using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
要保证其他源文件能使用普通全局变量,又不会重定义,就要使用extern关键字来声明。extern用来声明。
改:
/*share.cpp*/ #include"share.h" int sum = 100; //extern int sum = 100; 也是OK的。 //这里 的extern是可选的,加上extern 的唯一目的是,暗示这个变量会被其他文件使用
/*share.h*/ #ifndef _SHARE_H__ #define _SHARE_H__ extern int sum; #endif
/*main.cpp*/
#include<iostream> #include"share.h" using namespace std; int main() { cout << sum << endl; system("pause"); return 0; }
全局static变量
static修饰全局变量的目的就是为了将外部链接性改为内部链接性(仅仅在定义它的文件中共享,对其他文件隐藏自己,定义的实体是源文件私有的)。
这样避免了对外部链接性空间的名称污染,其他源文件完全可以定义具有同名的外部链接的变量,当然也可以定义同名的内部链接变量。
static修饰的全局变量 和 全局函数 都不要在对应模块的头文件中声明,因为static是为了隐藏在一个源文件中,而放在头文件中则是为了方便其他源文件使用,这2者显然矛盾了。
下面我们尝试开发一个对int数组排序的库来说明问题。举一反三靠大家自己了。
/*sort.cpp*/
/*
我们使用了2个函数完成排序:1、swap用于交换2个值,bubble_sort则是排序的实现。显然我们只想对其他使用者提供 bubble_sort这一个函数接口,而对外隐藏swap函数。于是将swap修饰为static
*/
#include"sort.h"
static void swap(int &a, int&b); //static 函数仅仅在自己的源文件声明就够了,不要在头文件中声明。
//为什么 需要在自己的源文件中声明呢?假如将下面的 swap函数和bubble_sort函数定义的位置交换下,那么
//编译器在从上往下解析sort.cpp时,会先看见swap在bubble_sort中的调用,而编译器事先不知道swap的任何声明和外观信息。
static void swap(int &a, int&b) { int t = a; a = b; b = t; } void bubble_sort(int arr[], int len) { for (int i = 0; i < len - 1; ++i) { for (int j = 0; j < len - 1 - i; ++j) { if (arr[j]>arr[j + 1]) swap(arr[j], arr[j + 1]); } } }
/*sort.h*/ #ifndef _SORT_H_ #define _SORT_H_ void bubble_sort(int arr[], int len); #endif
#include"sort.h" #include<iostream> using namespace std; int main() { int arr[5] = { 12, 6, -12, 44, -90 }; bubble_sort(arr, 5); for (size_t i = 0; i < 5; ++i) { cout << arr[i] << endl; } system("pause"); return 0; }
全局const常量
全局const默认是具有内部链接性,就像使用了static修饰后一样。(C程序员朋友注意,和C++不同,const常量在C中依旧是外部链接性的)
/*test.cpp*/ const int foo = 12; //等价于 static const int foo = 12;
由于const全局常量是内部链接性的,所以我们可以将 const定义放在头文件中,这样所有包含了这个头文件的源文件都有了自己的一组const 定义,由于const为文件内部链接性,所以不会有重定义错误。
/*one.cpp*/ #include"one.h"
/*one.h*/ #ifndef _ONE_H__ #define _ONE_H__ const int x = 1; const int y = 2; #endif
#include"one.h" int main() { return 0; }
预处理后
/*one.i */ const int x = 1; const int y = 2;
/*main.i*/
const int x = 1; const int y = 2; int main() { return 0; }
你会觉得这样很不经济,如果这个头文件被包含100次,那岂不是在这100个源文件都定义了这组全局常量?而且全局常量的存活期又很长。当然是有解决办法的。那就是使用extern
改:
/*one.cpp*/ #include"one.h" extern const int x = 1; extern const int y = 2;
/*one.h*/ #ifndef _ONE_H__ #define _ONE_H__ extern const int x; extern const int y; #endif
#include"one.h" int main() { return 0; }
预处理后的文件
/*one.i*/ extern const int x; extern const int y; extern const int x = 1; extern const int y = 2;
/*main.i*/ extern const int x; extern const int y; int main() { return 0; }
但是:在C++中,如果const常量只 和 #define宏那样使用,是不会占用内存的,而是加入编译器的符号常量表中,这就是C++的const常量折叠折叠现象。但有些操作会迫使const存储在内存中,比如对const常量取地址等。因此将const常量直接放在头文件也是OK的,大多数情况也会这样做。
因此,如果两个不同的文件中声明同名的const ,不取它的地址,也不把它定义成extern,那么理想的C++编译器不会为他分配内存,而只是简单的把它折叠到代码中。--《C++编程思想第一卷》
全局函数
C++类 calss 和 结构体struct 中的成员函数受 OOP封装规则限定。这里只谈全局函数:在自定义名称空间中的,以及在全局(::)名称空间中的函数。
全局函数默认都是有外部链接性的。因此,在一个源文件中定义的函数,其他源文件只要有声明,就可以使用。
也可以将全局函数使用static 修饰,使其在定义它的源文件中私有化。此时它和使用static 修饰了的全局变量一样,会隐藏外部链接中同名的全局函数,也不会引起重名错误。
注意:全局inline函数默认是内部链接性,这也就是inline函数为什么可以整体定义放在头文件中的原因了。这点和宏函数一样,宏也是仅仅在一个源文件中有效的。
头文件中放什么?
1、类的定义
2、结构的定义
3、enum的定义
4、内敛函数的定义 和声明。内敛函数整体都放在头文件中。这样包含它的每个源文件才知道怎样展开。
5、函数的声明
6、模板
7、#define 宏 和 const 常量(C++不建议使用宏:宏常量使用const替代,宏函数使用inline函数替代)