C++允许甚至鼓励程序员将组件函数放在独立的文件中,甚至可以单独编译这些文件,然后将它们链接成可执行的程序
可以将原来的程序分成三部分
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:包含与结构有关的函数的代码。
- 源代码文件:包含调用与结构相关的函数的代码。
请不要将函数定义或变量声明放到头文件中,例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则将出错。下面列出了头文件中常包含的内容
- 函数原型
- 使用
#define
或const
定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数
查找头文件规则
- 如果文件名包含在尖括号
< >
中,则 C++编译器将在存储标准头文件的主机系统的文件系统中查找;
- 如果文件名包含在双引号
" "
中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用" "
而不是< >
头文件定义结构体定于与与结构体相关的函数原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #ifndef COORDIN_H_ #define COORDIN_H_
struct polar { double distance; double angle; };
struct rect { double x; double y; };
polar rect_to_polar(rect xypos); void show_polar(polar dapos);
#endif
|
在同一个文件中只能将同一个头文件包含一次。这个规则很容易在不知情的情况下将头文件包含多次,所以使用 #ifndef
,这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
1 2 3 4 5
| #ifndef XXXX #define XXXX
#endif
|
为什么上面使用 COORDIN_H_
这么奇怪的名字?
根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称
file1.cpp
调用与结构相关的函数的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream> #include <cmath> #include "coordin.h" using namespace std;
polar rect_to_polar(rect xypos) { polar result; result.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y); return result; }
void show_polar(polar dapos) { const double RAD_TO_DEG = 57.295; cout << "show:" << dapos.angle * RAD_TO_DEG << endl; }
|
file2.cpp
定义与结构体相关的函数定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <iostream> #include "coordin.h" using namespace std;
int main() { rect rplace; polar pplace; rplace.x = 10.1; rplace.y = 20.2; pplace = rect_to_polar(rplace); show_polar(pplace); return 0; }
|
查看编译过程
1 2 3 4 5
| #make clean && make rm -rf build/*.o demo g++ -Wall -Werror -std=c++11 -c src/file1.cpp -o build/file1.o g++ -Wall -Werror -std=c++11 -c src/file2.cpp -o build/file2.o g++ build/file1.o build/file2.o -o demo
|
当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈 顶指针被重置为函数被调用前的值,从而释放新变量使用的内存
被调用的函数根据其形参描述来确定每个参数的地 址。
- 函数
fib( )
被调用时,传递一个2字节的int和一个 4字节的long。函数地址 0x1089
- 将名称
real
和tell
同参数关联起来,函数调用将其参数的值放在栈顶,然后重新设置栈顶指针
- 当
fib( )
结束时,栈顶指针重新指向以前的位置。新值没有被删除,但不再被标记,它们所占据的空间将被下一个将值加入到栈中的函数调用所使用
C++也为静态存储持续性变量提供了3种链接性
1 2 3 4 5 6 7 8 9 10 11 12
| ... int global = 1000; static int one_file = 50; int main() { ... } void funct1(int n) { static int count = 0; int llama = 0; }
|
- 外部链接性(可在其他文件中访问)
- 内部链接性(只能在当前文件中访问)
- 无链接性(只能在当前函数或代码块中访问)
编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地 初始化静态变量,编译器将把它设置为0
零初始化和常量表达式初始化被统称为静态初始化,在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。
C++提供了两种变量声明。
- 一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;
- 另一种是引用声(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。
引用声明使用关键字extern
,且不进行初始化;否则,声明为定义,分配存储空间
1 2 3
| double up; extren int blem; extern char gr = 'z';
|
cv 限定符 代表 const
和 volatile
const
表明内存被初始化后,程序便不 能再对它进行修改
volatile
表明即使程序代码没有对内存单元进行修改,其值也可能发生变化
mutable
即使结构(或类)变量为 const,其某个成员也可以被修改
1 2 3 4 5 6 7 8 9
| struct data { char name[30]; mutable int accesses; };
const data veep = {"hello", 12}; strcpy(veep.name, "hello world"); veep.accesses++;
|
全局const
定义就像使用了static
说明符一样,下面两者的内存存储是一样的
1 2
| const int fingers = 10; static const int fingers = 10;
|
C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,在整个程序执行期间都是一直存在的。函数的链接性为外部的,即可以在文件间共享。
- 可以使用关键字
extern
来指定函数在另一个文件中定义的(可选)
- 可以使用
static
将函数的连接性设置为内部的,使之只能在一个文件中使用
命名空间
第一个文件 namesp.h
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
| #include <string> using namespace std;
namespace pers { struct Person { string fname; string lname; };
void getPerson(Person &); void showPerson(const Person &); };
namespace debts { using namespace pers; struct Debt { Person name; double amount; };
void getDebt(Debt &); void showDebt(const Debt &); double sumDebts(const Debt ar[], int n); };
|
第二个文件 namesp.cpp
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
| #include <iostream> #include "namesp.h" using namespace std;
namespace pers { void getPerson(Person & rp) { rp.fname = "hello"; rp.lname = "world"; }
void showPerson(const Person &rp) { cout << rp.fname << ", " << rp.lname << endl; } };
namespace debts { void getDebt(Debt & rd) { getPerson(rd.name); cout << rd.amount << endl; }
void showDebt(const Debt & rd) { showPerson(rd.name); cout << rd.name.fname << rd.amount << endl; }
double sumDebts(const Debt ar[], int n) { double total = 0; for(int i = 0; i < n; i++) total += ar[i].amount; return total; } };
|
第三个文件 namespp.cpp
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 <iostream> #include "namesp.h"
void other(void); void another(void);
int main(void) { using debts::Debt; using debts::showDebt; Debt golf = {{"hello", "world"}, 120.0}; showDebt(golf); other(); another(); return 0; }
void other(void) { using namespace debts; Person dg = {"Doodles", "Glister"}; showPerson(dg); cout << endl;
Debt zippy[3]; for(int i = 0; i < 3; i++) getDebt(zippy[i]); for(int i = 0; i < 3; i++) showDebt(zippy[i]); cout << sumDebts(zippy, 3) << endl; }
void another(void) { using pers::Person; Person collector = {"Milo", "RightShift"}; pers::showPerson(collector); cout << endl; }
|
命名空间建议
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
- 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件
math.h
是与C语言兼容的, 没有使用名称空间,但C头文件cmath
应将各种数学库函数放在名称空间std中
- 不要在头文件中使用using编译指令。
- 这样做掩盖了要让哪些名称可用;
- 包含头文件的顺序可能影响程序的行为。如果非 要使用编译指令using,应将其放在所有预处理器编译指令
#include
之后。
- 导入名称时,首选使用作用域解析运算符或
using
声明的方法
- 对于
using
声明,首选将其作用域设置为局部而不是全局
参考链接
- 《C++ Primer Plus》