当前位置: 首页 > news >正文

C++零基础到工程实战(5.2.8)多文件声明定义函数和全局变量

目录

一、本节学习内容概要

1.1 为什么要学习多文件编译?

1.2 多文件编译的核心思想

二、.h 文件和 .cpp 文件到底分别写什么?

2.1 .h 文件:头文件通常放声明

2.2 .cpp 文件:源文件通常放定义

2.3 结合你的三个文件来理解

(1)base16.h

(1)base16.cpp

(3)test_muti_file.cpp

三、多文件中的全局变量声明、定义与extern

3.1 为什么不要在 .h 文件中定义全局变量?

3.2 extern 的基本语法

3.3 正确写法:.h 中声明,.cpp 中定义

3.4 extern 声明和变量定义的区别

(1)extern int gcount 是声明

(2)int gcount 是定义

(3)声明和定义必须配套

3.5 extern 声明时不要顺手初始化

3.6 声明和定义的区别

四、头文件保护、函数声明和默认参数

4.1 为什么需要头文件保护?

4.2 #pragma once 和 #ifndef 的区别

(1)#pragma once

(2)#ifndef

4.3 函数声明中可以写默认参数,定义中不要重复写

五、extern "C" 与完整多文件调用流程

5.1 extern "C" 是干什么的?

5.2 单个 C 函数声明

5.3 一组 C 函数声明

5.4 你的三个文件完整执行流程

(1)base16.h:

(2)base16.cpp:

(3)test_muti_file.cpp:

(4)完整执行逻辑是:

六、本节总结

6.1 多文件编译的核心思想

6.2 全局变量的声明与定义

6.3 不要在 .h 文件中直接定义全局变量

6.4 函数的声明与定义

6.5 默认参数一般放在函数声明中

6.6 头文件保护的作用

6.7 extern "C" 的作用

6.8 最终记忆口诀


一、本节学习内容概要

1.1 为什么要学习多文件编译?

前面写代码时,很多示例都放在一个.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中,有这样的代码:

#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;

注意,这里大部分内容都是声明,不是真正定义。

比如:

extern int gcount;

这句话不是创建一个新的全局变量,而是声明:

别的地方已经有一个 int 类型的全局变量 gcount,你们可以使用它。

再比如:

int TestFunc(int, int x, int y = 10);

这句话是函数声明。

它只是告诉编译器:

有一个函数叫 TestFunc; 返回值是 int; 参数是 int、int、int; 第三个参数默认值是 10。

但是它没有函数体,所以它还不是函数定义。


2.2 .cpp 文件:源文件通常放定义

base16.cpp中,有这样的代码:

#include "base16.h" int gcount; int TestFunc(int, int x, int y) { return 0; } extern "C" void TestC() { } extern "C" { void TestFuncC() { } }

这里就是真正的定义。

比如:

int gcount;

这句话会真正创建一个全局变量gcount

也就是说,它会在全局区分配空间。

再比如:

int TestFunc(int, int x, int y) { return 0; }

这是真正的函数定义。

因为它有函数体:

{ return 0; }

所以可以理解为:

  1. 声明:告诉编译器有这个东西
  2. 定义:真正把这个东西创建出来

2.3 结合你的三个文件来理解

(1)base16.h

base16.h

“声明文件”或者“头文件”

它主要放:

函数声明 全局变量声明 类声明 C 语言函数声明 宏定义

比如:

extern int gcount; int TestFunc(int, int x, int y = 10);

(1)base16.cpp

base16.cpp

“实现文件”或者“定义文件”

它主要放:

全局变量定义 函数定义 类成员函数定义 实际代码逻辑

比如:

int gcount; int TestFunc(int, int x, int y) { return 0; }

(3)test_muti_file.cpp

test_muti_file.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 文件中定义全局变量?

//不要在.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

它的基本语法是:

extern 类型 变量名;

例如:

extern int gcount; extern double gspeed; extern float gradius;

这里以:

extern int gcount;

为例。

