🔷 第十章 类和对象
📄 一句话概括

同一个命名空间可以横跨多个文件——每个文件往里面放自己那一块,链接时自动合成一个。工程上的标准做法是:头文件里放"声明"(函数声明、类定义、常量声明),源文件里放"定义"(函数实现、静态成员的定义),两者的命名空间块同名。一条铁律:头文件里绝对不要写 using namespace(它会污染所有包含者),也不要放"会分配内存的定义"(会导致多重定义,10.1.3 讲过)。

📝 本节和前面几节的关系

本篇是 10.1.3(类的实现) 和 10.6.2(命名空间定义) 的合体:"头文件 + 源文件 + main"三件套,现在每一件都套在命名空间里。还会用到 6.7.4 的 extern(跨文件共享:声明可以多份、定义只能一份)——这条规矩在命名空间里同样成立,而且是本节最容易踩的坑。

一、核心思路:头文件放声明,源文件放定义

一个典型的"自己造的小库"三件套:

文件 1:mylib.h(说明书)

#ifndef MYLIB_H                    // 头文件卫哨(10.1.3)
#define MYLIB_H

namespace mylib {                  // ★ 命名空间块开始

    // ── 常量:用 const 或 extern ──
    const double PI = 3.14159;     // 头文件里的 const 是"内部链接",多份不会冲突

    // ── 函数:只写声明 ──
    double circleArea(double r);
    void   banner(const char *title);

    // ── 类:完整定义(类必须一处写完整,10.1.2)──
    class Counter {
    private:
        int value;
        static int total;          // 静态成员:这里只是"声明"(10.4.3)
    public:
        Counter(int v = 0);
        void add(int n = 1);
        int  get() const;
        static int getTotal();
    };

}                                  // 命名空间结束

#endif                             // MYLIB_H

头文件里放的都是"声明":函数只有原型、类的成员函数只有原型、静态成员变量只有声明。唯一的例外是 const 常量(因为 const 在 C++ 里默认是"内部链接",每个包含它的文件各有一份也不会冲突)。

文件 2:mylib.cpp(操作过程)

#include "mylib.h"                 // 包含自己的头文件
#include <iostream>

namespace mylib {                  // ★ 重开同名命名空间,把实现放进去

    // ── 静态成员变量:必须在某处"定义"一次 ──
    int Counter::total = 0;

    // ── 函数实现 ──
    double circleArea(double r) {
        return PI * r * r;
    }

    void banner(const char *title) {
        std::cout << "=== " << title << " ===" << std::endl;
    }

    // ── 类成员函数的实现 ──
    Counter::Counter(int v) : value(v) {
        total++;
    }

    void Counter::add(int n) {
        value += n;
    }

    int Counter::get() const {
        return value;
    }

    int Counter::getTotal() {
        return total;
    }

}                                  // 命名空间结束

注意两个书写选择:

选择 说明
重开 namespace mylib { ... } 包住所有实现 这样实现里不用逐个写 mylib:: 前缀,看着干净
或者: 每个函数都写全前缀 double mylib::circleArea(double r) { ... }——更明确,但更长

两种都合法,选一种并保持一致就好。

文件 3:main.cpp(使用者)

#include "mylib.h"                 // 只要能看见声明
#include <iostream>

int main() {
    mylib::banner("开始");                                // 用全名(推荐)

    std::cout << "圆面积 = " << mylib::circleArea(2) << std::endl;

    mylib::Counter c(5);
    c.add(3);
    std::cout << "计数器 = " << c.get() << std::endl;

    mylib::Counter d(1);                                  // 再造一个
    std::cout << "一共造了 " << mylib::Counter::getTotal() << " 个" << std::endl;

    return 0;
}

看到什么算成功:

=== 开始 ===
圆面积 = 12.5636
计数器 = 8
一共造了 2 个

二、三条必须记牢的铁律

铁律 1:头文件里不放 using namespace

// mylib.h  —— 千万别这么写!
#ifndef MYLIB_H
#define MYLIB_H

using namespace std;        // ❌❌ 灾难:所有包含 mylib.h 的文件都被污染

namespace mylib {
    void banner(const char *title);
}                           // 这里的实现用了 cout,那就在 .cpp 里写 using 或用 std::
#endif

为什么是灾难:mylib.h 会被几十个文件包含,每个文件都会被迫"放行 std 里的所有名字"——别人自己写的 count、swap 可能从此撞名,而且他们完全不知道为什么。

正确做法:

位置 怎么写
头文件里 全部写 std:: 全名(或者只放行要用的:using std::cout;——但即便如此也建议写全名)
源文件里 可以在函数内部用 using namespace std;(10.6.1 说的"局部放行")
main.cpp 练习可以放开头;规范做法还是写全名

铁律 2:头文件里不放"会分配内存的定义"

