本章导读

前两章我们一直在「一个文件里写所有代码」。本章把视野拉大到多文件大型程序:数据在内存里活多久(存储持续性)、谁能看见它(作用域)、哪些文件能共享它(链接性),以及用名称空间来避免大型项目里的命名冲突。理解本章,你才算真正理解「程序在内存中的样子」。

9.1 分离式编译:把程序拆成多个文件

9.2 存储持续性、作用域与链接性

9.3 静态持续变量与单定义规则

9.4 说明符与 cv 限定符

9.5 函数与链接性

9.6 动态内存与 placement new

9.7 名称空间

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);
 
#endif

Listing 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 起是四种加线程)决定数据在内存里活多久:

  1. 自动存储持续性——函数(含形参)内声明的变量,进入其所在函数/块时创建,离开时释放;
  2. 静态存储持续性——在函数外定义或用 static 定义的变量,整个程序运行期间都存在;
  3. 线程存储持续性(C++11)——用 thread_local 声明,生命周期与所属线程相同;
  4. 动态存储持续性——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 的 errors

static 使 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 起不再是说明符)、registerstaticexternthread_local(C++11 新增,可与 static/extern 连用)、mutable。一条声明中至多使用一个(thread_local 除外)。

cv 限定符constvolatile

  • 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 背后的函数与可替换性

newnew[] 运算符底层调用两个分配函数,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 数组放进 buffer2

Listing 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),可以随时再打开它追加名字(比如先放原型,后面再放函数定义);除了用户定义的,还有一个全局名称空间,对应文件级声明区域——以前的「全局变量」现在归它管。

使用名称空间里的名字有三种方式,从精确到粗放:

  1. 作用域解析运算符Jack::pail = 12.34;Jack::fetch();(带前缀的叫限定名,不带的叫非限定名);
  2. using 声明using Jill::fetch;——只把一个名字引进当前声明区域,效果像在该位置声明了这个名字;若局部已有同名变量,会直接报错;
  3. 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: $420Rightshift, Milo。注意 debts 里已有 using namespace pers;,所以 other() 无需再引入 Person 就能直接用。

名称空间使用准则

  • 命名名称空间里的变量代替外部全局变量;
  • 未命名名称空间里的变量代替 static 全局变量;
  • 开发函数库或类库时,把它们放进名称空间(标准库就放在 std 里);
  • using namespace std; 这种指令只该当作老代码迁移的临时手段;
  • 头文件里不要写 using 指令——它会掩盖到底放行了哪些名字,还可能受包含顺序影响;要用也放在所有 include 之后;
  • 优先用作用域解析运算符或 using 声明来引入名字;引入位置优先选局部作用域。

一句话记住本章

变量的「一生」由三件事决定:活多久(存储持续性)、谁看得见(作用域)、哪些文件共享(链接性);new 给你一块不受作用域管辖的自由内存(placement new 还能指定位置);名称空间则是大型项目里给名字「分户口」的手段。写小程序时图省事用 using namespace std; 没什么大罪,写大项目时请改用 std:: 前缀或 using 声明。