[C++开发工具与项目开发] 关于.h头文件的常见问题 -新手必看

头文件保护宏

我们需要在.cpp中使用#include引入需要的.h文件,但是如果出现了头文件循环引入的的问题,就会出现“重复定义”的错误。例如:

a.h:

#include "b.h"
.....

b.h

#include "a.h"
........

main.cpp

#include "a.h"

那么在这种情况下,在main.cpp这个编译单元中,就会出现 变量/类型/函数等重定义的错误.

这个问题有两种常见的解决方式:

  1. 头文件保护宏
  2. #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:

  1. 某些版本较老的编译器不支持该命令
  2. 如果同一个文件被拷贝成两份,并且放在不同的路径下被引入,#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中定义了一个全局变量,也想让其他编译单元使用,就可以这么做.

常见的编译错误

  1. 头文件的循环引入 -> "xx类型重定义”
  2. 在头文件中定义变量/函数然后又被多个.cpp引入 -> “找到一个或多个重定义的符号”
  3. 在某一个.cpp文件中重复定义变函数 -> “xx函数已有主体” (同一个编译单元内也不允许有重复的符号出现)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值