这是 10.1.3 的老规矩("头文件里不该放定义,否则 multiple definition"),在命名空间里同样成立:

// mylib.h
namespace mylib {
    int counter = 0;            // ❌ 这是"定义"(分配了内存)
    void reset() { counter = 0; }   // ❌ 非内联函数的定义
}

如果 mylib.h 被两个 .cpp 包含,链接时会报:

multiple definition of `mylib::counter'

正确的写法:

想要什么 头文件里写 源文件里写
一个共享的整数 extern int counter;(声明) int counter = 0;(定义)
一个常量 const double PI = 3.14;(可以,const 默认内部链接) ——
一个函数 void reset();(声明) void reset() { ... }(定义)
一个短函数想内联 inline void reset() { ... }(可以,10.4.2 的例外) ——

记住这条判据:"这一行会让编译器分配内存吗?" 会(如 int counter = 0;、非内联的函数体)→ 只能放 .cpp;不会(声明、类定义、const、inline)→ 放 .h 可以。

铁律 3:每个 .cpp 都 #include "mylib.h"

不能靠"我记得加过"——每个用到 mylib 名字的文件,都必须自己包含那个头文件。忘了会报"未声明的标识符",而且报错看起来跟命名空间没关系,容易查半天。

三、Dev-C++ 里的实际操作

和 10.1.3 一模一样(10.1.3 第四节那张表仍然适用),只是文件内容多了命名空间:

步骤 操作
① File → New → Project,选 Console Application
② 保存到纯英文路径,自动生成 main.cpp 和 .dev
③ 在工程面板右键 → New file,另存为 mylib.h
④ 同样再加 mylib.cpp
⑤ 确认三个文件都在工程面板里(这是最容易漏的一步)
⑥ F11 编译、F9 运行

如果漏了第 ⑤ 步:症状是 undefined reference to 'mylib::circleArea(double)'——编译没问题(头文件里看得到声明),链接找不到实现(mylib.cpp 没参与编译)。这个错在 6.7.4、10.1.3 都出现过,现在你会一眼认出来。

四、动手 Demo:亲手拆一次

按上面的三件套建好工程,把代码敲进去。下面是"编译前检查清单":

看到什么算成功:0 errors,输出如第一节所示。

故意制造一个错误试试:在 mylib.h 里加一行 int sharedCount = 0;,然后编译。你会看到 multiple definition of 'mylib::sharedCount'——因为 mylib.cpp 和 main.cpp 都包含了这个头文件,各自生成了一份。改成 extern int sharedCount;(声明)+ 在 .cpp 里写 int sharedCount = 0;(定义),就好了。

五、常见误区

  1. "头文件里写 using namespace std; 省事"——头文件里最不能写这个(铁律 1)。你省的两秒,会让整个项目的人都踩坑。
  2. "命名空间在头文件里声明了,源文件里就不用再写"——不一定。源文件里如果要用命名空间里的名字,要么重开同名命名空间块(这样能直接写短名),要么写全前缀(mylib::)。两种都行,但不能什么都不写——那样函数就成了全局的,和头文件里的声明对不上。
  3. "静态成员变量在头文件里写 static int total; 就够了"——不够。头文件里那行只是声明;真正的"定义"(int Counter::total = 0;)必须放在某个 .cpp 里,且只能一处(10.4.3)。漏了会 undefined reference,写两处会 multiple definition。
  4. "const 常量放头文件里也会多重定义"——不会。C++ 里 const(以及 constexpr)变量默认是"内部链接",每个包含它的文件各有一份,链接时互不冲突。这是头文件里少数可以放"带初值的定义"的情况之一。
  5. "多文件太麻烦了,一个文件写到底"——小程序可以,长了不行。多文件带来的是:改实现不用重编别人(10.1.3)、代码容易找、多人能并行。命名空间让这套机制"有牌子可挂"——你的 mylib 不会和别人的库撞名。这就是工程化的起点。
自测三问(点开对答案) 1. 头文件里为什么绝对不能写 `using namespace`?——因为**头文件会被很多文件包含**,你的 `using` 会**把整个命名空间的名字塞给所有包含者**,导致别人代码里的同名标识符撞车(**报错还看不出原因**)。**规范做法:头文件里写 `std::` 全名。** 2. 头文件里能放什么、不能放什么?——**能放**:函数声明、类定义、`extern` 声明、`const` 常量、`inline` 函数。**不能放**:会分配内存的定义(如 `int x = 0;`)、非内联的函数体——否则**多个 `.cpp` 包含时 `multiple definition`**。 3. 静态成员变量的"定义"放在哪儿?漏了/写两遍分别报什么?——**放在某一个 `.cpp` 里,且只能一处**(`int Counter::total = 0;`)。**漏了报 `undefined reference`(链接错误);写两遍报 `multiple definition`。**

相关笔记