它的意思是:

  1. 我这里只是声明一下,有一个 int 类型的全局变量 gcount。
  2. 这个变量不是在这里创建的,它在其他 .cpp 文件中定义。

所以:

extern int gcount;

不是定义变量,也不是创建变量。

它只是告诉编译器:

  1. 有一个全局变量叫 gcount,你可以放心使用。
  2. 至于它真正在哪里创建,链接阶段会去其他 .cpp 文件中找。

而下面这句:

int gcount;

才是真正定义变量。

它的意思是:

创建一个 int 类型的全局变量 gcount。

所以这两句虽然看起来很像,但是含义完全不同:

写法含义是否真正创建变量推荐位置
extern int gcount;声明变量.h文件
int gcount;定义变量.cpp文件

可以简单理解为:

extern 类型 变量名; // 声明:告诉编译器有这个变量 类型 变量名; // 定义:真正创建这个变量

3.3 正确写法:.h 中声明,.cpp 中定义

全局变量的正确写法是:

  1. .h 文件中声明
  2. .cpp 文件中定义
  3. 其他 .cpp 文件包含头文件后使用

.h文件中声明:

extern int gcount;

.cpp文件中定义:

int gcount;

这两句意思完全不一样。

extern int gcount;的意思是:

我只是声明一下,gcount 在别的地方定义。

int gcount;的意思是:

我真正创建一个全局变量 gcount。

所以完整写法是:

// base16.h #pragma once extern int gcount;
// base16.cpp #include "base16.h" int gcount;
// main.cpp #include <iostream> #include "base16.h" using namespace std; int main() { gcount++; cout << gcount << endl; return 0; }

这样写时,程序中只有一个真正的gcount

其他.cpp文件只是通过头文件知道:有一个 gcount 可以用。


3.4 extern 声明和变量定义的区别

多文件全局变量中,最容易混淆的就是这两句:

extern int gcount;

和:

int gcount;

它们的区别如下:

(1)extern int gcount 是声明

extern int gcount;

含义是:

gcount 在别的文件中定义。 我这里只是声明一下。

它一般放在.h文件中。

例如:

// base16.h #pragma once extern int gcount;

这样其他.cpp文件只要包含base16.h,就可以知道gcount这个变量存在。


(2)int gcount 是定义

int gcount;

含义是:

真正创建一个全局变量 gcount。

它一般放在某一个.cpp文件中。

例如:

// base16.cpp #include "base16.h" int gcount;

一个全局变量在整个程序中通常只应该有一次定义。

如果多个.cpp文件中都写了:

int gcount;

容易造成重复定义问题。


(3)声明和定义必须配套

如果只有声明:

// 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 声明时不要顺手初始化

有些初学者可能会写成:

extern int gcount = 10;

这句代码虽然前面有extern,但是因为后面带了初始化:

= 10

所以它已经不是普通声明了,而是变成了定义。

也就是说:

extern int gcount = 10;

会真正创建变量。

因此不要把这种写法放在.h文件中。

推荐写法是:

// base16.h #pragma once extern int gcount;
// base16.cpp #include "base16.h" int gcount = 10;

这样结构最清晰。

.h文件负责声明:

extern int gcount;

.cpp文件负责定义和初始化:

int gcount = 10;

3.6 声明和定义的区别

可以用一句话区分:

  1. 声明:告诉编译器有这个东西
  2. 定义:真正创建这个东西

例如:

extern int gcount;

这是声明。

int gcount;

这是定义。

再比如函数:

int TestFunc(int, int x, int y = 10);

这是函数声明。

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中写了:

#pragma once #ifndef BASE16_H #define BASE16_H // 头文件内容 #endif

它们的作用是:

防止同一个头文件在同一个.cpp文件中被重复包含。

例如:

#include "base16.h" #include "base16.h" #include "base16.h"
  1. 如果没有保护,头文件内容就会被复制多次。
  2. 这可能导致重复声明、重复定义等问题。

