类长大了就要分家:头文件 .h 放"长什么样"(类定义:成员变量 + 函数声明),源文件 .cpp 放"怎么干"(函数实现)。这么分的好处是:别人只用改实现时,你不需要重新编译我的代码——编译一次省下的就是工程效率。多文件工程还要解决一件事:同一个头文件被包含两次会报"重复定义",靠头文件卫哨(#ifndef / #pragma once)挡住。
10.1.2 讲了"类里声明、类外实现",本节把这个习惯升级成工程规范。它会直接用到 1.3.1 的工程文件知识——那时我们知道了 .cpp/.o/.exe 的角色和"编译 + 链接两步";现在第一次真正体会到为什么要那样分。另外 6.7.4 的 extern(跨文件共享变量)也是同类问题:"声明"可以到处放,"定义"只能有一份。
一、为什么要分文件
到 8.7 为止,我们的程序都在一个 .cpp 里。但真实项目里:
| 问题 | 单文件的做法 | 分文件的做法 |
|---|---|---|
| 文件太长 | 一个 .cpp 三千行,找东西靠搜 |
每个类一个文件,一找就到 |
| 改动牵连 | 改一行,整个文件重编 | 改 .cpp 里的实现,别的文件不用重编(重要!) |
| 复用 | 想给别的程序用,得整段复制 | 拷 .h + .cpp 两三个文件就完事 |
| 多人协作 | 两个人改同一个文件,冲突不断 | 各改各的文件 |
最关键的是第二条。 回忆 1.3.1 的编译流程:编译是按文件做的(.cpp → .o),链接才是拼起来。所以只改实现(.cpp)不影响别人——别人手里的 .o 还能继续用,省掉大量重复编译时间。
一句话:头文件是"说明书",源文件是"操作过程"。 用户只看说明书(知道怎么调),不必看操作过程(里面的实现)。
二、三件套:.h + .cpp + main.cpp
把 10.1.2 的 Rectangle 拆成三个文件。
文件 1:Rectangle.h(说明书)
#ifndef RECTANGLE_H // ← 头文件卫哨(见下节)
#define RECTANGLE_H
class Rectangle { // 只写"长什么样":成员变量 + 函数声明
private:
double w, h;
public:
void set(double width, double height);
double area();
double perimeter();
void show();
};
#endif // ← 卫哨收尾
头文件里不放实现(有个例外:类内定义的短函数,见 10.4.2)。头文件的使命是"把类的长相告诉编译器"。
文件 2:Rectangle.cpp(操作过程)
#include "Rectangle.h" // ← 用双引号(2.1.1:先找本工程)
#include <iostream>
using namespace std;
void Rectangle::set(double width, double height) {
w = (width > 0) ? width : 1;
h = (height > 0) ? height : 1;
}
double Rectangle::area() { return w * h; }
double Rectangle::perimeter() { return 2 * (w + h); }
void Rectangle::show() {
cout << "宽 " << w << " 高 " << h
<< " → 面积 " << area()
<< ",周长 " << perimeter() << endl;
}
注意:这里必须 #include "Rectangle.h"——否则编译器不知道 Rectangle 是什么,Rectangle::set 也认不出来。
文件 3:main.cpp(使用者)
#include "Rectangle.h" // ← 只要能看见"说明书"就够了
int main() {
Rectangle r;
r.set(4, 3);
r.show();
return 0;
}
注意:main.cpp 只包含头文件,不需要包含 Rectangle.cpp——实现不用它操心,链接阶段会自己接上(1.3.1 的两步)。
看到什么算成功:宽 4 高 3 → 面积 12,周长 14
三、头文件卫哨:挡住"重复定义"
问题场景:A.h 和 B.h 都包含了 Point.h,而 main.cpp 又同时包含了 A.h 和 B.h:
// main.cpp
#include "A.h" // A.h 里 #include "Point.h"
#include "B.h" // B.h 里也 #include "Point.h"
// 结果:Point 的定义被塞进来两次 → 报错 redefinition of 'class Point'
解法:在头文件外面套一层"卫哨"。
#ifndef POINT_H // if not defined:如果 POINT_H 还没被定义过
#define POINT_H // 那就定义它
class Point {
...
};
#endif // 到这里结束
工作原理(一步步看):
| 第几次遇到这个头文件 | 发生了什么 |
|---|---|
| 第一次 | POINT_H 未定义 → 进入 → 定义 POINT_H → 编译类定义 |
| 第二次(同一编译单元内再次被包含) | POINT_H 已定义 → 整段跳过 → 不会重复定义 |
三个细节:
#ifndef后面的宏名要"唯我独有"——通常用文件名大写 + 下划线(POINT_H、RECTANGLE_H)。不同头文件用同一个名字,会导致某个头文件被莫名跳过。#define的位置在#ifndef之后、内容之前——这样内容才在"守卫"之内。#endif别忘了写(写完最好加个注释// POINT_H,配对一目了然)。- 现代写法还有一句就够的:
#pragma once // 放在头文件最上面,效果一样
#pragma once 更短、不用起名字,但它是编译器扩展,不是 C++ 标准。主流编译器(g++、MSVC、Clang)都支持,所以现在用得很广。本课两个都认,看到别人用哪种都不慌。
四、Dev-C++ 里怎么建多文件工程
教材同款 Dev-C++(1.2.1)的操作路径:
| 步骤 | 操作 | 看到什么算成功 |
|---|---|---|
| ① 新建工程 | 菜单 File → New → Project,选 Console Application、C++,起名如 RectDemo |
弹出保存对话框 |
| ② 保存 | 选一个纯英文路径(1.2.2 的坑),保存 | 自动生成 main.cpp 和 .dev 工程文件(1.3.1) |
| ③ 加头文件 | 在左侧工程面板右键 → New file → 另存为 Rectangle.h |
工程面板里出现 Rectangle.h |
| ④ 加源文件 | 同样再 New file → 存成 Rectangle.cpp |
面板里出现三个文件 |
| ⑤ 编译运行 | F11 编译、F9 运行 | 编译信息区显示 0 errors |
关键一步是"在工程面板里加文件",而不是"在磁盘上放几个文件"——只有被工程(.dev,1.3.1 那个"记账本")登记过的文件才会参与编译。磁盘上有文件但没加进工程 → 链接时找不到实现 → undefined reference(6.7.4 见过的脸)。这是新手第一个多文件工程最常卡住的地方。
五、动手 Demo:亲手拆一次(三个文件)
按上面第四节建好工程,把三个文件写出来。(代码见第二节,这里给出最容易出错的检查清单。)
编译前对着这份清单过一遍:
- ☐
Rectangle.h开头有没有#ifndef RECTANGLE_H/#define RECTANGLE_H、末尾有没有#endif? - ☐
Rectangle.h里的类定义末尾有分号吗?(10.1.2 的坑) - ☐
Rectangle.cpp开头包含自己的头文件了吗? - ☐
Rectangle.cpp里每个实现都带Rectangle::吗? - ☐ 三个文件都在工程面板里吗?
- ☐ 路径全是英文吗?
看到什么算成功:编译信息区 0 errors,运行输出 宽 4 高 3 → 面积 12,周长 14。
故意制造一个错误试试:把 Rectangle.cpp 里 set 的 Rectangle:: 删掉再 F11。你会看到 'w' was not declared in this scope——报错信息里还会带上文件名和行号(Rectangle.cpp:7)。多文件工程的报错会告诉你"哪个文件、第几行"——这是它的好处。
六、常见误区
- "头文件里该把实现也写上"——不该。头文件被多个
.cpp包含,如果里面是"定义"(不是声明),就会出现多份定义,链接时报multiple definition。头文件里的东西必须是"声明"或"内联的"(10.4.2 的内联函数是例外)。 - "
main.cpp要#include "Rectangle.cpp""——千万不要。那样等于把实现整段抄进main.cpp,而Rectangle.cpp自己也要编译一份 → 同一个函数两份定义 → 链接错误。包含头文件就够了,实现由链接器负责接上(1.3.1)。 - "头文件卫哨的名字随便起"——不能随便。两个不同头文件用同一个宏名,会导致后遇到的那个被整段跳过,症状是"明明写了类却找不到"(这错特别难查)。
- "磁盘上建了文件就算加进工程了"——不算。必须在 Dev-C++ 的工程面板里添加(或者手动改
.dev文件)。"文件在但没编" 的典型症状就是undefined reference。 - "头文件包含得越多越好"——越少越好。每多包含一个头文件,编译时就多读一遍,项目大了会明显拖慢编译。规范做法:能用前向声明(10.1.2 第四节)就不用
#include。
自测三问(点开对答案)
1. 头文件和源文件各放什么?——**头文件放"长什么样"**:类定义(成员变量 + 函数声明);**源文件放"怎么干"**:函数实现(带 `类名::`)。 2. 头文件卫哨是干什么的?三句话怎么写?——防止**同一个头文件被重复包含**导致重定义报错。写法:`#ifndef 宏名` → `#define 宏名` → 内容 → `#endif`;现代写法一句 **`#pragma once`** 也行。 3. 为什么 `main.cpp` 不该 `#include "Rectangle.cpp"`?——因为 `.cpp` 里是**定义**。包含它会让实现在 `main.cpp` 里也有一份,而 `Rectangle.cpp` 本身还要编译一份,**同一个函数两份定义 → 链接错误**。正确做法是只包含 `.h`,让链接器把两份 `.o` 拼接起来。- ☐ 10.1.4 对象的声明(下一节)
相关笔记
- 类的声明与定义 —— 上一节:类里声明、类外实现
- 对象的声明 —— 下一节:怎么造出对象来用
- C++工程项目文件 ——
.cpp/.o/.exe与"编译 + 链接"两步 - extern变量 —— 同类问题:声明可以多份、定义只能一份
- include 指令 ——
< >与" "的区别 - 🔷 C++基础 · 索引 —— 全课路线图