C++ Primer Plus 第9章:内存模型和名称空间

9.1 单独编译

9.1.1 为什么需要单独编译?

大型程序通常分成多个文件,每个文件单独编译,最后链接在一起。这样做的好处:

  • 修改一个文件,只需重新编译该文件,不影响其他文件

  • 团队协作:不同成员负责不同模块

  • 代码复用:公共功能放在独立文件中

    典型的多文件项目结构:
    ┌─────────────────────────────────────────┐
    │ coordin.h(头文件) │
    │ - 结构体定义 │
    │ - 函数原型 │
    └──────────┬──────────────────────────────┘
    │ #include
    ┌──────┴──────┐
    ▼ ▼
    coordin.cpp main.cpp
    (函数实现) (主程序)
    │ │
    └──────┬──────┘

    链接器


    可执行程序


9.1.2 头文件的内容规范

cpp 复制代码
// coordin.h -- 头文件示例
// 头文件保护(防止重复包含)
#ifndef COORDIN_H_
#define COORDIN_H_

// ✅ 头文件中应该包含:
// 1. 函数原型
// 2. 使用 #define 或 const 定义的符号常量
// 3. 结构体声明
// 4. 类声明
// 5. 模板声明
// 6. 内联函数

struct Polar {
    double distance;   // 距离
    double angle;      // 角度(弧度)
};

struct Rect {
    double x;   // 横坐标
    double y;   // 纵坐标
};

// 函数原型
Polar rectToPolar(Rect xypos);
void  showPolar(Polar dapos);

#endif  // COORDIN_H_
cpp 复制代码
// coordin.cpp -- 函数实现文件
#include <iostream>
#include <cmath>
#include "coordin.h"   // 包含自定义头文件(用引号)

Polar rectToPolar(Rect xypos)
{
    Polar answer;
    answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
    answer.angle    = atan2(xypos.y, xypos.x);
    return answer;
}

void showPolar(Polar dapos)
{
    using namespace std;
    const double Rad_to_deg = 57.29577951;
    cout << "距离 = " << dapos.distance << endl;
    cout << "角度 = " << dapos.angle * Rad_to_deg << " 度" << endl;
}
cpp 复制代码
// main.cpp -- 主程序文件
#include <iostream>
#include "coordin.h"

int main()
{
    using namespace std;

    Rect rplace;
    Polar pplace;

    cout << "请输入直角坐标 x 和 y:" << endl;
    while (cin >> rplace.x >> rplace.y)
    {
        pplace = rectToPolar(rplace);
        showPolar(pplace);
        cout << "继续输入(或 Ctrl+Z 退出):" << endl;
    }
    return 0;
}

9.1.3 头文件保护

cpp 复制代码
// 方式1:传统的 #ifndef 保护(最通用)
#ifndef MYHEADER_H_
#define MYHEADER_H_

// 头文件内容...

#endif

// 方式2:#pragma once(现代编译器支持,更简洁)
#pragma once

// 头文件内容...

⚠️ 为什么需要头文件保护?

如果 main.cpp 同时包含了 a.hb.h,而 b.h 也包含了 a.h

没有保护的话 a.h 的内容会被包含两次,导致重复定义错误。


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

9.2.1 四种存储持续性

存储类型 持续时间 存储位置 示例
自动存储 代码块执行期间 栈(Stack) 局部变量、函数参数
静态存储 程序整个运行期间 静态区 全局变量、static 变量
线程存储 线程存在期间 线程局部存储 thread_local 变量(C++11)
动态存储 手动控制 堆(Heap) new 分配的内存

9.2.2 自动存储持续性

cpp 复制代码
// auto_storage.cpp -- 自动存储示例
#include <iostream>

void func()
{
    int local = 10;   // 自动存储:函数调用时创建,返回时销毁
    std::cout << "local = " << local << std::endl;
    local++;
}   // local 在这里销毁

int main()
{
    func();   // local = 10
    func();   // local = 10(每次调用都重新创建)
    func();   // local = 10

    // 代码块作用域
    {
        int block_var = 100;   // 只在这个代码块内有效
        std::cout << "block_var = " << block_var << std::endl;
    }
    // block_var 在这里已经销毁
    // std::cout << block_var;   // ❌ 错误:block_var不在作用域内

    return 0;
}

9.2.3 静态存储持续性

静态变量在程序启动时创建,程序结束时销毁,只初始化一次

cpp 复制代码
// static_storage.cpp -- 静态存储示例
#include <iostream>

