本章导读
前两章我们一直在「一个文件里写所有代码」。本章把视野拉大到多文件大型程序:数据在内存里活多久(存储持续性)、谁能看见它(作用域)、哪些文件能共享它(链接性),以及用名称空间来避免大型项目里的命名冲突。理解本章,你才算真正理解「程序在内存中的样子」。
9.1 分离式编译:把程序拆成多个文件
C++ 允许(甚至鼓励)把程序的各个函数放在不同的源文件中单独编译,最后再链接成可执行程序。这样改一个文件只需重新编译那一个文件,大型项目管理起来轻松得多。
但直接把第 7 章的坐标转换程序「剪成两半」会出问题:main() 和另两个函数共用同一份结构声明,如果在两个文件里各抄一份,以后改一处忘另一处,程序就悄悄错了。解决方案是第 4 章见过的老朋友——#include 头文件。于是程序被拆成三部分:
- 头文件:结构声明 + 函数原型;
- 源文件一:与结构相关的函数定义;
- 源文件二:调用这些函数的 main()。
这就像一份「接口契约」:头文件告诉所有文件「有哪些类型、有哪些函数」,实现和使用各自独立。
头文件里不该放什么
不要把函数定义或变量声明放进头文件。若头文件里有一个函数定义,又被两个源文件包含,程序里就出现两份定义——链接错误(inline 函数除外)。头文件常放的是:函数原型、
#define/const常量、结构与类声明、模板声明、inline 函数。另外,在 IDE 里不要把头文件加进项目,也不要用#include包含源文件。
包含自己的头文件时要用双引号而不是尖括号:"coordin.h" 让编译器先在当前目录找,找不到才去标准头文件目录;<coordin.h> 则直接去标准位置找。
防止头文件被重复包含:头文件可能嵌套包含,同一份内容可能被包含多次。标准做法是用预处理器的 #ifndef(if not defined)「守卫」——第一次包含时宏未定义,处理全部内容并 #define 该宏;第二次包含时宏已定义,直接跳到 #endif。它不是阻止包含,而是让后面的包含内容作废。
Listing 9.1、9.2、9.3 把第 7 章的坐标转换程序拆成了三个文件。
Listing 9.1 coordin.h
// coordin.h -- 结构模板与函数原型
#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);
#endifListing 9.2 file1.cpp
// file1.cpp -- 三文件程序示例
#include <iostream>
#include "coordin.h" // 结构模板、函数原型
using namespace std;
int main()
{
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) // 巧妙地用 cin 做循环条件
{
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Bye!\n";
return 0;
}Listing 9.3 file2.cpp
// file2.cpp -- 包含 file1.cpp 调用的函数
#include <iostream>
#include <cmath>
#include "coordin.h"
// 直角坐标转极坐标
polar rect_to_polar(rect xypos)
{
using namespace std;
polar answer;
answer.distance =
sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer;
}
// 显示极坐标,角度转为度
void show_polar(polar dapos)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle * Rad_to_deg;
cout << " degrees\n";
}编译链接后的运行示例:
Enter the x and y values: 120 80
distance = 144.222, angle = 33.6901 degrees
Next two numbers (q to quit): q
▲ 图 9.1 在 Unix 系统上编译多文件 C++ 程序的流程:一条命令编译两个源文件,预处理器把头文件内容并入源码,再分别编译、链接。
最后提醒一句:不同编译器对函数名的修饰(name mangling)规则不同,用 A 编译器编译的目标文件很可能无法和 B 编译器的目标文件链接——链接时务必保证所有目标文件来自同一编译器。
9.2 存储持续性、作用域与链接性
C++ 用四种方案(C++11 起是四种加线程)决定数据在内存里活多久:
- 自动存储持续性——函数(含形参)内声明的变量,进入其所在函数/块时创建,离开时释放;
- 静态存储持续性——在函数外定义或用
static定义的变量,整个程序运行期间都存在; - 线程存储持续性(C++11)——用
thread_local声明,生命周期与所属线程相同; - 动态存储持续性——
new分配的内存,直到delete或程序结束才释放,俗称自由存储区/堆。
另外两个概念要分清:
- 作用域(scope):名字在文件内多大的范围可见。局部(块)作用域只在
{}内可见;全局(文件)作用域从定义点起整个文件可见。函数原型中的作用域只限于参数列表括号内。 - 链接性(linkage):名字能否跨文件共享。外部链接性可跨文件共享;内部链接性仅限本文件;自动变量没有链接性。
自动变量与栈
自动变量默认是「进块创建、出块销毁」的。块可以嵌套:内层块声明的同名变量会**隐藏(hide)**外层变量,出块后外层变量恢复可见。

