C++编译中头文件重复包含问题

#Cpp #编译 #头文件保护

在C++开发过程中,重定义错误是一个常见的问题。如果存在一个头文件被多个文件引用,而头文件没有防重复包含保护的情况,就会引发重定义的问题,表现如下:

1
2
3
4
5
6
7
8
9
[build] C:/Users/ZeroHzzzz/Desktop/QT/Player/image.h:106:7: error: redefinition of 'class ImageHandler'
[build] class ImageHandler { [build] ^~~~~~~~~~~~
[build] In file included from C:/Users/ZeroHzzzz/Desktop/QT/Player/datareader.h:3:0,
[build] from C:\Users\ZeroHzzzz\Desktop\QT\Player\mainwindow.h:4, [build] from C:\Users\ZeroHzzzz\Desktop\QT\Player\mainwindow.cpp:1:
[build] C:/Users/ZeroHzzzz/Desktop/QT/Player/image.h:106:7: note: previous definition of 'class ImageHandler'
[build] class ImageHandler {
[build] ^~~~~~~~~~~~
[build] In file included from C:/Users/ZeroHzzzz/Desktop/QT/Player/datareader.h:14:0,
[build] from C:\Users\ZeroHzzzz\Desktop\QT\Player\datareader.cpp:1:

解决方法

使用预处理指令 #ifndef#define#endif

image.h头文件的开头和结尾添加如下的预处理指令:

1
2
3
4
5
6
7
8
9
10
// image.h 文件的开头
#ifndef IMAGE_H // 检查是否定义了IMAGE_H宏
#define IMAGE_H // 定义IMAGE_H宏

// 类定义和其他代码
class ImageHandler {
// 类的成员和方法
};

#endif // IMAGE_H // 结束条件编译

#ifndef IMAGE_H:检查是否已经定义了IMAGE_H宏,如果没有定义,则继续执行下面的代码。
#define IMAGE_H:定义IMAGE_H宏,用于标记该头文件已经被包含。这确保了即使image.h被多次包含,ImageHandler类也只会被定义一次。
#endif:结束条件编译

#ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况

使用 #pragma once

image.h文件的开头添加:

1
2
3
4
5
6
#pragma once

// 类定义和其他代码
class ImageHandler {
// 类的成员和方法
};

#pragma once是一种编译器指令,指示编译器只包含一次该头文件。它的效果与使用#ifndef/#define保护符相同,但写法更简洁。这种方法更易于维护,但需要注意的是,#pragma once不是所有编译器都支持的标准,因此在一些特定的编译环境中可能需要退回到传统的防护符号方法。

#pragma once由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。

但是:

你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。

其好处是,你不必再担心宏名冲突了,当然也就不会出现宏名冲突引发的奇怪问题。大型项目的编译速度也因此提高了一些。

对应的缺点就是如果某个头文件有多份拷贝,这个方法不能保证他们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。

因此,对于可移植性方面而言,我更倾向于使用#ifndef的方法

检查包含关系

头文件的包含关系也需要注意。循环包含会导致难以定位的问题,影响程序的可维护性。

使用编译器的预处理选项查看包含树

许多编译器提供了查看包含文件树的选项,可以帮助你理解头文件的包含关系。

例如gccg++

1
g++ -H main.cpp -o main

输出:

1
2
3
4
. main.cpp
.. image.h
... other_header.h
.. another_header.h

使用 [[CMake]] 的include-what-you-use工具

include-what-you-use(IWYU)是一个专门分析C/C++代码的工具,可以帮助检测和优化头文件的包含情况。IWYU可以生成详细的报告,指出哪些头文件是多余的,哪些头文件是缺失的。

使用方法

  1. 安装include-what-you-use工具。
  2. 在CMake项目中,将CMAKE_CXX_INCLUDE_WHAT_YOU_USE设置为iwyu的路径。

在CMakeLists.txt中添加:

1
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "include-what-you-use")

然后,运行CMake和构建系统,会自动生成包含分析的报告。


C++编译中头文件重复包含问题
http://zerohzzzz.github.io/2024/09/01/C++编译中头文件重复包含问题/
Author
ZeroHzzzz
Posted on
September 1, 2024
Licensed under