// 1. 静态外部变量(全局变量):整个程序可见
int globalCount = 0;

// 2. 静态内部变量(static全局):只在本文件可见
static int fileCount = 0;

void counter()
{
    // 3. 静态局部变量:只在函数内可见,但持续整个程序
    static int callCount = 0;   // 只初始化一次!
    int localCount = 0;         // 每次调用都重新初始化

    callCount++;
    localCount++;
    globalCount++;

    std::cout << "调用次数(static):" << callCount
              << " 局部计数(auto):" << localCount
              << " 全局计数:" << globalCount << std::endl;
}

int main()
{
    counter();   // 调用次数:1  局部计数:1  全局计数:1
    counter();   // 调用次数:2  局部计数:1  全局计数:2
    counter();   // 调用次数:3  局部计数:1  全局计数:3
    return 0;
}

输出:

复制代码
调用次数(static):1 局部计数(auto):1 全局计数:1
调用次数(static):2 局部计数(auto):1 全局计数:2
调用次数(static):3 局部计数(auto):1 全局计数:3

💡 静态局部变量的典型应用:计数器、单例模式、缓存等需要"记住上次状态"的场景。


9.2.4 作用域与链接性

复制代码
作用域(Scope):变量在哪里可以被访问
链接性(Linkage):变量在多个文件间的可见性

┌─────────────────────────────────────────────┐
│ 链接性类型                                   │
├──────────────┬──────────────────────────────┤
│ 外部链接性   │ 可在多个文件中访问(全局变量)  │
│ 内部链接性   │ 只在本文件中访问(static全局)  │
│ 无链接性     │ 只在代码块中访问(局部变量)    │
└──────────────┴──────────────────────────────┘
cpp 复制代码
// file1.cpp -- 演示链接性
#include <iostream>

// 外部链接性:其他文件可以用 extern 访问
int sharedVar = 100;

// 内部链接性:只有本文件可以访问
static int privateVar = 200;

// 外部链接性的函数
void showShared()
{
    std::cout << "sharedVar = " << sharedVar << std::endl;
}
cpp 复制代码
// file2.cpp -- 访问其他文件的变量
// extern 声明:告诉编译器变量在其他文件中定义
extern int sharedVar;   // 声明,不是定义

// extern int privateVar;  // ❌ 错误:privateVar是内部链接性

void modifyShared()
{
    sharedVar = 999;   // 可以访问和修改
}

9.2.5 变量的作用域规则

cpp 复制代码
// scope_rules.cpp -- 作用域规则示例
#include <iostream>

int x = 1;   // 全局变量

void func()
{
    int x = 2;   // 局部变量,遮蔽全局变量
    std::cout << "func内 x = " << x << std::endl;   // 2

    {
        int x = 3;   // 更内层的局部变量
        std::cout << "内层块 x = " << x << std::endl;   // 3
        std::cout << "全局 x = " << ::x << std::endl;   // 1(::访问全局)
    }

    std::cout << "func内 x = " << x << std::endl;   // 2(内层x已销毁)
}

int main()
{
    std::cout << "main前 x = " << x << std::endl;   // 1
    func();
    std::cout << "main后 x = " << x << std::endl;   // 1(全局未改变)

    int x = 10;   // 局部变量遮蔽全局
    std::cout << "局部 x = " << x << std::endl;     // 10
    std::cout << "全局 x = " << ::x << std::endl;   // 1

    return 0;
}

💡 作用域解析运算符 ::::变量名 可以访问被遮蔽的全局变量。


9.3 说明符和限定符

9.3.1 存储说明符

cpp 复制代码
// storage_specifiers.cpp -- 存储说明符示例
#include <iostream>

// auto(C++11前):自动存储,现在auto用于类型推断
// register:建议编译器将变量存入寄存器(现代编译器会忽略)
// static:静态存储
// extern:外部链接性声明
// mutable:允许const对象的成员被修改

struct Config {
    int id;
    mutable int accessCount;   // 即使Config是const,也可以修改

    void access() const
    {
        accessCount++;   // ✅ mutable成员可以在const函数中修改
    }
};

int main()
{
    using namespace std;

    const Config cfg = {1, 0};
    cfg.access();
    cfg.access();
    cout << "访问次数:" << cfg.accessCount << endl;   // 2

    // static局部变量
    auto countCalls = []() {
        static int count = 0;
        return ++count;
    };

    cout << countCalls() << endl;   // 1
    cout << countCalls() << endl;   // 2
    cout << countCalls() << endl;   // 3

    return 0;
}

