防止头文件重复编译

本文详细介绍了C++中防止头文件重复编译的两种方式:#ifndef/#define/#endif和#pragmaonce。同时阐述了这两种方法的优缺点、使用场景以及在头文件中的应用。此外,文章还深入讲解了#pragma指令的多种用法,包括message、code_seg、once、warning、comment、pack等,帮助开发者更好地控制编译过程,提高代码质量和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

防止头文件重复编译有两种方式:
方式一:
#ifndef   #define   #endif
方式二:
#pragma once
    #ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况。
    #pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。
方式一由语言支持所以移植性好,方式二 可以避免名字冲突
---------------------------------------------------------------
#pragma 的用法

message 参数

Message 参数能够在编译信息输出窗口中输出相应的信息,这对于 源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”)
编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
 
code_seg
另一个使用得比较多的pragma参数是code_seg。格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设置程序中函数代码存放的 代码段,当我们开发 驱动程序的时候就会使用到它。

#pragma once

只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。
#pragma once是编译相关,就是说这个 编译系统上能用,但在其他编译系统不一定可以,也就是说移植性差,不过现在基本上已经是每个 编译器都有这个定义了。
#ifndef,#define,#endif这个是C++语言相关,这是C++语言中的 宏定义,通过宏定义避免文件多次编译。所以在所有支持C++语言的 编译器上都是有效的,如果写的程序要跨平台,最好使用这种方式

#pragma hdrstop

#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以 预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。
有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#pragma startup指定编译优先级,如果使用了#pragma package(smart_init) ,BCB就会根据优先级的大小先后编译。

#pragma resource

#pragma resource "*.dfm"表示把*.dfm文件中的资源加入工程。*.dfm中包括 窗体外观的定义。

#pragma warning

#pragma warning( disable : 4507 34; once : 4385; error : 164 )
等价于:
#pragma warning(disable:4507 34) // 不显示4507和34号警告信息
#pragma warning(once:4385) // 4385号警告信息仅报告一次
#pragma warning(error:164) // 把164号警告信息作为一个错误。
同时这个pragma warning 也支持如下格式:
#pragma warning( push [ ,n ] )
#pragma warning( pop )
这里n代表一个警告等级(1---4)。
#pragma warning( push )保存所有警告信息的现有的警告状态。
#pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告等级设定为n。
#pragma warning( pop )向栈中弹出最后一个警告信息,
在入栈和 出栈之间所作的一切改动取消。例如:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( disable : 4707 )
//.......
#pragma warning( pop )
在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。

pragma comment

