多文件声明定义函数和全局变量)
目录一、本节学习内容概要1.1 为什么要学习多文件编译1.2 多文件编译的核心思想二、.h 文件和 .cpp 文件到底分别写什么2.1 .h 文件头文件通常放声明2.2 .cpp 文件源文件通常放定义2.3 结合你的三个文件来理解1base16.h1base16.cpp3test_muti_file.cpp三、多文件中的全局变量声明、定义与extern3.1 为什么不要在 .h 文件中定义全局变量3.2 extern 的基本语法3.3 正确写法.h 中声明.cpp 中定义3.4 extern 声明和变量定义的区别1extern int gcount 是声明2int gcount 是定义3声明和定义必须配套3.5 extern 声明时不要顺手初始化3.6 声明和定义的区别四、头文件保护、函数声明和默认参数4.1 为什么需要头文件保护4.2 #pragma once 和 #ifndef 的区别1#pragma once2#ifndef4.3 函数声明中可以写默认参数定义中不要重复写五、extern C 与完整多文件调用流程5.1 extern C 是干什么的5.2 单个 C 函数声明5.3 一组 C 函数声明5.4 你的三个文件完整执行流程1base16.h2base16.cpp3test_muti_file.cpp4完整执行逻辑是六、本节总结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; }所以可以理解为声明告诉编译器有这个东西定义真正把这个东西创建出来2.3 结合你的三个文件来理解1base16.hbase16.h是“声明文件”或者“头文件”它主要放函数声明 全局变量声明 类声明 C 语言函数声明 宏定义比如extern int gcount; int TestFunc(int, int x, int y 10);1base16.cppbase16.cpp是“实现文件”或者“定义文件”它主要放全局变量定义 函数定义 类成员函数定义 实际代码逻辑比如int gcount; int TestFunc(int, int x, int y) { return 0; }3test_muti_file.cpptest_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 负责启动程序三、多文件中的全局变量声明、定义与extern3.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;为例。它的意思是我这里只是声明一下有一个 int 类型的全局变量 gcount。这个变量不是在这里创建的它在其他 .cpp 文件中定义。所以extern int gcount;不是定义变量也不是创建变量。它只是告诉编译器有一个全局变量叫 gcount你可以放心使用。至于它真正在哪里创建链接阶段会去其他 .cpp 文件中找。而下面这句int gcount;才是真正定义变量。它的意思是创建一个 int 类型的全局变量 gcount。所以这两句虽然看起来很像但是含义完全不同写法含义是否真正创建变量推荐位置extern int gcount;声明变量否.h文件int gcount;定义变量是.cpp文件可以简单理解为extern 类型 变量名; // 声明告诉编译器有这个变量 类型 变量名; // 定义真正创建这个变量3.3 正确写法.h 中声明.cpp 中定义全局变量的正确写法是.h 文件中声明.cpp 文件中定义其他 .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;它们的区别如下1extern int gcount 是声明extern int gcount;含义是gcount 在别的文件中定义。 我这里只是声明一下。它一般放在.h文件中。例如// base16.h #pragma once extern int gcount;这样其他.cpp文件只要包含base16.h就可以知道gcount这个变量存在。2int 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 声明和定义的区别可以用一句话区分声明告诉编译器有这个东西定义真正创建这个东西例如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如果没有保护头文件内容就会被复制多次。这可能导致重复声明、重复定义等问题。4.2 #pragma once 和 #ifndef 的区别1#pragma once#pragma once含义是保证当前头文件在同一个 .cpp 文件中只被包含一次。优点写法简单 编译效率较高 主流编译器基本都支持比如 MSVC、GCC、Clang 都支持。2#ifndef#ifndef BASE16_H #define BASE16_H // 头文件内容 #endif含义是如果没有定义过BASE16_H就定义它并且展开头文件内容。如果已经定义过了就不再重复展开。优点兼容性更好 可移植性更强 是非常传统的写法实际工程中一般二选一就可以。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的函数声明中。原因是其他.cpp文件调用函数时只能看到.h文件。如果默认参数写在.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 C5.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 你的三个文件完整执行流程现在来看你的三个文件。1base16.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 存在2base16.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 函数3test_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 调用 TestFuncC4完整执行逻辑是1. main.cpp 包含 base16.h2. 编译器知道 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 全局变量的声明与定义对于全局变量推荐写法是// .h 中声明 extern int gcount;// .cpp 中定义 int gcount;其中extern int gcount;表示声明全局变量。它只是告诉编译器有一个 int 类型的全局变量 gcount它在其他 .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 C6.8 最终记忆口诀多文件编译可以记住这几句话.h 文件放声明 .cpp 文件放定义 main.cpp 文件负责调用全局变量记住.h 中 extern 声明 .cpp 中真正定义函数默认参数记住默认参数写在声明中 函数定义中不要重复写头文件保护记住#pragma once 简单高效 #ifndef 兼容性更好C 语言函数记住extern C 用来告诉 C 这个函数按照 C 语言方式链接所以.h .cpp main.cpp这种结构就是 C 工程中最经典、最常见的代码组织方式。