▲ 图 9.2 块与作用域:内层块的 websight 只在内层可见,内层的 teledeli 隐藏了外层的同名变量,出块后外层版本恢复。
Listing 9.4 autoscp.cpp(自动变量的作用域演示,输出显示三个 texas 各有各的地址,互不干扰)
// autoscp.cpp -- 自动变量的作用域
#include <iostream>
void oil(int x);
int main()
{
using namespace std;
int texas = 31;
int year = 2011;
cout << "In main(), texas = " << texas << ", &texas = "
<< &texas << endl;
oil(texas);
cout << "In main(), texas = " << texas << ", &texas = "
<< &texas << endl; // 值与地址都没变
return 0;
}
void oil(int x)
{
using namespace std;
int texas = 5; // 隐藏 main() 里的 texas
cout << "In oil(), texas = " << texas
<< ", &texas = " << &texas << endl;
{
int texas = 113; // 再隐藏一层
cout << "In block, texas = " << texas
<< ", &texas = " << &texas << endl;
cout << "In block, x = " << x << endl; // 用的是形参 x
} // 这个 texas 出块即销毁
cout << "Post-block texas = " << texas << endl; // 回到 5
}编译器如何管理这些「涨了又缩」的变量?答案是栈:一块按后进先出(LIFO)管理的内存。函数被调用时,它的自动变量和实参被「压」到栈顶;函数结束时,栈顶指针拨回原位,这些变量占的空间就被下一个函数复用。就像食堂里摞盘子:最后放上去的最先被取走。

▲ 图 9.3 用栈传递参数:fib() 被调用时,int 和 long 实参依次压栈,fib() 结束后栈顶指针复位,旧值虽未被擦除但已「无主」,会被下次压栈覆盖。
顺带一提:C++11 把 register 关键字的「寄存器提示」含义废弃了,现在它只是显式声明「这是自动变量」;旧的 auto(自动存储说明符)用法在 C++11 中也失效——auto 已让位给类型推导(第 3、7、8 章)。
9.3 静态持续变量与单定义规则
静态持续变量有三种链接性,都活到程序结束,且未初始化时所有位被置 0(零初始化):
int global = 1000; // 静态持续,外部链接性(函数外定义)
static int one_file = 50; // 静态持续,内部链接性(函数外 + static)
void funct1(int n)
{
static int count = 0; // 静态持续,无链接性(块内 + static)
int llama = 0; // 自动变量
}注意 static 的「一词两义」:用在块内声明的是存储持续性(局部作用域但活到程序结束);用在函数外声明的是链接性(内部链接性)。编译器为所有静态变量分配一块固定内存,不需要栈来管理。
| 存储方式 | 持续性 | 作用域 | 链接性 | 声明方式 |
|---|---|---|---|---|
| 自动 | 自动 | 块 | 无 | 块内 |
| 寄存器 | 自动 | 块 | 无 | 块内,加 register |
| 静态,无链接性 | 静态 | 块 | 无 | 块内,加 static |
| 静态,外部链接性 | 静态 | 文件 | 外部 | 所有函数外 |
| 静态,内部链接性 | 静态 | 文件 | 内部 | 所有函数外,加 static |
静态变量的初始化分三档:零初始化(全部置 0)→常量表达式初始化(编译器仅凭文件内容就能算出,如 int y = 5;、long z = 13 * 13;)→动态初始化(要等链接、执行时才算,如 const double pi = 4.0 * atan(1.0); 需要调用 atan())。前两档合称静态初始化。
单定义规则(ODR)与 extern
外部变量(external variable)在每个用到它的文件里都要声明,但整个程序中只能有一处定义——这就是单定义规则(One Definition Rule)。C++ 用两种声明来满足它:
- 定义声明(definition):分配存储,如
double up;; - 引用声明(declaration):用
extern且不初始化,表示「变量在别处已定义」,如extern int blem;。
double up; // 定义,up 被零初始化
extern int blem; // blem 在别处定义
extern char gr = 'z'; // 带初始化,所以仍是定义!extern 加了初始化就变成了定义
extern int cats = 20;因为带初始化,它是定义而不是引用声明。多文件共享一个变量时:只在一个文件里定义(extern 可省略),其余文件用不带初始化的extern int cats;来引用。若两个文件都定义了同名外部变量,链接器会报「重复定义」。

