目录
[1.1 为什么要学习多文件编译?](#1.1 为什么要学习多文件编译?)
[1.2 多文件编译的核心思想](#1.2 多文件编译的核心思想)
[二、.h 文件和 .cpp 文件到底分别写什么?](#二、.h 文件和 .cpp 文件到底分别写什么?)
[2.1 .h 文件:头文件通常放声明](#2.1 .h 文件:头文件通常放声明)
[2.2 .cpp 文件:源文件通常放定义](#2.2 .cpp 文件:源文件通常放定义)
[2.3 结合你的三个文件来理解](#2.3 结合你的三个文件来理解)
[3.1 为什么不要在 .h 文件中定义全局变量?](#3.1 为什么不要在 .h 文件中定义全局变量?)
[3.2 extern 的基本语法](#3.2 extern 的基本语法)
[3.3 正确写法:.h 中声明,.cpp 中定义](#3.3 正确写法:.h 中声明,.cpp 中定义)
[3.4 extern 声明和变量定义的区别](#3.4 extern 声明和变量定义的区别)
[(1)extern int gcount 是声明](#(1)extern int gcount 是声明)
[(2)int gcount 是定义](#(2)int gcount 是定义)
[3.5 extern 声明时不要顺手初始化](#3.5 extern 声明时不要顺手初始化)
[3.6 声明和定义的区别](#3.6 声明和定义的区别)
[4.1 为什么需要头文件保护?](#4.1 为什么需要头文件保护?)
[4.2 #pragma once 和 #ifndef 的区别](#pragma once 和 #ifndef 的区别)
[(1)#pragma once](#pragma once)
[4.3 函数声明中可以写默认参数,定义中不要重复写](#4.3 函数声明中可以写默认参数,定义中不要重复写)
[五、extern "C" 与完整多文件调用流程](#五、extern "C" 与完整多文件调用流程)
[5.1 extern "C" 是干什么的?](#5.1 extern "C" 是干什么的?)
[5.2 单个 C 函数声明](#5.2 单个 C 函数声明)
[5.3 一组 C 函数声明](#5.3 一组 C 函数声明)
[5.4 你的三个文件完整执行流程](#5.4 你的三个文件完整执行流程)
[6.1 多文件编译的核心思想](#6.1 多文件编译的核心思想)
[6.2 全局变量的声明与定义](#6.2 全局变量的声明与定义)
[6.3 不要在 .h 文件中直接定义全局变量](#6.3 不要在 .h 文件中直接定义全局变量)
[6.4 函数的声明与定义](#6.4 函数的声明与定义)
[6.5 默认参数一般放在函数声明中](#6.5 默认参数一般放在函数声明中)
[6.6 头文件保护的作用](#6.6 头文件保护的作用)
[6.7 extern "C" 的作用](#6.7 extern "C" 的作用)
[6.8 最终记忆口诀](#6.8 最终记忆口诀)
一、本节学习内容概要

1.1 为什么要学习多文件编译?
前面写代码时,很多示例都放在一个 .cpp 文件中。
例如:
cpp
#include <iostream>
using namespace std;
int TestFunc(int x, int y)
{
return x + y;
}
int main()
{
cout << TestFunc(1, 2) << endl;
return 0;
}
这种写法在代码量很少时没问题。
但是项目一大,就会出现几个问题:
(1)所有代码都堆在一个文件里,不好维护。
(2)函数越来越多,不方便查找。
(3)全局变量、函数、类混在一起,结构不清晰。
(4)多个 .cpp 文件想共用同一个函数时,不知道怎么引用。
所以实际工程中,通常会把代码拆成多个文件。
常见结构是:
base16.h // 头文件,放声明
base16.cpp // 源文件,放定义
test_muti_file.cpp // main 函数所在文件,负责调用
这也是现在 C++ 项目里非常普遍的一种组织方式。
1.2 多文件编译的核心思想
多文件编译可以简单理解为:
.h 文件:告诉别人我有什么
.cpp 文件:真正实现这些东西
main.cpp:使用这些东西
更准确地说:
(1)
.h头文件主要放声明。
(2).cpp源文件主要放定义。
(3)每个.cpp文件会被单独编译。
(4)最后由链接器把多个.cpp编译出来的结果合并成一个可执行程序。
编译过程大致如下:
base16.cpp -> base16.obj
test_muti_file.cpp -> test_muti_file.obj
base16.obj + test_muti_file.obj -> 最终 exe 程序
所以一定要记住一句话:
.h 文件通常不是单独编译的,它是被 #include 到某个 .cpp 文件中,然后跟着 .cpp 一起参与编译。
二、.h 文件和 .cpp 文件到底分别写什么?
2.1 .h 文件:头文件通常放声明
base16.h 中,有这样的代码:
cpp
#pragma once
#ifndef BASE16_H
#define BASE16_H
extern int gcount;
int TestFunc(int, int x, int y = 10);
extern "C" void TestC();
extern "C"
{
void TestFuncC();
}
#endif
这个文件的主要作用是:
告诉其他 .cpp 文件:
我这里有一个全局变量 gcount;
我这里有一个函数 TestFunc;
我这里有两个 C 语言方式链接的函数 TestC 和 TestFuncC;
注意,这里大部分内容都是声明,不是真正定义。
比如:
cpp
extern int gcount;
这句话不是创建一个新的全局变量,而是声明:
别的地方已经有一个 int 类型的全局变量 gcount,你们可以使用它。
再比如:
cpp
int TestFunc(int, int x, int y = 10);
这句话是函数声明。
它只是告诉编译器:
有一个函数叫 TestFunc;
返回值是 int;
参数是 int、int、int;
第三个参数默认值是 10。
但是它没有函数体,所以它还不是函数定义。
2.2 .cpp 文件:源文件通常放定义
base16.cpp 中,有这样的代码:
cpp
#include "base16.h"
int gcount;
int TestFunc(int, int x, int y)
{
return 0;
}
extern "C" void TestC()
{
}
extern "C"
{
void TestFuncC()
{
}
}
这里就是真正的定义。
比如:
cpp
int gcount;
这句话会真正创建一个全局变量 gcount。
也就是说,它会在全局区分配空间。
再比如:
cpp
int TestFunc(int, int x, int y)
{
return 0;
}
这是真正的函数定义。
因为它有函数体:
{
return 0;
}
所以可以理解为:
- 声明:告诉编译器有这个东西
- 定义:真正把这个东西创建出来
2.3 结合你的三个文件来理解
(1)base16.h
base16.h
是**"声明文件"或者"头文件"**
它主要放:
函数声明
全局变量声明
类声明
C 语言函数声明
宏定义
比如:
cpp
extern int gcount;
int TestFunc(int, int x, int y = 10);
(1)base16.cpp
base16.cpp
是**"实现文件"或者"定义文件"**
它主要放:
全局变量定义
函数定义
类成员函数定义
实际代码逻辑
比如:
cpp
int gcount;
int TestFunc(int, int x, int y)
{
return 0;
}
(3)test_muti_file.cpp
cpp
test_muti_file.cpp
是主程序文件。
它里面有:
cpp
int main()
{
gcount++;
cout << gcount << endl;
TestFunc(1, 2);
TestC();
TestFuncC();
}
这个文件负责调用前面定义好的变量和函数。
所以这三个文件的关系可以总结为:
| 文件 | 主要作用 | 是否常见 |
|---|---|---|
| base16.h | 放声明,提供接口 | 非常常见 |
| base16.cpp | 放定义,提供实现 | 非常常见 |
| test_muti_file.cpp | 放 main 函数,负责调用 | 非常常见 |
这就是 C++ 工程中非常经典的文件组织方式:
.h 负责声明接口
.cpp 负责实现功能
main.cpp 负责启动程序
三、多文件中的全局变量声明、定义与extern
3.1 为什么不要在 .h 文件中定义全局变量?
cpp
//不要在.h中定义全局变量
//int x;
这句话非常重要。
假设你在 base16.h 中写:
int x;
然后两个 .cpp 文件都包含它:
// base16.cpp
#include "base16.h"
// test_muti_file.cpp
#include "base16.h"
预处理之后,相当于两个 .cpp 文件里面都出现了:
int x;
也就是说:
base16.cpp 里定义了一个 x
test_muti_file.cpp 里也定义了一个 x
最后链接时,链接器会发现:
怎么有两个全局变量 x?
于是就可能报错:
multiple definition of x
这就是为什么:
不要在 .h 文件中直接定义普通全局变量。
3.2 extern 的基本语法
多文件中声明全局变量,通常要使用 extern。
它的基本语法是:
cpp
extern 类型 变量名;
例如:
cpp
extern int gcount;
extern double gspeed;
extern float gradius;
这里以:
cpp
extern int gcount;
为例。
它的意思是:
- 我这里只是声明一下,有一个 int 类型的全局变量 gcount。
- 这个变量不是在这里创建的,它在其他 .cpp 文件中定义。
所以:
cpp
extern int gcount;
不是定义变量,也不是创建变量。
它只是告诉编译器:
- 有一个全局变量叫 gcount,你可以放心使用。
- 至于它真正在哪里创建,链接阶段会去其他 .cpp 文件中找。
而下面这句:
cpp
int gcount;
才是真正定义变量。
它的意思是:
创建一个 int 类型的全局变量 gcount。
所以这两句虽然看起来很像,但是含义完全不同:
| 写法 | 含义 | 是否真正创建变量 | 推荐位置 |
|---|---|---|---|
extern int gcount; |
声明变量 | 否 | .h 文件 |
int gcount; |
定义变量 | 是 | .cpp 文件 |
可以简单理解为:
extern 类型 变量名; // 声明:告诉编译器有这个变量
类型 变量名; // 定义:真正创建这个变量
3.3 正确写法:.h 中声明,.cpp 中定义
全局变量的正确写法是:
- .h 文件中声明
- .cpp 文件中定义
- 其他 .cpp 文件包含头文件后使用
在 .h 文件中声明:
cpp
extern int gcount;
在 .cpp 文件中定义:
cpp
int gcount;
这两句意思完全不一样。
extern int gcount; 的意思是:
我只是声明一下,gcount 在别的地方定义。
int gcount; 的意思是:
我真正创建一个全局变量 gcount。
所以完整写法是:
cpp
// base16.h
#pragma once
extern int gcount;
cpp
// base16.cpp
#include "base16.h"
int gcount;
cpp
// main.cpp
#include <iostream>
#include "base16.h"
using namespace std;
int main()
{
gcount++;
cout << gcount << endl;
return 0;
}
这样写时,程序中只有一个真正的 gcount。
其他 .cpp 文件只是通过头文件知道:有一个 gcount 可以用。
3.4 extern 声明和变量定义的区别
多文件全局变量中,最容易混淆的就是这两句:
cpp
extern int gcount;
和:
cpp
int gcount;
它们的区别如下:
(1)extern int gcount 是声明
cpp
extern int gcount;
含义是:
gcount 在别的文件中定义。
我这里只是声明一下。
它一般放在 .h 文件中。
例如:
cpp
// base16.h
#pragma once
extern int gcount;
这样其他 .cpp 文件只要包含 base16.h,就可以知道 gcount 这个变量存在。
(2)int gcount 是定义
cpp
int gcount;
含义是:
真正创建一个全局变量 gcount。
它一般放在某一个 .cpp 文件中。
例如:
cpp
// base16.cpp
#include "base16.h"
int gcount;
一个全局变量在整个程序中通常只应该有一次定义。
如果多个 .cpp 文件中都写了:
int gcount;
就容易造成重复定义问题。
(3)声明和定义必须配套
如果只有声明:
cpp
// base16.h
extern int gcount;
但是没有任何 .cpp 文件真正定义:
int gcount;
那么编译阶段可能能通过,但是链接阶段会出问题。
因为链接器找不到真正的 gcount。
可能会出现类似错误:
undefined reference to gcount
或者:
unresolved external symbol gcount
所以一定要保证:
extern int gcount; // 声明
int gcount; // 定义
这两部分要同时存在。
3.5 extern 声明时不要顺手初始化
有些初学者可能会写成:
cpp
extern int gcount = 10;
这句代码虽然前面有 extern,但是因为后面带了初始化:
= 10
所以它已经不是普通声明了,而是变成了定义。
也就是说:
extern int gcount = 10;
会真正创建变量。
因此不要把这种写法放在 .h 文件中。
推荐写法是:
cpp
// base16.h
#pragma once
extern int gcount;
cpp
// base16.cpp
#include "base16.h"
int gcount = 10;
这样结构最清晰。
.h 文件负责声明:
cpp
extern int gcount;
.cpp 文件负责定义和初始化:
cpp
int gcount = 10;
3.6 声明和定义的区别
可以用一句话区分:
- 声明:告诉编译器有这个东西
- 定义:真正创建这个东西
例如:
cpp
extern int gcount;
这是声明。
cpp
int gcount;
这是定义。
再比如函数:
cpp
int TestFunc(int, int x, int y = 10);
这是函数声明。
cpp
int TestFunc(int, int x, int y)
{
return 0;
}
这是函数定义。
因为函数定义里面有真正的函数体。
| 类型 | 声明 | 定义 |
|---|---|---|
| 全局变量 | extern int gcount; |
int gcount; |
| 函数 | int TestFunc(int, int, int = 10); |
int TestFunc(int, int, int) { return 0; } |
四、头文件保护、函数声明和默认参数
4.1 为什么需要头文件保护?
base16.h 中写了:
cpp
#pragma once
#ifndef BASE16_H
#define BASE16_H
// 头文件内容
#endif
它们的作用是:
防止同一个头文件在同一个 .cpp 文件中被重复包含。
例如:
cpp
#include "base16.h"
#include "base16.h"
#include "base16.h"
- 如果没有保护,头文件内容就会被复制多次。
- 这可能导致重复声明、重复定义等问题。
4.2 #pragma once 和 #ifndef 的区别
(1)#pragma once
cpp
#pragma once
含义是:
保证当前头文件在同一个 .cpp 文件中只被包含一次。
优点:
写法简单
编译效率较高
主流编译器基本都支持
比如 MSVC、GCC、Clang 都支持。
(2)#ifndef
cpp
#ifndef BASE16_H
#define BASE16_H
// 头文件内容
#endif
含义是:
- 如果没有定义过
BASE16_H,就定义它,并且展开头文件内容。- 如果已经定义过了,就不再重复展开。
优点:
兼容性更好
可移植性更强
是非常传统的写法
实际工程中,一般二选一就可以。
4.3 函数声明中可以写默认参数,定义中不要重复写
base16.h 中有:
cpp
int TestFunc(int, int x, int y = 10);
这里给第三个参数设置了默认值:
cpp
int y = 10
所以调用时可以这样写:
cpp
TestFunc(1, 2);
虽然只传了两个参数,但是第三个参数会自动使用默认值 10。
相当于:
TestFunc(1, 2, 10);
但是在 base16.cpp 中,函数定义不能再重复设置默认参数。
正确写法是:
cpp
int TestFunc(int, int x, int y)
{
return 0;
}
不要写成:
cpp
int TestFunc(int, int x, int y = 10)
{
return 0;
}
因为默认参数只能在函数声明或者函数定义的某一个地方设置一次。
在多文件项目中,通常把默认参数放在 .h 的函数声明中。
原因是:
- 其他
.cpp文件调用函数时,只能看到.h文件。- 如果默认参数写在
.cpp文件中,其他文件看不到这个默认值,就无法正确使用。
所以推荐写法是:
cpp
// base16.h
int TestFunc(int, int x, int y = 10);
cpp
// base16.cpp
int TestFunc(int, int x, int y)
{
return 0;
}
五、extern "C" 与完整多文件调用流程
5.1 extern "C" 是干什么的?
cpp
extern "C" void TestC();
还有:
cpp
extern "C"
{
void TestFuncC();
}
extern "C" 的作用是:
让 C++ 编译器按照 C 语言的方式处理函数名。
为什么需要这个东西?
因为 C++ 支持函数重载。
比如:
cpp
void Test(int);
void Test(double);
void Test(int, int);
这三个函数名字都叫 Test,但是参数不同。
C++ 编译器为了区分它们,底层会对函数名进行改编,这个过程叫:
name mangling
也就是函数名修饰。
但是 C 语言不支持函数重载。
C 语言函数名在底层通常还是比较直接的名字。
所以如果 C++ 想调用 C 语言写的函数,就需要告诉 C++ 编译器:
这个函数按 C 语言规则来链接,不要按 C++ 规则改名字。
于是就有了:
extern "C"
5.2 单个 C 函数声明
如果只有一个 C 语言函数,可以这样写:
cpp
extern "C" void TestC();
对应定义可以这样写:
cpp
extern "C" void TestC()
{
}
然后在 main 中调用:
cpp
TestC();
5.3 一组 C 函数声明
如果有多个 C 语言函数,可以用大括号统一包起来:
cpp
extern "C"
{
void TestFuncC();
void TestFuncC2();
void TestFuncC3();
}
对应定义也可以这样写:
cpp
extern "C"
{
void TestFuncC()
{
}
void TestFuncC2()
{
}
void TestFuncC3()
{
}
}
5.4 你的三个文件完整执行流程
现在来看你的三个文件。
(1)base16.h:
cpp
#pragma once
#ifndef BASE16_H
#define BASE16_H
extern int gcount;
int TestFunc(int, int x, int y = 10);
extern "C" void TestC();
extern "C"
{
void TestFuncC();
}
#endif
它的作用是声明:
gcount 存在
TestFunc 存在
TestC 存在
TestFuncC 存在
(2)base16.cpp:
#include "base16.h"
int gcount;
int TestFunc(int, int x, int y)
{
return 0;
}
extern "C" void TestC()
{
}
extern "C"
{
void TestFuncC()
{
}
}
它的作用是真正定义:
创建全局变量 gcount
实现 TestFunc 函数
实现 TestC 函数
实现 TestFuncC 函数
(3)test_muti_file.cpp:
cpp
#include <iostream>
#include "base16.h"
using namespace std;
int main()
{
gcount++;
cout << gcount << endl;
TestFunc(1, 2);
TestC();
TestFuncC();
}
它的作用是使用:
使用 gcount
调用 TestFunc
调用 TestC
调用 TestFuncC
(4)完整执行逻辑是:
1. main.cpp 包含 base16.h
2. 编译器知道 gcount、TestFunc、TestC、TestFuncC 都存在
3. main.cpp 可以正常通过编译
4. base16.cpp 中真正定义了这些变量和函数
5. 链接器把 main.cpp 和 base16.cpp 的编译结果合并
6. 程序最终成功运行
六、本节总结
6.1 多文件编译的核心思想
多文件编译最核心的思想是:
- .h 文件放声明
- .cpp 文件放定义
- main.cpp 文件负责调用
也可以理解为:
- .h 文件:告诉别人我有什么
- .cpp 文件:真正实现这些东西
- main.cpp 文件:使用这些东西
6.2 全局变量的声明与定义
对于全局变量,推荐写法是:
cpp
// .h 中声明
extern int gcount;
cpp
// .cpp 中定义
int gcount;
其中:
cpp
extern int gcount;
表示声明全局变量。
它只是告诉编译器:
- 有一个 int 类型的全局变量 gcount,
- 它在其他 .cpp 文件中定义。
而:
cpp
int gcount;
才是真正定义全局变量。
它会真正创建变量,并分配全局变量空间。
6.3 不要在 .h 文件中直接定义全局变量
不要在 .h 文件中直接写:
cpp
int gcount;
原因是:
.h 文件可能会被多个 .cpp 文件包含。
如果多个 .cpp 文件都包含这个头文件,那么每个 .cpp 文件中都会出现一份:
cpp
int gcount;
这样就可能导致全局变量重复定义。
链接时可能会报错:
multiple definition of gcount
所以全局变量的推荐写法是:
.h 中用 extern 声明
.cpp 中真正定义
6.4 函数的声明与定义
对于函数,推荐写法是:
cpp
// .h 中声明
int TestFunc(int, int x, int y = 10);
cpp
// .cpp 中定义
int TestFunc(int, int x, int y)
{
return 0;
}
函数声明只是告诉编译器:
有一个函数叫 TestFunc。
它的返回值是 int。
它有三个 int 类型参数。
第三个参数默认值是 10。
函数定义才是真正实现函数逻辑。
因为函数定义中有函数体:
{
return 0;
}
6.5 默认参数一般放在函数声明中
多文件编译中,默认参数一般写在 .h 文件的函数声明中:
cpp
int TestFunc(int, int x, int y = 10);
而 .cpp 文件中的函数定义不要重复写默认参数:
cpp
int TestFunc(int, int x, int y)
{
return 0;
}
不要写成:
cpp
int TestFunc(int, int x, int y = 10)
{
return 0;
}
因为默认参数只能设置一次。
在多文件项目中,其他 .cpp 文件通常只能看到 .h 文件,所以默认参数放在 .h 文件中最合适。
6.6 头文件保护的作用
头文件保护是为了防止同一个头文件被重复包含。
常见写法有两种。
第一种是:
#pragma once
特点是:
写法简单
编译效率较高
主流编译器基本支持
第二种是:
#ifndef BASE16_H
#define BASE16_H
// 头文件内容
#endif
特点是:
兼容性更好
传统工程中非常常见
实际项目中,一般二选一即可。
6.7 extern "C" 的作用
对于 C 语言函数,可以这样声明:
cpp
extern "C" void TestC();
也可以声明一组 C 语言函数:
cpp
extern "C"
{
void TestFuncC();
}
它的作用是:
让 C++ 编译器按照 C 语言规则处理函数名。
因为 C++ 支持函数重载,底层会对函数名进行改编。
而 C 语言不支持函数重载,函数名处理方式和 C++ 不一样。
所以当 C++ 调用 C 语言函数时,常常需要使用:
extern "C"
6.8 最终记忆口诀
多文件编译可以记住这几句话:
.h 文件放声明
.cpp 文件放定义
main.cpp 文件负责调用
全局变量记住:
.h 中 extern 声明
.cpp 中真正定义
函数默认参数记住:
默认参数写在声明中
函数定义中不要重复写
头文件保护记住:
#pragma once 简单高效
#ifndef 兼容性更好
C 语言函数记住:
extern "C" 用来告诉 C++:
这个函数按照 C 语言方式链接
所以:
.h + .cpp + main.cpp
这种结构,就是 C++ 工程中最经典、最常见的代码组织方式。