头文件保护宏
我们需要在.cpp中使用#include引入需要的.h文件,但是如果出现了头文件循环引入的的问题,就会出现“重复定义”的错误。例如:
a.h:
#include "b.h"
.....
b.h
#include "a.h"
........
main.cpp
#include "a.h"
那么在这种情况下,在main.cpp这个编译单元中,就会出现 变量/类型/函数等重定义的错误.
这个问题有两种常见的解决方式:
- 头文件保护宏
- #pragma once
头文件保护宏:
在.h头文件加上如下代码即可
#ifndef INCLUDE_XXXX_H
#define INCLUDE_XXXX_H
.......................
#endif // INCLUDE_XXXX_H
原理:当第一次引入该头文件时,ifndef 会判断后面的宏是否存在,如果不存在则继续执行,第一次引入时, INCLUDE_XXXX_H显然是不存在的,所以继续执行并通过 define定义该宏。后续再次引入该头文件时,就会因为INCLUDE_XXXX_H 存在而不继续往下走,所以实现每个头文件只引入一次的目的.
#pragma once
.h头文件顶部添加如下即可
#pragma once
原理:#pragma once是一个编译器指令,它在编译阶段生效,当第一次引入该头文件时,他会将该头文件标记为 “已处理”的状态,后续再次引入该头文件时,编译器则会忽略被标记为 “已处理” 状态的头文件,直到处理完所有的包含请求之后,“已处理”状态才会被解除,以此避免循环引用,实现每个头文件只引入一次.
PS:
- 某些版本较老的编译器不支持该命令
- 如果同一个文件被拷贝成两份,并且放在不同的路径下被引入,#pragma once无法防止多重定义.
为什么要在.h文件中声明,并在.cpp文件中定义
在一个C++ 工程中每一个 .cpp文件都可以看做成一个 “编译单元”,在汇编阶段时,会为每一个.cpp文件,生成对应的目标文件(.o或.obj),目标文件中会包含.cpp文件符号的定义。在.obj文件中会有一个符号表,符号表记录着文件中定义和引用的符号,包括变量,函数等。符号表中会记录符号的名称,位置,类型等信息。 在链接阶段时,会把若干个.obj文件和库文件,链接成最终的可执行文件。如果链接器发现,同一个符号在多个符号表中都有定义,就会爆出错误:“找到一个或多个重定义的符号”
a.h
int function()
{
}
1.cpp
include "a.h"
2.cpp
include "a.h"
这种情况就会导致出现链接错误,如果1.cpp和2.cpp在同一个cpp工程中,那么1.cpp和2.cpp就会同时拥有 a.h中 function函数的符号定义,在链接阶段就会爆出错误,所以我们一般不直接在.h文件中定义实现.
另外,头文件保护宏仅能保护同一个.h文件不被一个.cpp文件多次引入,并不代表一个.h文件不能被多个.cpp文件引入。
但是如果我就是偏要在.h中定义函数的实现,并且让多个.cpp使用呢,有两个关键字可以解决:
static 关键字
声明内部链接,在变量函数前加上static关键字,即可标记该变量/函数为内部链接,内部链接的符号仅在声明它的编译单元内有效,不会影响其他编译单元(相当于是在每一个cpp文件中都有一个独立且不同的变量/函数),所以链接阶段不会产生错误。
但是也有副作用,即每一个编译单元中都有有一个 被static关键字声明 的函数/变量的副本,可执行程序大小会变大.
inline关键字
声明为内联函数,(在高版本可以声明内联变量),在将函数声明为内联函数之后,编译器会将函数的实现嵌入到调用点,跟传统的函数调用不同。内联函数定义在多个编译单元中是允许的.
误区:
我们在.h中定义变量/函数,然后再被多个.cpp引入,都会导致链接错误,但是为什么定义类不会导致链接错误呢,而且如果在类中直接实现函数体,也不会产生链接错误,这是为什么?
a.h
class A
{
void myfunction()
{
std::cout << "hello world" << std::endl;
}
};
如果在类的内部定义函数实现,就不会出现链接错误。
首先,类仅仅是一个类型描述,不涉及内存的分配与符号的产生,所以就更不会出现后续多个编译单元中符号表中的符号重复的现象。只有当类被实例化之后,才会分配内存和产生符号。
第二,凡是在类的内部直接实现的函数,都是内联函数,也就是说类的内部会将所有已经实现函数体的函数视为内联函数,相当于加上了inline的关键字,所以也不会产生上述说的链接错误的问题.
类和类的相互使用
在实际开发中经常会遇到两个类的相互使用问题,这也是很多C++ 初学者很容易遇到的问题
a.h:
#ifndef INCLUDE_A_H
#define INCLUDE_A_H
#include "b.h”
class A
{
B b;
};
#endif
b.h:
#ifndef INCLUDE_B_H
#define INCLUDE_B_H
#include "a.h"
class B
{
A a;
};
#endif
上述代码即是这种情况,类A中使用了类B,类B中又使用了类A,并且没有做其他处理。
这样肯定是错的。
解决方案就是使用前向声明:
A.h:
#ifndef INCLUDE_A_H
#define INCLUDE_A_H
class B;
class A
{
B b;
};
#endif
B.h:
#ifndef INCLUDE_B_H
#define INCLUDE_B_H
class A;
class B
{
A a;
};
#endif
main.cpp
#include "a.h”
#include "b.h”
......
前向声明只是告诉编译器有这个类存在,这样在类A中就可以使用类B, 类B中可以使用类A
另外,前向声明是不会和真正的 class A 或 class B 起冲突的.
global.h 与 extern 关键字
global.h:
当有一组头文件需要被多个其他文件引用时,我们可以把这组头文件,统一放入一个global.h头文件中(名字不固定,global表示全局),接下来凡是需要引入这组头文件的文件,只需要直接引入global.h即可,这样的作法较为简洁且美观,而且当需要改动时,也只需要修改global.h中的内容即可,不需要挨个文件的修改,较为方便。
extern关键字:
如果我们想在一个文件中使用某个头文件中定义的东西,我们直接引入这个头文件即可,但是如果我们想使用一个.cpp中定义的东西呢,该如何解决呢.
在一些函数/变量之前加上 extern关键字声明一下即可使用,例如
a.cpp
void myFunction() {
std::cout << "Hello from myFunction!" << std::endl;
}
b.cpp
extern void myFunction();
int main() {
myFunction(); // 调用在 a.cpp 中定义的函数
return 0;
}
想用extern关键字来声明变量也是同理.
原理:extern关键字是用来告诉编译器该函数/变量是在其他文件中定义的(其他的编译单元),这是一个外部定义的符号,所以编译器不会去查找这个变量/函数的的定义,只需要知道它的类型和名称即可
然后在链接阶段,所有的.cpp都已经被编译了.o或是.obj文件,链接器会将这些目标文件链接起来组成新的可执行程序/库文件,这个阶段链接器就会去其他文件中查找extern关键字声明的变量/函数的定义,然后将这些目标文件正确的链接起来.
误区:如果在多个.cpp文件中,有好几个同名的全局变量,那么这种情况下,extern关键字具体获取的是哪一个.cpp文件中的变量呢?
回答:我们在前文中提到过,CPP是不允许在多个编译单元中,出现名称一致的全局变量的,这样本身就会导致链接错误。
小技巧:如果我们在.cpp中定义了一个全局变量,那么我们可以在global.h中去extern这个变量,然后在让其他的.cpp文件去引入global.h,这样这个全局变量就可以优雅的被其他.cpp使用。例如我们在main.cpp中定义了一个全局变量,也想让其他编译单元使用,就可以这么做.
常见的编译错误
- 头文件的循环引入 -> "xx类型重定义”
- 在头文件中定义变量/函数然后又被多个.cpp引入 -> “找到一个或多个重定义的符号”
- 在某一个.cpp文件中重复定义变函数 -> “xx函数已有主体” (同一个编译单元内也不允许有重复的符号出现)