9.3.2 cv 限定符(const 和 volatile)

cpp 复制代码
// cv_qualifiers.cpp -- const和volatile示例
#include <iostream>

int main()
{
    using namespace std;

    // const:值不能被修改
    const int MAX_SIZE = 100;
    // MAX_SIZE = 200;   // ❌ 错误

    // const指针的四种形式
    int a = 10, b = 20;

    int* p1 = &a;           // 普通指针:指向和值都可改
    const int* p2 = &a;     // 指向const的指针:值不可改,指向可改
    int* const p3 = &a;     // const指针:指向不可改,值可改
    const int* const p4 = &a; // 指向const的const指针:都不可改

    *p1 = 15;   // ✅ 可以修改值
    p1  = &b;   // ✅ 可以改变指向

    // *p2 = 15; // ❌ 不能修改值
    p2  = &b;   // ✅ 可以改变指向

    *p3 = 15;   // ✅ 可以修改值
    // p3 = &b; // ❌ 不能改变指向

    // *p4 = 15; // ❌ 不能修改值
    // p4 = &b;  // ❌ 不能改变指向

    cout << "a = " << a << endl;

    // volatile:告诉编译器变量可能被外部因素改变,不要优化
    // 常用于硬件寄存器、多线程共享变量
    volatile int hardware_reg = 0;   // 每次都从内存读取,不缓存

    return 0;
}

const指针总结:

声明 指向可改 值可改 记忆方法
int* p 普通指针
const int* p const在*左边:值不变
int* const p const在*右边:指向不变
const int* const p 两个const:都不变

9.4 名称空间(namespace)

9.4.1 为什么需要名称空间?

cpp 复制代码
// 问题:不同库中可能有同名函数/变量,导致命名冲突
// 例如:两个库都定义了 fetch() 函数

// 库A
void fetch() { /* 从数据库获取 */ }

// 库B
void fetch() { /* 从网络获取 */ }

// ❌ 冲突!编译器不知道调用哪个fetch()

名称空间通过将名称封装在独立的区域来解决命名冲突。


9.4.2 定义和使用名称空间

cpp 复制代码
// namespace_basic.cpp -- 名称空间基础
#include <iostream>

// 定义名称空间
namespace Jack {
    double pail;            // 变量
    void fetch();           // 函数原型
    int pal;
    struct Well { int depth; };
}

namespace Jill {
    double bucket(double n) { return n * 2; }
    double fetch;           // 与Jack::fetch不冲突
    int pal;
    struct Hill { int height; };
}

// 在名称空间外定义成员函数
void Jack::fetch()
{
    std::cout << "Jack::fetch() 被调用" << std::endl;
}

int main()
{
    using namespace std;

    // 方式1:作用域解析运算符(最明确)
    Jack::pail = 12.34;
    Jill::fetch = 56.78;
    Jack::fetch();

    cout << "Jack::pail = " << Jack::pail << endl;
    cout << "Jill::fetch = " << Jill::fetch << endl;

    // 方式2:using声明(引入单个名称)
    using Jill::bucket;
    cout << "bucket(5) = " << bucket(5) << endl;

    // 方式3:using编译指令(引入整个名称空间)
    using namespace Jack;
    cout << "pail = " << pail << endl;   // Jack::pail

    return 0;
}

9.4.3 名称空间的嵌套

cpp 复制代码
// namespace_nested.cpp -- 嵌套名称空间
#include <iostream>

namespace Outer {
    int x = 10;

    namespace Inner {
        int x = 20;   // 与Outer::x不冲突

        void show()
        {
            std::cout << "Inner::x = " << x << std::endl;
            std::cout << "Outer::x = " << Outer::x << std::endl;
        }
    }

    void show()
    {
        std::cout << "Outer::x = " << x << std::endl;
    }
}

int main()
{
    Outer::show();          // Outer::x = 10
    Outer::Inner::show();   // Inner::x = 20, Outer::x = 10

    // using简化嵌套访问
    using namespace Outer::Inner;
    show();   // 调用Inner::show()

    return 0;
}

9.4.4 using 声明 vs using 编译指令

cpp 复制代码
// using_comparison.cpp -- using声明与编译指令对比
#include <iostream>

namespace A {
    int x = 1;
    void func() { std::cout << "A::func()" << std::endl; }
}

namespace B {
    int x = 2;
    void func() { std::cout << "B::func()" << std::endl; }
}

