本章导读
前两章我们一直在「一个文件里写所有代码」。本章把视野拉大到多文件大型程序:数据在内存里活多久(存储持续性)、谁能看见它(作用域)、哪些文件能共享它(链接性),以及用名称空间来避免大型项目里的命名冲突。理解本章,你才算真正理解「程序在内存中的样子」。
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 是最轻量的方案——不用全局变量,也不会污染其他文件。书上那句玩笑话很形象:静态变量就像「转世投胎还带着前世记忆」。
9.4 说明符与 cv 限定符
存储说明符有这些:auto(C++11 起不再是说明符)、register、static、extern、thread_local(C++11 新增,可与 static/extern 连用)、mutable。一条声明中至多使用一个(thread_local 除外)。
cv 限定符指 const 和 volatile:
volatile:告诉编译器「这个内存位置的值可能被程序之外的东西改变」(如硬件端口、另一个程序),别把它缓存在寄存器里做优化,每次都要老老实实读内存。mutable:即使结构/类变量是 const,被 mutable 标记的成员仍可修改:
struct data
{
char name[30];
mutable int accesses; // 即使对象是 const 也能改
};
const data veep = {"Claybourne Clodde", 0};
// strcpy(veep.name, "Joye Joux"); // 不允许
veep.accesses++; // 允许const 全局变量:默认内部链接性
C++(C 语言没有这条规则)里,const 全局变量默认是内部链接性的,相当于隐式加了 static。这是刻意设计:你可以放心地把常量定义放进头文件,被多个文件包含也不违反单定义规则——每个文件得到自己私有的一份常量,名字还互不冲突。所以「把常量放头文件」是好习惯。
若真想让 const 常量跨文件共享(只存一份),用 extern const int states = 50; 覆盖默认,之后所有使用它的文件都要用 extern const int states; 声明,且只有定义它的文件能初始化。
别滥用全局变量
全局变量「所有函数都能访问」看起来方便,代价是数据完整性失控——任何函数都可能改坏它。经验法则:优先用局部变量,按需传参。全局变量的正当用途主要是常量数据(如月份名数组),配合 const 保护起来:
const char * const months[12] =
{
"January", "February", "March", "April", "May",
"June", "July", "August", "September", "October",
"November", "December"
};第一个 const 保护字符串本身,第二个 const 让每个指针始终指向原来的字符串。
9.5 函数与链接性
函数和变量一样有链接性。C++ 不允许函数嵌套定义,所以所有函数都自动是静态持续性。默认情况下函数有外部链接性,可跨文件共享;在原型和定义前都加上 static,则函数只在本文件可见——不同文件还能用同名函数互不打架:
static int private(double x); // 原型
static int private(double x) // 定义
{ /* ... */ }单定义规则也适用于非 inline 函数:每个非 inline 函数在整个程序中只能有一个定义(可以放在库里),但每个使用它的文件都应有其原型。inline 函数例外——正因如此,inline 定义可以放进头文件,但要求各文件中的定义完全一致。
语言链接性(language linking):链接器要求每个函数有唯一的符号名。C 里同名函数只能有一个,名字简单加前缀(如 spiff → _spiff);C++ 支持重载,靠**名称修饰(name mangling)**生成不同符号(如 spiff(int) → _spiff_i,spiff(double, double) → _spiff_d_d)。想在 C++ 程序里调用 C 库的预编译函数,得告诉链接器按 C 协议查找:
extern "C" void spiff(int); // 按 C 语言链接性查找
extern void spoff(int); // 默认 C++ 协议
extern "C++" void spaff(int); // 显式 C++ 协议9.6 动态内存与 placement new
new/delete 管理的动态内存不受作用域和链接性规则约束:一个函数分配,另一个函数释放都行。编译器通常分三块内存:静态变量区、自动变量区(栈)、动态存储区(堆)。要注意:float * p_fees = new float[20]; 中 80 字节内存活得比指针久——出块后指针没了,内存还在;想跨函数用这 80 字节,得把地址传出去或返回。
new 的初始化
- C++98:内置标量类型可用圆括号初始化——
int *pi = new int(6);、double *pd = new double(99.99);; - C++11:结构和数组可用列表初始化(花括号)——
int *ar = new int[4] {2,4,6,7};、单值也能用new int {}。
new 找不到足够内存时,如今的行为是抛出 std::bad_alloc 异常(古早版本返回空指针)。
new 背后的函数与可替换性
new 和 new[] 运算符底层调用两个分配函数,delete 调用对应的释放函数:
void * operator new(std::size_t); // new 使用
void * operator new[](std::size_t); // new[] 使用int *pi = new int; 会被翻译成类似 new(sizeof(int)) 的调用。有趣的是,这些函数是可替换的——你可以提供自己的版本,为特定类定制分配策略。
placement new:指定位置的 new
普通 new 负责去堆里找一块够大的内存;placement new 让你指定要用的位置——用于自建内存管理机制、对接特定硬件地址、在特定位置构造对象。用法:包含 <new> 头文件,在 new 后面加一个地址参数:
#include <new>
p2 = new (buffer1) chaff; // 把结构放进 buffer1
p4 = new (buffer2) int[20]; // 把 int 数组放进 buffer2Listing 9.10 newplace.cpp(普通 new 与 placement new 的对比)
// newplace.cpp -- 使用 placement new
#include <iostream>
#include <new> // placement new 需要
const int BUF = 512;
const int N = 5;
char buffer[BUF]; // 一大块静态内存
int main()
{
using namespace std;
double *pd1, *pd2;
int i;
cout << "Calling new and placement new:\n";
pd1 = new double[N]; // 用堆
pd2 = new (buffer) double[N]; // 用 buffer 数组
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout << "Memory addresses:\n" << " heap: " << pd1
<< " static: " << (void *) buffer << endl;
for (i = 0; i < N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
cout << "\nCalling new and placement new a second time:\n";
double *pd3, *pd4;
pd3 = new double[N]; // 找新地址
pd4 = new (buffer) double[N]; // 覆盖旧数据
for (i = 0; i < N; i++)
pd4[i] = pd3[i] = 1000 + 40.0 * i;
cout << pd3[0] << " at " << &pd3[0] << "; "
<< pd4[0] << " at " << &pd4[0] << endl;
cout << "\nCalling new and placement new a third time:\n";
delete [] pd1;
pd1 = new double[N];
pd2 = new (buffer + N * sizeof(double)) double[N]; // 偏移 40 字节
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 60.0 * i;
cout << pd1[0] << " at " << &pd1[0] << "; "
<< pd2[0] << " at " << &pd2[0] << endl;
delete [] pd1;
delete [] pd3;
return 0;
}输出(节选)表明三点:placement new 确实把数组放进了 buffer(地址与 buffer 相同);第二次调用普通 new 得到了新地址,而 placement new 原封不动地又用了一遍 buffer 开头,把旧数据覆盖了;第三次给 buffer 加上偏移 buffer + N * sizeof(double) 才用到了新位置。
placement new 的内存不能 delete
delete 只能用于普通 new 分配的堆内存。
delete [] pd2;(pd2 指向静态数组 buffer)会导致运行时错误。placement new 只使用你给的地址,不记录哪些位置用过、也不搜索空闲内存——内存管理的担子转到了程序员肩上,你得自己规划偏移、自己决定何时覆盖。
另外,默认的 placement new 函数基本什么都不干,只是把你传的地址转成 void * 返回;它不可替换,但可以重载(第一个参数必须是 std::size_t)。
9.7 名称空间
程序一大、用的库一多,名字冲突就来了:两个库都定义了 List、Tree、Node 类怎么办?C++ 用名称空间让你划出自定义的「名字地盘」,地盘之间互不冲突。
先明确三个术语:
- 声明区域(declarative region):可以进行声明的区域。全局变量的声明区域是整个文件,函数内变量的声明区域是所在块。
- 潜在作用域(potential scope):从声明点开始到声明区域结束。
- 作用域(scope):变量实际可见的范围——潜在作用域可能被嵌套区域里的同名变量遮住一部分。
创建与使用名称空间
namespace Jack {
double pail; // 变量声明
void fetch(); // 函数原型
int pal;
struct Well { /* ... */ };
}
namespace Jill {
double bucket(double n) { /* ... */ }
double fetch; // 与 Jack::fetch 互不冲突
int pal;
struct Hill { /* ... */ };
}
▲ 图 9.5 声明区域:全局变量、函数内变量、名称空间各自的声明区域。

▲ 图 9.6 潜在作用域与作用域:全局变量 x 的潜在作用域被函数内同名局部变量截断,实际作用域只剩两头。
要点:名称空间只能放在全局层级或嵌套在其他名称空间里,不能放进代码块;名称空间是开放的(open),可以随时再打开它追加名字(比如先放原型,后面再放函数定义);除了用户定义的,还有一个全局名称空间,对应文件级声明区域——以前的「全局变量」现在归它管。
使用名称空间里的名字有三种方式,从精确到粗放:
- 作用域解析运算符:
Jack::pail = 12.34;、Jack::fetch();(带前缀的叫限定名,不带的叫非限定名); - using 声明:
using Jill::fetch;——只把一个名字引进当前声明区域,效果像在该位置声明了这个名字;若局部已有同名变量,会直接报错; - using 指令:
using namespace Jill;——把所有名字一次性放行。
using 声明与 using 指令的冲突行为不同
假设名称空间和当前区域都有同名 fetch:用 using 声明引入 → 两个名字冲突,编译错误;用 using 指令引入 → 局部版本静默遮住名称空间版本,不报任何错。所以 using 声明更安全——它把冲突摆在你面前,而指令可能让你蒙在鼓里。另外函数内使用 using 指令,名称空间名字并不会因此对本文件其他函数可见。
using 指令还有一个传递性:using namespace myth; 而 myth 里又 using namespace elements;,那么这一条指令同时放行了 myth 和 elements 的全部名字——相当于两条指令。
嵌套、别名与未命名名称空间
名称空间可以嵌套:elements::fire::flame;也可以给长名字起别名:
namespace mvft = my_very_favorite_things;
namespace MEF = myth::elements::fire; // 简化嵌套
using MEF::flame;省略名称空间的名字就得到未命名名称空间——它后面像自动跟着一条 using 指令,名字在本文件可见,但无法在其他文件中访问。因此它是「内部链接性静态变量」的现代替代品:
namespace // 未命名名称空间
{
int ice; // 相当于 static int ice;
int bandycoot;
}三文件名称空间示例
Listing 9.11 namesp.h(头文件里放两个名称空间:pers 管人员结构,debts 借用 pers 管债务)
// namesp.h
#include <string>
namespace pers
{
struct Person
{
std::string fname;
std::string lname;
};
void getPerson(Person &);
void showPerson(const Person &);
}
namespace debts
{
using namespace pers; // debts 借用 pers 的名字
struct Debt
{
Person name;
double amount;
};
void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
}Listing 9.12 namesp.cpp(利用名称空间的开放性,把函数定义追加进各自名称空间)
// namesp.cpp -- namesp.h 的实现
#include <iostream>
#include "namesp.h"
namespace pers
{
using std::cout;
using std::cin;
void getPerson(Person & rp)
{
cout << "Enter first name: ";
cin >> rp.fname;
cout << "Enter last name: ";
cin >> rp.lname;
}
void showPerson(const Person & rp)
{
std::cout << rp.lname << ", " << rp.fname;
}
}
namespace debts
{
void getDebt(Debt & rd)
{
getPerson(rd.name);
std::cout << "Enter debt: ";
std::cin >> rd.amount;
}
void showDebt(const Debt & rd)
{
showPerson(rd.name);
std::cout << ": $" << rd.amount << std::endl;
}
double sumDebts(const Debt ar[], int n)
{
double total = 0;
for (int i = 0; i < n; i++)
total += ar[i].amount;
return total;
}
}Listing 9.13 usenmsp.cpp(三种引入方式的实战演示)
// usenmsp.cpp -- 使用名称空间
#include <iostream>
#include "namesp.h"
void other(void);
void another(void);
int main(void)
{
using debts::Debt; // using 声明:只引入需要的
using debts::showDebt;
Debt golf = { {"Benny", "Goatsniff"}, 120.0 };
showDebt(golf);
other();
another();
return 0;
}
void other(void)
{
using std::cout;
using std::endl;
using namespace debts; // using 指令:全部引入
Person dg = {"Doodles", "Glister"}; // pers 名字也能用
showPerson(dg);
cout << endl;
Debt zippy[3];
int i;
for (i = 0; i < 3; i++)
getDebt(zippy[i]);
for (i = 0; i < 3; i++)
showDebt(zippy[i]);
cout << "Total debt: $" << sumDebts(zippy, 3) << endl;
}
void another(void) // 第三种:using 声明 + 限定名
{
using pers::Person;
Person collector = { "Milo", "Rightshift" };
pers::showPerson(collector);
std::cout << std::endl;
}运行节选:Goatsniff, Benny: $120 → 输入三笔债务后显示各自金额与 Total debt: $420 → Rightshift, Milo。注意 debts 里已有 using namespace pers;,所以 other() 无需再引入 Person 就能直接用。
名称空间使用准则
- 用命名名称空间里的变量代替外部全局变量;
- 用未命名名称空间里的变量代替 static 全局变量;
- 开发函数库或类库时,把它们放进名称空间(标准库就放在 std 里);
using namespace std;这种指令只该当作老代码迁移的临时手段;- 头文件里不要写 using 指令——它会掩盖到底放行了哪些名字,还可能受包含顺序影响;要用也放在所有 include 之后;
- 优先用作用域解析运算符或 using 声明来引入名字;引入位置优先选局部作用域。
一句话记住本章
变量的「一生」由三件事决定:活多久(存储持续性)、谁看得见(作用域)、哪些文件共享(链接性);new 给你一块不受作用域管辖的自由内存(placement new 还能指定位置);名称空间则是大型项目里给名字「分户口」的手段。写小程序时图省事用
using namespace std;没什么大罪,写大项目时请改用 std:: 前缀或 using 声明。
自测:homer 是函数的形参,secret 要被两个文件共享,topsecret 只想在一个文件内的函数间共享,beencalled 要记录所在函数被调用的次数——各该用什么存储方案?
- homer:自动存储(形参默认自动变量,块作用域、无链接性)。
- secret:静态持续 + 外部链接性——在一个文件里定义
int secret = ...;,其他文件用extern int secret;引用。- topsecret:静态持续 + 内部链接性——在函数外定义
static int topsecret;,或放进未命名名称空间。- beencalled:静态持续 + 无链接性——函数内
static int beencalled = 0;,每次调用beencalled++;。
自测:using 声明和 using 指令有什么区别?
using 声明(
using Jill::fetch;)只引入一个名字,效果如同在当前位置声明了它;若当前区域已有同名名字,编译报错——冲突被摆在明面上。using 指令(using namespace Jill;)放行名称空间的全部名字,名字解析发生在同时包含指令与名称空间的最小声明区域中;若与局部同名,局部版本静默遮住名称空间版本,不报错。所以 using 声明更安全,指令省事但容易埋雷。
自测:为什么 C++ 把 const 全局变量默认设为内部链接性?想让它跨文件共享该怎么办?
为了让你能放心把常量定义放进头文件:多个文件包含同一头文件时,每个文件各自得到一份私有常量,互不冲突,不违反单定义规则(普通全局变量这样做会重复定义)。若真要跨文件共享同一份 const:在定义处写
extern const int states = 50;覆盖默认链接性,之后所有使用它的文件都用extern const int states;声明,且只有定义处能初始化。
自测:placement new 和普通 new 有哪三点不同?
① 位置:普通 new 自己去堆里找内存;placement new 用你指定的地址(如静态数组 buffer)。② 重复分配:普通 new 每次给新地址;placement new 只管用传入的地址,不记是否用过、不找空闲区,重复调用会覆盖旧数据,偏移要自己算。③ 释放:普通 new 的内存用 delete[] 释放;placement new 的内存不能 delete(delete 只对堆内存有效),用完的自然消亡或由你管理缓冲区。
自测:static 用在块内和用在函数外,含义有何不同?
这是 static 的「一词两义」。块内:改变的是存储持续性——变量仍只在块内可见(局部作用域、无链接性),但活到程序结束,值在多次调用间保留(如 Listing 9.9 的 total)。函数外:改变的是链接性——变量本就是静态持续,加 static 使它从外部链接性变为内部链接性,仅本文件可见,可与别的文件的同名外部变量共存而不违反单定义规则。