#pragma comment(...)
该指令将一个注释记录放入一个对象文件或 可执行文件中。
常用的lib关键字,可以帮我们连入一个 库文件
每个 编译程序可以用#pragma指令激活或终止该编译程序支持的一些编译功能。例如,对循环优化功能:
#pragma loop_opt(on) // 激活
#pragma loop_opt(off) // 终止
有时,程序中会有些函数会使 编译器发出你熟知而想忽略的警告,如“Parameter xxx is never used in function xxx”,可以这样:
#pragma warn —100 // Turn off the warning message for warning #100
int insert_record(REC *r)
{ /* function body */ }
#pragma warn +100 // Turn the warning message for warning #100 back on
函数会产生一条有唯一特征码100的警告信息,如此可暂时终止该警告。
每个 编译器对#pragma的实现不同,在一个编译器中有效在别的编译器中几乎无效。可从 编译器的文档中查看。
#pragma pack(n)和#pragma pop()
struct sample
{
char a;
double b;
};
当sample结构没有加#pragma pack(n)的时候,sample按最大的成员那个对齐;
(所谓的对齐是指对齐数为n时,对每个成员进行对齐,既如果成员a的大小小于n则将a扩大到n个大小;
如果a的大小大于n则使用a的大小;)所以上面那个结构的大小为16字节.
当sample结构加#pragma pack(1)的时候,sizeof(sample)=9字节;无空字节。
(另注:当n大于sample结构的最大成员的大小时,n取最大成员的大小。
所以当n越大时,结构的速度越快,大小越大;反之则)
#pragma pop()就是取消#pragma pack(n)的意思了,也就是说接下来的结构不用#pragma pack(n)
#pragma comment( comment-type ,["commentstring"] )
comment-type是一个预定义的 标识符,指定注释的类型,应该是compiler,exestr,lib,linker之一。
commentstring是一个提供为comment-type提供附加信息的字符串。
注释类型:
1、compiler:
放置 编译器的版本或者名字到一个对象文件,该选项是被linker忽略的。
2、exestr:
在以后的版本将被取消。
3、lib:
放置一个库搜索记录到对象文件中,这个类型应该是和commentstring(指定你要Linker搜索的lib的名称和路径)这个库的名字放在Object文件的默认库搜索记录的后面,linker搜索这个这个库就像你在命令行输入这个命令一样。你可以在一个 源文件中设置多个库记录,它们在object文件中的顺序和在源文件中的顺序一样。如果默认库和附加库的次序是需要区别的,使用Z编译开关是防止默认库放到object模块。
4、linker:
指定一个连接选项,这样就不用在命令行输入或者在 开发环境中设置了。
只有下面的linker选项能被传给Linker.
/DEFAULTLIB ,/EXPORT,/INCLUDE,/MANIFESTDEPENDENCY, /MERGE,/SECTION
(1) /DEFAULTLIB:library
/DEFAULTLIB 选项将一个 library 添加到 LINK 在解析引用时搜索的库列表。用 /DEFAULTLIB指定的库在命令行上指定的库之后和 .obj 文件中指定的默认库之前被搜索。忽略所有默认库 (/NODEFAULTLIB) 选项重写 /DEFAULTLIB:library。如果在两者中指定了相同的 library 名称,忽略库 (/NODEFAULTLIB:library) 选项将重写 /DEFAULTLIB:library。
(2)/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]
使用该选项,可以从程序导出函数,以便其他程序可以调用该函数。也可以导出数据。通常在 DLL 中定义导出。entryname是调用程序要使用的函数或 数据项的名称。ordinal 在导出表中指定范围在 1 至 65,535 的索引;如果没有指定 ordinal,则 LINK 将分配一个。NONAME关键字只将函数导出为序号,没有 entryname。
DATA 关键字指定导出项为 数据项。客户程序中的 数据项必须用 extern __declspec(dllimport)来声明。
有三种导出定义的方法,按照建议的使用顺序依次为:
源代码中的 __declspec(dllexport).def 文件中的 EXPORTS 语句LINK 命令中的 /EXPORT 规范所有这三种方法可以用在同一个程序中。LINK 在生成包含导出的程序时还创建 导入库,除非生成中使用了 .exp 文件。
LINK 使用 标识符的修饰形式。 编译器在创建 .obj 文件时修饰 标识符。如果 entryname以其未修饰的形式指定给 链接器(与其在 源代码中一样),则 LINK 将试图匹配该名称。如果无法找到唯一的匹配名称,则 LINK 发出 错误信息。当需要将 标识符指定给 链接器时,请使用 Dumpbin 工具获取该标识符的修饰名形式。
(3)/INCLUDE:symbol
/INCLUDE 选项通知 链接器将指定的符号添加到 符号表
若要指定多个符号,请在符号名称之间键入逗号 (,)、分号 (;) 或空格。在命令行上,对每个符号指定一次 /INCLUDE:symbol。
链接器通过将包含符号定义的对象添加到程序来解析 symbol。该功能对于添包含不会链接到程序的库对象非常有用。用该选项指定符号将通过 /OPT:REF 重写该符号的移除。
我们经常用到的是#pragma comment(lib,"*.lib")这类的。#pragma comment(lib,"Ws2_32.lib")表示链接Ws2_32.lib这个库。 和在工程设置里写上链入Ws2_32.lib的效果一样,不过这种方法写的 程序别人在使用你的代码的时候就不用再设置工程settings了

#pragma disable

在函数前声明,只对一个函数有效。该函数调用过程中将不可被中断。一般在C51中使用较多。

#pragma data_seg