void demo_using_declaration()
{
    // using声明:引入特定名称,如有冲突会报错
    using A::x;
    using A::func;
    // using B::x;   // ❌ 错误:x已经被A::x占用

    std::cout << "x = " << x << std::endl;   // 1
    func();   // A::func()
}

void demo_using_directive()
{
    // using编译指令:引入整个名称空间
    // 如有冲突,局部变量优先,否则需要显式指定
    using namespace A;
    using namespace B;

    // x;   // ❌ 二义性:A::x 还是 B::x?
    std::cout << "A::x = " << A::x << std::endl;   // 必须显式指定
    std::cout << "B::x = " << B::x << std::endl;
}

int main()
{
    demo_using_declaration();
    demo_using_directive();
    return 0;
}

using声明 vs using编译指令对比:

特性 using A::name using namespace A
引入范围 单个名称 整个名称空间
冲突处理 编译错误 需显式指定
推荐程度 ✅ 推荐 ⚠️ 谨慎使用
适用场景 精确控制 快速原型/小程序

9.4.5 名称空间的其他特性

cpp 复制代码
// namespace_features.cpp -- 名称空间高级特性
#include <iostream>

// 1. 开放性:可以分多次添加成员
namespace MyLib {
    int version = 1;
}

// 在其他地方继续添加
namespace MyLib {
    void hello() { std::cout << "Hello from MyLib v" << version << std::endl; }
    double pi = 3.14159;
}

// 2. 匿名名称空间(替代static全局变量)
namespace {
    int secret = 42;   // 只在本文件可见,相当于static
    void helper() { std::cout << "内部辅助函数" << std::endl; }
}

// 3. 名称空间别名
namespace VeryLongNamespaceName {
    void func() { std::cout << "长名称空间的函数" << std::endl; }
}
namespace VLN = VeryLongNamespaceName;   // 别名

int main()
{
    using namespace std;

    // 使用开放名称空间
    MyLib::hello();
    cout << "MyLib::pi = " << MyLib::pi << endl;

    // 使用匿名名称空间(直接访问,无需前缀)
    cout << "secret = " << secret << endl;
    helper();

    // 使用别名
    VLN::func();

    return 0;
}

9.4.6 std 名称空间的正确使用

cpp 复制代码
// std_namespace.cpp -- std名称空间的使用建议
#include <iostream>
#include <string>
#include <vector>

// ❌ 不推荐:在头文件中使用 using namespace std
// 会污染所有包含该头文件的文件

// ✅ 推荐方式1:在函数内部使用 using namespace std
void func1()
{
    using namespace std;
    cout << "在函数内使用 using namespace std" << endl;
}

// ✅ 推荐方式2:using声明(只引入需要的名称)
void func2()
{
    using std::cout;
    using std::endl;
    using std::string;

    string s = "Hello";
    cout << s << endl;
}

// ✅ 推荐方式3:直接使用 std:: 前缀(最规范)
void func3()
{
    std::string s = "World";
    std::cout << s << std::endl;
}

int main()
{
    func1();
    func2();
    func3();
    return 0;
}

9.5 综合示例:多文件项目模拟

以下模拟一个完整的多文件项目(在单文件中演示概念):

cpp 复制代码
// multi_file_simulation.cpp -- 多文件项目综合示例
#include <iostream>
#include <string>
#include <vector>

// ===== 模拟 math_utils.h =====
namespace MathUtils {
    const double PI = 3.14159265358979;
    const double E  = 2.71828182845905;

    double circleArea(double r);
    double sphereVolume(double r);
    bool   isPrime(int n);
}

// ===== 模拟 string_utils.h =====
namespace StringUtils {
    std::string toUpper(std::string s);
    std::string toLower(std::string s);
    int         countChar(const std::string& s, char c);
    std::string repeat(const std::string& s, int n);
}

// ===== 模拟 math_utils.cpp =====
namespace MathUtils {
    double circleArea(double r)
    {
        return PI * r * r;
    }

    double sphereVolume(double r)
    {
        return (4.0 / 3.0) * PI * r * r * r;
    }

    bool isPrime(int n)
    {
        if (n < 2) return false;
        for (int i = 2; i * i <= n; i++)
            if (n % i == 0) return false;
        return true;
    }
}

// ===== 模拟 string_utils.cpp =====
#include <cctype>
namespace StringUtils {
    std::string toUpper(std::string s)
    {
        for (char& c : s) c = toupper(c);
        return s;
    }