4.2 #pragma once 和 #ifndef 的区别

(1)#pragma once

#pragma once

含义是:

保证当前头文件在同一个 .cpp 文件中只被包含一次。

优点:

写法简单 编译效率较高 主流编译器基本都支持

比如 MSVC、GCC、Clang 都支持。


(2)#ifndef

#ifndef BASE16_H #define BASE16_H // 头文件内容 #endif

含义是:

  1. 如果没有定义过BASE16_H,就定义它,并且展开头文件内容。
  2. 如果已经定义过了,就不再重复展开。

优点:

兼容性更好 可移植性更强 是非常传统的写法

实际工程中,一般二选一就可以。


4.3 函数声明中可以写默认参数,定义中不要重复写

base16.h中有:

int TestFunc(int, int x, int y = 10);

这里给第三个参数设置了默认值:

int y = 10

所以调用时可以这样写:

TestFunc(1, 2);

虽然只传了两个参数,但是第三个参数会自动使用默认值10

相当于:

TestFunc(1, 2, 10);

但是在base16.cpp中,函数定义不能再重复设置默认参数。

正确写法是:

int TestFunc(int, int x, int y) { return 0; }

不要写成:

int TestFunc(int, int x, int y = 10) { return 0; }

因为默认参数只能在函数声明或者函数定义的某一个地方设置一次。

在多文件项目中,通常把默认参数放在.h的函数声明中。

原因是:

  1. 其他.cpp文件调用函数时,只能看到.h文件。
  2. 如果默认参数写在.cpp文件中,其他文件看不到这个默认值,就无法正确使用。

所以推荐写法是:

// base16.h int TestFunc(int, int x, int y = 10);
// base16.cpp int TestFunc(int, int x, int y) { return 0; }

五、extern "C" 与完整多文件调用流程

5.1 extern "C" 是干什么的?

extern "C" void TestC();

还有:

extern "C" { void TestFuncC(); }

extern "C"的作用是:

让 C++ 编译器按照 C 语言的方式处理函数名。

为什么需要这个东西?

因为 C++ 支持函数重载。

比如:

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 语言函数,可以这样写:

extern "C" void TestC();

对应定义可以这样写:

extern "C" void TestC() { }

然后在main中调用:

TestC();

5.3 一组 C 函数声明

如果有多个 C 语言函数,可以用大括号统一包起来:

extern "C" { void TestFuncC(); void TestFuncC2(); void TestFuncC3(); }

对应定义也可以这样写:

extern "C" { void TestFuncC() { } void TestFuncC2() { } void TestFuncC3() { } }

5.4 你的三个文件完整执行流程

现在来看你的三个文件。

(1)base16.h

#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

#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 多文件编译的核心思想

多文件编译最核心的思想是:

  1. .h 文件放声明
  2. .cpp 文件放定义
  3. main.cpp 文件负责调用

也可以理解为:

  1. .h 文件:告诉别人我有什么
  2. .cpp 文件:真正实现这些东西
  3. main.cpp 文件:使用这些东西

6.2 全局变量的声明与定义

对于全局变量,推荐写法是:

// .h 中声明 extern int gcount;
// .cpp 中定义 int gcount;

其中:

extern int gcount;

表示声明全局变量。

它只是告诉编译器:

  1. 有一个 int 类型的全局变量 gcount,
  2. 它在其他 .cpp 文件中定义。

而:

int gcount;

才是真正定义全局变量

它会真正创建变量,并分配全局变量空间。


6.3 不要在 .h 文件中直接定义全局变量

不要在.h文件中直接写:

int gcount;

原因是:

.h文件可能会被多个.cpp文件包含。

如果多个.cpp文件都包含这个头文件,那么每个.cpp文件中都会出现一份:

int gcount;

这样就可能导致全局变量重复定义。

链接时可能会报错:

multiple definition of gcount

所以全局变量的推荐写法是:

.h 中用 extern 声明 .cpp 中真正定义

6.4 函数的声明与定义

对于函数,推荐写法是:

// .h 中声明 int TestFunc(int, int x, int y = 10);
// .cpp 中定义 int TestFunc(int, int x, int y) { return 0; }

函数声明只是告诉编译器:

有一个函数叫 TestFunc。 它的返回值是 int。 它有三个 int 类型参数。 第三个参数默认值是 10。

函数定义才是真正实现函数逻辑。

因为函数定义中有函数体:

{ return 0; }

6.5 默认参数一般放在函数声明中

多文件编译中,默认参数一般写在.h文件的函数声明中:

int TestFunc(int, int x, int y = 10);

.cpp文件中的函数定义不要重复写默认参数:

int TestFunc(int, int x, int y) { return 0; }

不要写成:

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 语言函数,可以这样声明:

extern "C" void TestC();

也可以声明一组 C 语言函数:

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++ 工程中最经典、最常见的代码组织方式。

http://www.zskr.cn/news/1445026.html

相关文章:

  • Doris Array类型避坑指南:别再乱用Duplicate模型了,这些场景用Unique模型更香
  • AI病历写作中的语法风险:患者主体消失与临床责任模糊化
  • 无创血糖监测技术:从泪液传感原理到智能隐形眼镜应用
  • 游泳训练游戏化:基于传感器与实时反馈的智能训练系统设计
  • 别再折腾官方教程了!手把手教你用Ubuntu 22.04 + ROS2 Humble搞定YDlidar雷达驱动(附常见报错解决)
  • 2026年服务优质的大金中央空调/中央空调新风一体优质推荐 - 行业平台推荐
  • 拆解软件工程六大神话:从布鲁克斯法则到技术债务管理
  • 华为“韬(T)定律”的短期、中期与长期!
  • 如何高效构建模块化3D高斯溅射工作流?Gaustudio实战深度解析
  • 告别ViT的平方复杂度!手把手带你用VMamba-Tiny复现ImageNet分类实验(附代码)
  • Qwen3-14B企业级部署方案:高可用架构与负载均衡配置
  • 告别纯命令行:用Blue Kenue可视化你的TELEMAC二维水力模型结果(附动画制作)
  • 2026年上门服务中央空调/中央空调新风一体/家用中央空调/中央空调一拖四热销推荐 - 品牌宣传支持者
  • 如何快速备份QQ空间:GetQzonehistory一键导出终极指南
  • 别再直接删文件了!Docker镜像‘污染’导致--gpus all失败的根治方案
  • 大角鹿防水涂料怎么样?大角鹿防水效果好吗?.2026大角鹿辅材售后详解 - 栗子测评
  • LongCat-Flash-Lite-FP8安全与部署注意事项:MIT许可证详解与使用限制
  • 如何将Multilingual-MiniLM-L12-H384集成到现有系统中:兼容性指南
  • 2026年口碑好的2PE防腐钢管/重庆环氧树脂防腐钢管实力工厂推荐 - 行业平台推荐
  • OpenCode LSP集成架构解析:构建高效终端开发环境
  • 数字媒体真实性验证实战指南:从元数据到AI检测的完整工具箱
  • PyTorch-NPU/baichuan2_7b_base模型蒸馏技术:如何从小模型获得大模型性能
  • Campus-iMaoTai:基于Spring Boot的茅台预约自动化系统架构设计与实现
  • DeepSeek Coder 33B Instruct常见问题解决:从安装错误到推理异常的完整排查指南
  • 微软翻译技术演进:从统计机器翻译到深度神经网络的服务化实践
  • SPACER求解器:Z3中模型检测与定理证明融合的程序验证引擎
  • 微信小程序原生2048游戏源码,带完整页面+逻辑+资源,开箱即调
  • 2026年知名的广东七字执手/平开窗执手/执手批量采购厂家推荐 - 行业平台推荐
  • 从SPI时序到数据解析:深入理解AS5047P磁性编码器的通信协议
  • 告别手动剪辑:5分钟学会用AI智能剪辑你的视频内容