作为C/C++的开发人员,编写头文件代码是必不可少的常见操作。为了避免头文件宏名称的冲突,往往为了#ifndef XXX(常与 #define XXX和 #endif组合使用)的宏名称想个老半天。好不容易想了个好的宏名称,但是可惜的是,在多人协作开发的复杂系统中,还是容易存在冲突。此外,头文件过多,繁琐的#ifndef XXX、#define XXX、#endif冗余代码也是让人烦不胜烦。
一、背景
我们知道,C/C++头文件中的 #ifndef XXX、#define XXX、#endif是C/C++头文件的保护“铁三角”机制,其核心用途是为了防止头文件被重复包含,避免因重复定义导致的代码编译错误。当多个源文件(.c或者.cpp等)包含同一个头文件时,或头文件之间互相嵌套包含会导致头文件的内容被多次编译,其中包含的变量/函数定义、类声明等会触发重复定义错误,这就是我们编译过程常常遇到的“redefinition of ‘XXX’”错误。
能不能不包含头文件呢?不包含就找不到对应的变量/函数定义、类声明等,也会编译报错。同时,多个头文件形成循环依赖时,会导致编译时预处理陷入死循环,比如头文件A.h包含B.h,B.h又包含A.h,若没有头文件包含防护机制,就会形成事实上的死循环。
二、替换方案
让人高兴的是,基于#pragma once的替换方案能够完美解决防止C/C++头文件重复定义使用的#ifndef XXX“铁三角”。
#pragma once最早由 Microsoft Visual C++在1990年代引入,初衷是解决传统宏守卫的痛点(如前面所说的宏名冲突和代码冗余)。由于其简洁高效,GCC、Clang、ICC 等主流编译器都相继提供支持,使其成为现代C与C++项目的默认选择。一些老版本的编译器可能不支持#pragma once,但是就目前而言,应该大部分都是支持的高版本编译器,对于普通开发人员来说,基本不用过于担心。
使用#ifndef XXX、#define XXX、#endif机制的头文件代码大致如下:
#ifndef __XXX_H__
#define __XXX_H__
……
#endif
而使用#pragma once的头文件代码大致如下:
#pragma once
……
从代码上开,可以明显看到#pragma once机制相比之前机制的优势,代码异常简洁,不用担心费尽心思的名称是否存在重复。
三、替换后的其它优劣
抛开前述的宏名冲突和代码冗余优化,有一说一,使用#pragma once还有其它一些优势和不足。
先说优势,使用#pragma once可提高编译速度。采用#ifndef XXX机制时,预处理器每次遇到#ifndef语句都要打开文件检查宏状态,递归包含时开销明显增加。而#pragma once的编译基于文件物理路径,当编译器首次遇到#pragma once时,会记录该头文件的物理唯一标识符(通常为文件系统的绝对路径或inode值),并将其加入内部已包含文件集合。后续再遇到相同标识符的#include指令时,直接跳过文件读取和解析过程。基于网上的公开数据,在Linux内核项目中,使用#pragma once可减少 10%到20% 的预处理时间。
再说不足,#pragma once在个别场景会出现问题。主要是相同头文件多次出现,不管是相同路径还是不同路径。这种场景一般出现在头文件的多个副本,在对头文件进行多次复制后,在相同路径存在多个不同名称的副本,或者在不同路径存在多个相同或者不同名称的副本,此时代码文件同时包含上述的多个头文件时,#pragma once因为无法识别为同一文件,会导致重复定义。但是这个问题大家也无需担心,对于实际开发来说,出现的概率少之又少,基本普通人一辈子的开发生涯都无法遇到。所以这里说明主要是进行提醒,让大家知道有这么一种场景需要注意。
四、结语
#pragma once属于编译器的扩展功能,虽然未成为C与C++的标准,但是目前已经被广泛使用,是当前C与C++开发推荐使用的高效且简洁的代码方式。如果时间允许,建议你花点时间进行#pragma once的替换优化,你会爱上这种优化机制的。
五、联系
如果有任何疑问欢迎随时交流。学无止境,实事求是,每天进步一点点!