    std::string toLower(std::string s)
    {
        for (char& c : s) c = tolower(c);
        return s;
    }

    int countChar(const std::string& s, char c)
    {
        int count = 0;
        for (char ch : s)
            if (ch == c) count++;
        return count;
    }

    std::string repeat(const std::string& s, int n)
    {
        std::string result;
        for (int i = 0; i < n; i++)
            result += s;
        return result;
    }
}

// ===== 模拟 main.cpp =====
int main()
{
    using namespace std;

    // 使用 MathUtils
    cout << "===== 数学工具 =====" << endl;
    cout << "PI = " << MathUtils::PI << endl;
    cout << "半径5的圆面积:" << MathUtils::circleArea(5) << endl;
    cout << "半径3的球体积:" << MathUtils::sphereVolume(3) << endl;

    cout << "\n100以内的质数:";
    for (int i = 2; i <= 100; i++)
        if (MathUtils::isPrime(i))
            cout << i << " ";
    cout << endl;

    // 使用 StringUtils
    cout << "\n===== 字符串工具 =====" << endl;
    using namespace StringUtils;   // 局部using

    string s = "Hello, World!";
    cout << "原字符串:" << s << endl;
    cout << "转大写:" << toUpper(s) << endl;
    cout << "转小写:" << toLower(s) << endl;
    cout << "'l'的个数:" << countChar(s, 'l') << endl;
    cout << "重复3次:" << repeat("Ha", 3) << endl;

    // 静态局部变量示例
    cout << "\n===== 静态变量示例 =====" << endl;
    auto makeCounter = [](int start = 0) {
        static int count = start;
        return ++count;
    };

    for (int i = 0; i < 5; i++)
        cout << "计数:" << makeCounter() << endl;

    return 0;
}

输出:

复制代码
===== 数学工具 =====
PI = 3.14159
半径5的圆面积:78.5398
半径3的球体积:113.097

100以内的质数:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

===== 字符串工具 =====
原字符串:Hello, World!
转大写:HELLO, WORLD!
转小写:hello, world!
'l'的个数:3
重复3次:HaHaHa

===== 静态变量示例 =====
计数:1
计数:2
计数:3
计数:4
计数:5

📝 第9章知识点总结

知识点 核心要点
单独编译 头文件(声明)+ 实现文件(定义)+ 主文件,用 #ifndef 防止重复包含
头文件内容 函数原型、结构体/类声明、const常量、模板、内联函数
自动存储 局部变量,代码块结束时销毁,存储在栈上
静态存储 全局变量和 static 变量,程序运行期间一直存在,只初始化一次
动态存储 new/delete 管理,存储在堆上,手动控制生命周期
外部链接性 全局变量,多文件可见,用 extern 声明
内部链接性 static 全局变量,只在本文件可见
作用域解析 :: 访问被遮蔽的全局变量或名称空间成员
const指针 const int*(值不变)vs int* const(指向不变)
volatile 告诉编译器不要优化该变量,每次从内存读取
mutable 允许 const 对象的特定成员被修改
namespace 解决命名冲突,用 :: 访问,可嵌套、可开放、可设别名
匿名namespace 替代 static 全局变量,限制文件内可见性
using声明 using A::name,引入单个名称,冲突时报错
using编译指令 using namespace A,引入全部,谨慎使用
相关推荐
zz34572981131 小时前
函数:python与c语言
c语言·开发语言·python
愿天垂怜1 小时前
【C++脚手架】gtest 单元测试库的介绍与使用
linux·服务器·c++·gitee·前端框架·gtest
小欣加油1 小时前
leetcode 3300 替换为数位和后的最小元素
数据结构·c++·算法·leetcode
晚风予卿云月1 小时前
【枚举】普通枚举
数据结构·c++·算法·竞赛·算法随笔
MR.欻2 小时前
ZLMediaKit 源码分析(四):RTP/RTCP 协议栈实现分析
c++·人工智能·vscode·ffmpeg·音视频
峥嵘life2 小时前
Android getprop 属性限制详解:User 版本属性获取问题分析
android·开发语言·python·学习
郝学胜-神的一滴2 小时前
Qt 高级开发 019:从零定制登录窗口按钮、Logo 样式与交互悬浮效果
开发语言·c++·qt·程序人生·交互·用户界面
YikNjy2 小时前
string(c++)
java·服务器·c++
星夜夏空992 小时前
FreeRTOS学习(5)——内存映射
开发语言·学习