▲ 图 9.4 定义声明与引用声明:file01.cpp 里定义 cats 和 dogs,file02.cpp/file98.cpp 用 extern 引用它们;fleas 没被 extern 引用,其他文件就看不见它。
Listing 9.5 + 9.6 external.cpp / support.cpp(跨文件共享外部变量 + 作用域解析运算符)
// external.cpp -- 外部变量(与 support.cpp 一起编译)
#include <iostream>
using namespace std;
double warming = 0.3; // 定义外部变量
void update(double dt);
void local();
int main()
{
cout << "Global warming is " << warming << " degrees.\n";
update(0.1); // 修改全局变量
cout << "Global warming is " << warming << " degrees.\n";
local(); // 用了局部同名变量的函数
cout << "Global warming is " << warming << " degrees.\n";
return 0;
}// support.cpp -- 使用另一文件里的外部变量
#include <iostream>
extern double warming; // 引用声明:warming 在别处定义
void update(double dt)
{
extern double warming; // 可选的再次声明,起文档作用
warming += dt; // 使用全局 warming
std::cout << "Updating global warming to "
<< warming << " degrees.\n";
}
void local() // 局部变量隐藏全局变量
{
double warming = 0.8; // 新变量,遮住外部变量
std::cout << "Local warming = " << warming << " degrees.\n";
std::cout << "But global warming = " << ::warming
<< " degrees.\n";// 用 :: 访问全局版本
}输出:update() 把全局 warming 改成 0.4;local() 里同名局部变量让它显示 0.8,但 ::warming 仍能访问全局的 0.4——作用域解析运算符 :: 放在变量名前,明确表示「用全局版本」。
内部链接性与 static
想让两个文件用同名的不同变量怎么办?直接在两边都写 int errors = 20; 会违反单定义规则;正确做法是在其中一个文件加 static:
// file1
int errors = 20; // 外部链接性
// file2
static int errors = 5; // 内部链接性,仅 file2 可见
void froobish() { cout << errors; } // 用的是 file2 的 errorsstatic 使 file2 的 errors 只在本文件有效,不试图引入外部定义,因此不违反 ODR。
Listing 9.7 + 9.8 twofile1.cpp / twofile2.cpp(通过地址验证:tom 两文件共享,dick 和 harry 各是独立变量)
// twofile1.cpp -- 外部与内部链接性(与 twofile2.cpp 一起编译)
#include <iostream>
int tom = 3; // 外部变量定义
int dick = 30; // 外部变量定义
static int harry = 300; // static:内部链接性
void remote_access();
int main()
{
using namespace std;
cout << "main() reports the following addresses:\n";
cout << &tom << " = &tom, " << &dick << " = &dick, "
<< &harry << " = &harry\n";
remote_access();
return 0;
}// twofile2.cpp -- 内部与外部链接性
#include <iostream>
extern int tom; // tom 在别处定义
static int dick = 10; // 覆盖外部 dick,仅本文件可见
int harry = 200; // 外部定义,不与 twofile1 的 harry 冲突
void remote_access()
{
using namespace std;
cout << "remote_access() reports the following addresses:\n";
cout << &tom << " = &tom, " << &dick << " = &dick, "
<< &harry << " = &harry\n";
}运行结果显示:tom 的地址在两个文件中相同(共享),dick 和 harry 的地址不同(各是各的变量)。
静态局部变量
块内加 static 的变量虽只在块内可见,却在程序整个运行期存在——它的值在两次函数调用之间保持,且只在程序启动时初始化一次。
Listing 9.9 static.cpp(static 局部变量实现累计计数)
// static.cpp -- 使用静态局部变量
#include <iostream>
const int ArSize = 10;
void strcount(const char * str);
int main()
{
using namespace std;
char input[ArSize];
char next;
cout << "Enter a line:\n";
cin.get(input, ArSize);
while (cin)
{
cin.get(next);
while (next != '\n') // 一行没读完就丢弃余下字符
cin.get(next);
strcount(input);
cout << "Enter next line (empty line to quit):\n";
cin.get(input, ArSize);
}
cout << "Bye\n";
return 0;
}
void strcount(const char * str)
{
using namespace std;
static int total = 0; // 静态局部变量
int count = 0; // 自动局部变量
cout << "\"" << str << "\" contains ";
while (*str++) // 数到字符串末尾
count++;
total += count;
cout << count << " characters\n";
cout << total << " characters total\n";
}输出节选:每行报自己的字符数(count 每次重置为 0),而 total 一路累加(9 → 15 → 24 → 26)。这就是静态局部变量「记得住过去」的本事。
用 static 局部变量做「记忆」
需要函数记住调用历史(计数器、累计值、只初始化一次的查找表)时,块内 static 是最轻量的方案——不用全局变量,也不会污染其他文件。书上那句玩笑话很形象:静态变量就像「转世投胎还带着前世记忆」。