深入理解c++中.h/.cpp文件,extern关键字,typedef,using关键字
源文件是代码的“具体实现”。它包含了所有在头文件中声明的实体所对应的完整定义。源文件中应该包含什么?头文件的包含:#include "my_header.h",以获取所有声明。函数的完整定义类成员函数的完整实现全局变量的定义(这是该变量唯一被分配内存的地方)为什么要这么做?每个 .cpp 文件都被编译器单独编译成一个目标文件(.o 或 .obj)。这些目标文件包含了实际的机器码。
目录
由于对于.h和.cpp文件一直有所模糊,所以特地总结一下。
C++ 模块化编程的精髓,声明与定义分离。
在 C++ 中,声明(Declaration)和定义(Definition)是两个不同的概念:
- 声明:告诉编译器某个实体(如变量、函数、类)的名称和类型,但不分配内存。它像是一份“契约”或“目录”,让编译器知道某个东西是存在的。一个实体可以被声明多次
- 定义:为实体分配内存并提供其完整实现。一个实体只能被定义一次。
.h 和 .cpp 文件正是为了实现这种分离而诞生的。
.h 文件(头文件):声明的集合
头文件的主要作用是作为代码的“接口”或“目录”。它包含了其他文件需要了解的所有声明,但不包含具体的实现细节。
.h文件中坚决不能包含普通函数的定义,全局变量的定义!这样很可能出现重复定义!最好只包含对应的声明(普通函数声明自带extern,全局变量的声明需要显示添加extern声明)。
头文件中应该包含什么?
函数声明(原型):int calculate_sum(int a, int b);
类和结构体的定义:class MyClass { ... }; (注意:通常只包含成员函数的声明,不包含实现)
extern 全局变量的声明:extern int g_app_version;
typedef 和 using 类型别名:using StringVector = std::vector<std::string>;
宏定义(#define)
为什么要这么做?
当一个源文件 (.cpp) #include 一个头文件时,预处理器会把头文件的内容原封不动地复制粘贴到这个源文件中。这让编译器能够检查语法、函数调用、类型匹配等,而无需知道函数的具体实现。
为了防止头文件被多次包含而引发重复定义错误,头文件通常会使用 宏定义(#ifndef)来防止重复包含,这被称为“头文件保护”。
虽然说对于类来说可以在.h中对类的成员函数进行实现,不会造成重复定义,但是也不推荐,这样比较混乱,还是建议.h中进行声明,.cpp中进行实现。
除非如果函数非常短小,并且你希望编译器将其作为内联函数处理,以减少函数调用的开销,可以将其定义在头文件中。简单的 getter 和 setter 函数就很适合这么做(但我更喜欢将所有类成员函数放到.cpp中进行实现,感觉比较清晰)
.cpp 文件(源文件):定义的集合
源文件是代码的“具体实现”。它包含了所有在头文件中声明的实体所对应的完整定义。
源文件中应该包含什么?
头文件的包含:#include "my_header.h",以获取所有声明。
函数的完整定义:int calculate_sum(int a, int b) { return a + b; }
类成员函数的完整实现:void MyClass::doSomething() { ... }
全局变量的定义:int g_app_version = 1;(这是该变量唯一被分配内存的地方)
为什么要这么做?
每个 .cpp 文件都被编译器单独编译成一个目标文件(.o 或 .obj)。这些目标文件包含了实际的机器码。最后,链接器(Linker)会将所有目标文件和所需的库文件组合在一起,解析所有函数调用和变量引用,生成最终的可执行程序。这种分离编译的方式可以极大地加快大型项目的编译速度。
extern关键字的介绍
extern 是 C 和 C++ 语言中的一个关键字,它的主要作用是声明一个变量或函数是在其他文件中定义的。
简单来说,它的核心思想是:“我在这个文件里要用一个东西,但它并不在这个文件里定义,它的定义在别处。”
通常来说extern只有两个作用:
- 声明外部变量(全局变量)(extern最常见用法)
- 声明外部函数(全局函数)(通常省略extern)
声明全局变量(extern最常见用法)
当你在一个文件中想要使用另一个文件(比如 file2.c)中定义的全局变量时,就需要使用 extern 来声明它。这告诉编译器,这个变量(例如 int globalVar;)不是在这个文件中创建的,而是在其他地方,你只需要知道它的存在和类型即可。
当你在多个 .cpp 文件中使用全局变量时,最推荐的方式就是在其中一个 .cpp 文件中进行定义,然后在其他所有需要使用它的 .cpp 文件中,通过 extern 声明来引用它。
具体的实现方式最好是:extern 声明通常是放在一个头文件(.h)中,然后将这个头文件包含到需要引用的 .cpp 文件里。
正确的实践流程(推荐的全局变量声明方式)
假设你需要在一个项目中共享一个名为 g_counter 的全局变量。
1. 创建一个头文件 globals.h 在这个头文件中,使用 extern 来声明这个全局变量。
// globals.h
#ifndef GLOBALS_H
#define GLOBALS_H
extern int g_counter; // 声明
#endif
2. 创建一个源文件 globals.cpp 在这个源文件中,对全局变量进行定义和初始化。
// globals.cpp
#include "globals.h"
int g_counter = 0; // 定义,只在这一处定义
3. 在其他 .cpp 文件中使用它 在任何需要使用 g_counter 的 .cpp 文件中,只需包含 globals.h 头文件。
// main.cpp
#include <iostream>
#include "globals.h"
int main() {
g_counter = 10;
std::cout << g_counter << std::endl;
return 0;
}
在头文件中定义全局变量(禁止)
最常见也最危险的错误—— 在头文件中定义全局变量
“多重定义错误”(Multiple Definition Error)发生在程序的链接阶段。它的本质是:链接器试图将多个独立编译的目标文件(.o 或 .obj)合并成一个可执行文件时,发现同一个全局变量或非内联函数在多个目标文件中都被定义了。链接器不知道该使用哪一个定义,所以报错。
为什么在头文件中定义全局变量会导致这个错误?
让我们通过一个具体的例子,模拟编译和链接的完整过程,来详细解释这个问题。
假设你有以下三个文件:
1. common.h (有问题的头文件)
// common.h
#ifndef COMMON_H
#define COMMON_H
// 错误示范:在头文件中定义全局变量
int g_counter = 0;
#endif
2. file1.cpp (使用该头文件的源文件)
#include "common.h"
void increment_counter() {
g_counter++;
}
file2.cpp (也使用该头文件的另一个源文件)
#include "common.h"
void decrement_counter() {
g_counter--;
}
当多个.cpp源文件包含同一个头文件时,每个源文件都会生成一个g_counter的独立副本。链接器在将这些目标文件(.o)组合时,会发现g_counter被定义了多次,从而引发多重定义错误,导致编译失败。
不推荐的全局变量声明方式
在不使用头文件的情况下,直接在每个 .cpp 文件里手动写 extern 声明全局变量这种做法在实际项目中非常不推荐。
在一个地方定义了全局变量,比如 file.cpp
// file.cpp
int g_counter = 0; // 全局变量的定义
在其他需要使用它的地方,不通过头文件,而是手动写 extern 声明
// other_file1.cpp
extern int g_counter; // 声明
void do_something() {
g_counter++;
}
// other_file2.cpp
extern int g_counter; // 再次声明
void do_something_else() {
g_counter--;
}
尽管这种做法在语法上是正确的,且包含了定义,但它违背了现代 C++ 的工程实践原则。主要问题在于没有统一的声明来源。
- 代码重复与冗余:在一个大型项目中,如果一个全局变量被几十甚至上百个文件使用,那么每个文件都需要手动重复编写 extern int g_counter; 这行代码。这不仅浪费时间,也使得代码变得冗长。
- 风险:如果 g_counter 的类型或名称需要修改,你必须手动去寻找每一个使用了它的文件,并一一修改 extern 声明。这种方式在大型项目中极易出错且难以维护。
尽量避免使用全局变量
虽然 extern 是解决全局变量跨文件共享的正确方式,但现代 C++ 编程强烈建议在大多数情况下避免使用全局变量。
因为全局变量会增加代码的耦合性,使程序状态难以追踪,降低可维护性和可测试性。通常,更好的做法是将数据封装在类中,并通过函数参数或返回值的形式来传递数据。只有在少数特殊场景(例如,需要一个在整个应用程序生命周期中唯一的配置对象或单例模式)下,才可能考虑使用全局变量。
声明全局函数(通常省略extern)
函数的默认声明就是 extern 的,所以通常情况下你不需要显式地写它。例如,int add(int a, int b); 这条函数声明就隐式地包含了 extern 的含义。
但是,如果你需要强调某个函数是在外部定义的,或者为了代码风格的一致性,你也可以显式地使用 extern
typedef 关键字
typedef 是 C 和 C++ 语言中的一个关键字,它的主要作用是为已有的类型创建一个新的别名。这并不会创建新的类型,而是让你能用一个更具可读性或更方便的名字来引用一个现有的类型。
typedef 的常见用法如下:
- 为复杂类型创建别名
- 为函数指针创建别名
- 增强代码的语义性
为复杂类型创建别名
typedef 在为结构体(struct)、联合体(union)和枚举(enum)创建别名时特别有用,可以简化代码,使其更易于阅读和编写。
// 没有使用 typedef
struct Student {
int id;
char name[50];
};
struct Student s1; // 必须写 struct Student
// 使用 typedef
typedef struct Student {
int id;
char name[50];
} StudentType;
StudentType s2; // 现在,只需写 StudentType 即可
在c++中使用结构体声明对象的时候,可以不加struct,但是在c语言中必须加struct,这时候就可以为结构体取一个别名,简化我们的定义操作,为结构体类型起别名在c语言中非常常见。
为函数指针创建别名
这是 typedef 最强大的用法之一,它可以让复杂的函数指针声明变得非常简洁和清晰。
// 没有使用 typedef 的函数指针声明,可读性较差
int (*p_func)(int, int);
// 使用 typedef
typedef int (*CalculatorFunc)(int, int);
CalculatorFunc add_ptr; // 现在,你可以用 CalculatorFunc 来声明函数指针
这里,CalculatorFunc 代表了“一个接受两个 int 类型参数并返回一个 int 1类型”的函数指针。
增强代码语义性
使用 typedef 可以让类型名称带有特定的含义,使代码更具自文档化能力,让读者一眼就知道变量的用途。
typedef unsigned int StudentID;
typedef unsigned int FileSize;
typedef long long TimeStamp;
// 比直接使用 unsigned int 和 long long 更能表达变量的用途
StudentID current_student = 101;
FileSize file_size_in_bytes = 2048;
TimeStamp login_time = 1672531200;
看到 StudentID,你马上就知道这是一个学生ID,而不是一个普通的无符号整数。
using 关键字(c++推荐)
在 C++11 及更高版本中,using 关键字提供了与 typedef 类似的功能,并且在处理模板别名时更加强大和灵活。因此,在现代 C++ 项目中,通常更推荐使用 using。
using 关键字创建类型别名主要有两个明显的优势:
1. 语法更清晰、可读性更高
using 的语法格式是 using 新名字 = 原始类型;,这看起来更直观,更像是一个赋值操作,让人一眼就能明白 新名字 是 原始类型 的一个别名。
相比之下,typedef 的语法是 typedef 原始类型 新名字;,这可能会让人感到困惑,特别是对于复杂的类型声明。
// 使用 typedef
typedef std::map<std::string, int> MyMap;
MyMap data;
// 使用 using (更直观)
using MyMap = std::map<std::string, int>;
MyMap data;
2. 可以用于模板(Template)
这是 using 最大的优势,也是 typedef 无法做到的。using 关键字可以与模板一起使用,创建模板别名(template alias),这在泛型编程中非常有用。
typedef 由于其语法限制,无法在模板中进行别名化。
假设你想为 std::vector 创建一个别名,使其元素类型可以自定义。
// 使用 using,可以轻松实现模板别名
template<typename T>
using MyVector = std::vector<T>;
MyVector<int> int_vec;
MyVector<double> double_vec;
使用 typedef 无法实现上述功能。
总结来说,在纯 C++11 及更高版本的项目中,using 应该成为你的首选。它的语法更清晰,而且支持强大的模板别名功能,这让它在现代 C++ 编程中更具优势。
然而,如果你需要与 C 语言代码进行交互,或者维护一个大量使用 typedef 的传统代码库,那么 typedef 仍然是不可或缺的。
更多推荐
所有评论(0)