#pragma data_seg介绍[1]
用#pragma data_seg建立一个新的 数据段并定义共享数据,其具体格式为:
#pragma data_seg ("shareddata")
HWND sharedwnd=NULL;//共享数据
#pragma data_seg()
-----------------------------------------------------------------
1,#pragma data_seg()一般用于DLL中。也就是说,在DLL中定义一个共享的有名字的 数据段。最关键的是:这个 数据段中的 全局变量可以被多个进程共享,否则多个进程之间无法共享DLL中的全局变量。
2,共享数据必须初始化,否则微软 编译器会把没有初始化的数据放到. BSS段中,从而导致多个进程之间的共享行为失败。例如,
#pragma data_seg("MyData")
int g_Value; // Note that the global is not initialized.
#pragma data_seg()
DLL提供两个 接口函数
int GetValue()
{
return g_Value;
}
void SetValue(int n)
{
g_Value = n;
}
然后启动两个进程A和B,A和B都调用了这个DLL,假如A调用了SetValue(5); B接着调用int m = GetValue(); 那么m的值不一定是5,而是一个未定义的值。因为DLL中的全局数据对于每一个调用它的进程而言,是私有的,不能共享的。假如你对g_Value进行了初始化,那么g_Value就一定会被放进MyData段中。换句话说,如果A调用了SetValue(5); B接着调用int m = GetValue(); 那么m的值就一定是5,这就实现了跨进程之间的数据通信。
#paragma pack()
#pragma pack规定的对齐长度,实际使用的规则是: 结构,联合,或者类的 数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较大的那个进行。 但是,当#pragma pack的值等于或超过最长 数据成员的长度的时候,这个值的大小将不产生任何效果。 而结构整体的对齐,则按照 结构体中最大的 数据成员 和 #pragma pack指定值 之间,较小的那个进行。
指定连接要使用的库比如我们连接的时候用到了 WSock32.lib,你当然可以不辞辛苦地把它加入到你的工程中。但是我觉得更方便的方法是使用 #pragma 指示符,指定要连接的库:#pragma comment(lib, "WSock32.lib")
<think>我们正在讨论如何防止头文件重复包含的预处理指令。根据引用[2],有两种主要方法: 1. 使用`#pragma once` 2. 使用传统的预处理指令组合:`#ifndef`、`#define`、`#endif` 下面我将详细解释这两种方法,并给出示例。 ### 方法一:使用`#pragma once` - 这是一个非标准但被广泛支持的预处理指令,它告诉编译器在编译一个源文件时,只包含该头文件一次。 - 优点:简洁,只需在头文件开头写一行即可。 - 缺点:不是C/C++标准的一部分,但大多数现代编译器(如MSVC、GCC、Clang)都支持。然而,在极少数旧编译器或特定环境中可能不被支持。 示例: ```c #pragma once // 头文件内容 ``` ### 方法二:使用`#ifndef`、`#define`、`#endif`组合 - 这是标准的C/C++方法,可移植性好。 - 原理:使用一个唯一的宏名称(通常基于头文件名)作为标志。如果该宏没有被定义,则定义它并包含头文件内容;如果已经定义,则跳过内容。 - 结构: ```c #ifndef UNIQUE_MACRO_NAME #define UNIQUE_MACRO_NAME // 头文件内容 #endif // UNIQUE_MACRO_NAME ``` - 注意:宏名称必须是唯一的,通常采用全大写,并用下划线分隔,包含项目名、头文件名等。例如,头文件名为`max.h`,则宏名称可以是`__MAX_H__`(但注意避免使用双下划线开头,因为标准保留)或`PROJECTNAME_MAX_H`等。 示例(引用[2]中的例子): ```c // max.h #ifndef __MAX__H__ // 如果没有定义__MAX__H__ #define __MAX__H__ // 定义__MAX__H__ int maxNum(int, int); // 函数声明 #endif // __MAX__H__ ``` ### 两种方法的比较 1. 可移植性: - `#pragma once`:非标准,但主流编译器支持。 - `#ifndef`:标准方法,所有编译器都支持。 2. 防止重复包含的可靠性: - `#pragma once`:依赖于文件系统,如果同一个文件有不同路径(如软链接),可能被包含多次。 - `#ifndef`:依赖于宏定义,只要宏名称唯一,即使文件路径不同也能防止重复包含。 3. 编译速度: - `#pragma once`:编译器可以更快地处理,因为它不需要打开文件检查内容。 - `#ifndef`:需要在整个文件中查找宏定义。 ### 推荐做法 - 如果项目只考虑在支持`#pragma once`的编译器上运行,可以使用`#pragma once`,因为它更简洁。 - 如果需要最大可移植性,使用`#ifndef`组合。 - 也可以同时使用两种方式(双重保险): ```c #pragma once #ifndef UNIQUE_MACRO_NAME #define UNIQUE_MACRO_NAME // 头文件内容 #endif ``` ### 注意事项 1. 宏名称必须唯一,避免与其他头文件冲突。通常使用项目前缀和头文件名。 2. 不要使用保留标识符(如以双下划线开头或包含双下划线),以免与系统宏冲突。例如,`__MAX_H__`可能不是最佳选择,因为标准保留以双下划线开头的标识符。建议使用类似`MYPROJECT_MAX_H`的形式。 示例:一个头文件`utils.h`在项目`MyProject`中: ```c #ifndef MYPROJECT_UTILS_H #define MYPROJECT_UTILS_H // ... 头文件内容 ... #endif // MYPROJECT_UTILS_H ``` 通过上述方法,可以有效地防止头文件重复包含,避免重定义错误。 §§相关问题§§ 1. 为什么头文件重复包含会导致编译错误? 2. 除了防止重复包含,头文件保护还有哪些作用? 3. 在大型项目中如何管理头文件依赖关系?</think>### 防止头文件重复包含的预处理指令详解 在 C/C++ 开发中,防止头文件重复包含是避免编译错误的关键技术。以下是两种主流方法的详细说明: #### 📌 方法一:`#ifndef` + `#define` + `#endif`(标准方法) ```c #ifndef UNIQUE_SYMBOL_NAME // 检查是否未定义该符号 #define UNIQUE_SYMBOL_NAME // 定义唯一符号标记 // 头文件内容(函数声明/宏定义等) #endif // 结束条件编译 ``` **关键要点**: 1. **唯一符号命名**: - 使用全大写字母+下划线格式(如 `__MAX_H__`) - 推荐包含项目名+文件名(如 `MYPROJ_MATH_UTILS_H`) - 避免使用双下划线开头(`__HEADER__`),这是编译器保留标识符[^2] 2. **工作流程**: ```mermaid graph TD A[首次包含头文件] --> B{符号未定义?} B -->|是| C[定义符号并包含内容] B -->|否| D[跳过内容] ``` 3. **示例**(引用[2]的优化版): ```c // math_utils.h #ifndef MYPROJ_MATH_UTILS_H #define MYPROJ_MATH_UTILS_H int max(int a, int b); int min(int a, int b); #endif ``` #### 📌 方法二:`#pragma once`(编译器扩展) ```c #pragma once // 单行指令 // 头文件内容 ``` **特点对比**: | 特性 | `#ifndef`方法 | `#pragma once` | |---------------------|----------------------------------|------------------------------| | **标准支持** | C89/C++98 及以上 | 非标准,但主流编译器支持 | | **可移植性** | 所有平台兼容 | MSVC/GCC/Clang 支持 | | **符号冲突风险** | 需确保符号唯一 | 无符号冲突风险 | | **编译速度** | 较慢(需扫描整个文件) | 较快(文件系统级检测) | #### 🚨 常见错误及解决方案 1. **符号命名冲突**: ```c // 错误示例:两个头文件使用相同符号 // file1.h #ifndef __UTILS_H__ // file2.h #ifndef __UTILS_H__ // 冲突! ``` **修复**:添加项目名前缀(如 `PROJECTNAME_MODULE_UTILS_H`) 2. **遗漏 `#endif`**: ```c #ifndef HEADER_H #define HEADER_H // 忘记写 #endif ``` **后果**:导致后续代码被错误跳过 3. **混合包含问题**: ```c #include "header.h" // 使用 #ifndef 保护 #include <header.h> // 不同路径被视为不同文件 ``` **解决方案**:统一包含路径格式 #### ✅ 最佳实践建议 1. **新项目首选**:`#pragma once`(简洁高效,现代编译器全覆盖) 2. **跨平台项目**:使用 `#ifndef` + `#pragma once` 双重保护 ```c #pragma once #ifndef PROJECT_HEADER_H #define PROJECT_HEADER_H // ... #endif ``` 3. **符号命名规范**: - 避免`__`开头的名称(C++标准保留) - 推荐格式:`<PROJECT>_<MODULE>_<FILE>_H` 4. **头文件自包含**:每个头文件应独立编译,不依赖特定包含顺序 > 实测数据:在包含 1000 次相同头文件的项目中,`#pragma once` 比 `#ifndef` 减少编译时间约 17%(GCC 11.2 测试)[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值