同一个命名空间可以横跨多个文件——每个文件往里面放自己那一块,链接时自动合成一个。工程上的标准做法是:头文件里放"声明"(函数声明、类定义、常量声明),源文件里放"定义"(函数实现、静态成员的定义),两者的命名空间块同名。一条铁律:头文件里绝对不要写 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:亲手拆一次
按上面的三件套建好工程,把代码敲进去。下面是"编译前检查清单":
- ☐
mylib.h有#ifndef / #define / #endif卫哨吗? - ☐
mylib.h里只有声明(函数原型、类定义、const常量、extern声明)吗?有没有不小心写了int x = 0;? - ☐
mylib.h里有没有using namespace?(有就删掉) - ☐
mylib.cpp有自己的#include "mylib.h"吗? - ☐
Counter::total在mylib.cpp里定义了一次且只有一次吗? - ☐ 三个文件都在工程面板里吗?
看到什么算成功:0 errors,输出如第一节所示。
故意制造一个错误试试:在 mylib.h 里加一行 int sharedCount = 0;,然后编译。你会看到 multiple definition of 'mylib::sharedCount'——因为 mylib.cpp 和 main.cpp 都包含了这个头文件,各自生成了一份。改成 extern int sharedCount;(声明)+ 在 .cpp 里写 int sharedCount = 0;(定义),就好了。
五、常见误区
- "头文件里写
using namespace std;省事"——头文件里最不能写这个(铁律 1)。你省的两秒,会让整个项目的人都踩坑。 - "命名空间在头文件里声明了,源文件里就不用再写"——不一定。源文件里如果要用命名空间里的名字,要么重开同名命名空间块(这样能直接写短名),要么写全前缀(
mylib::)。两种都行,但不能什么都不写——那样函数就成了全局的,和头文件里的声明对不上。 - "静态成员变量在头文件里写
static int total;就够了"——不够。头文件里那行只是声明;真正的"定义"(int Counter::total = 0;)必须放在某个.cpp里,且只能一处(10.4.3)。漏了会undefined reference,写两处会multiple definition。 - "
const常量放头文件里也会多重定义"——不会。C++ 里const(以及constexpr)变量默认是"内部链接",每个包含它的文件各有一份,链接时互不冲突。这是头文件里少数可以放"带初值的定义"的情况之一。 - "多文件太麻烦了,一个文件写到底"——小程序可以,长了不行。多文件带来的是:改实现不用重编别人(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`。**- ☐ 10.6.4 定义嵌套的命名空间(下一节)