为什么C++标准头文件都有类似以下的结构?

本文详细介绍了C++标准头文件中常见的预处理指令结构,包括如何避免重复包含、宏定义及其用法、条件编译指令等内容。

为什么C++标准头文件都有类似以下的结构?

#ifndef __INCvxWorksh
#define __INCvxWorksh

#ifdef __cplusplus
extern "C" {
#endif

/*...*/

#ifdef __cplusplus
}
#endif

#endif /* __INCvxWorksh */

 

最佳答案

C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令:

指令 用途
#
空指令,无任何效果
#include
包含一个源代码文件
#define
定义宏
#undef
取消已定义的宏
#if
如果给定条件为真,则编译下面代码
#ifdef
如果宏已经定义,则编译下面代码
#ifndef
如果宏没有定义,则编译下面代码
#elif
如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个#if……#else条件编译块
#error
停止编译并显示错误信息

一、文件包含
#include
预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。
预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时,通过给定编译时的条件来达到不同的效果。例如:

#define AAA
#include "t.c"
#undef AAA
#include "t.c"

为了避免那些只能包含一次的头文件被多次包含,可以在头文件中用编译时条件来进行控制。例如:
/*my.h*/
#ifndef MY_H
#define MY_H
……
#endif

在程序中包含头文件有两种格式:
#include <my.h>
#include "my.h"
第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。
采用两种不同包含格式的理由在于,编译器是安装在公共子目录下的,而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。

二、宏
宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏,这样的宏可以像函数一样被调用,但它是在调用语句处展开宏,并用调用时的实际参数来代替定义中的形式参数。
1.#define
指令
#define
预处理指令是用来定义宏的。该指令最简单的格式是:首先神明一个标识符,然后给出这个标识符代表的代码。在后面的源代码中,就用这些代码来替代该标识符。这种宏把程序中要用到的一些全局值提取出来,赋给一些记忆标识符。
#define MAX_NUM 10
int array[MAX_NUM];
for(i=0;i<MAX_NUM;i++) /*……*/

在这个例子中,对于阅读该程序的人来说,符号MAX_NUM就有特定的含义,它代表的值给出了数组所能容纳的最大元素数目。程序中可以多次使用这个值。作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序红的宏标识符和一般变量标识符区别开来。如果想要改变数组的大小,只需要更改宏定义并重新编译程序即可。
宏表示的值可以是一个常量表达式,其中允许包括前面已经定义的宏标识符。例如:
#define ONE 1
#define TWO 2
#define THREE (ONE+TWO)
注意上面的宏定义使用了括号。尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。例如:
six=THREE*TWO;
预处理过程把上面的一行代码转换成:
six=(ONE+TWO)*TWO;
如果没有那个括号,就转换成six=ONE+TWO*TWO;了。
宏还可以代表一个字符串常量,例如:
#define VERSION "Version 1.0 Copyright(c) 2003"
2.
带参数的#define指令
带参数的宏和函数调用看起来有些相似。看一个例子:
#define Cube(x) (x)*(x)*(x)
可以时任何数字表达式甚至函数调用来代替参数x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:
int num=8+2;
volume=Cube(num);
展开后为(8+2)*(8+2)*(8+2);
如果没有那些括号就变为8+2*8+2*8+2了。
下面的用法是不安全的:
volume=Cube(num++);
如果Cube是一个函数,上面的写法是可以理解的。但是,因为Cube是一个宏,所以会产生副作用。这里的参数不是简单的表达式,它们将产生意想不到的结果。它们展开后是这样的:
volume=(num++)*(num++)*(num++);
很显然,结果是10*11*12,而不是10*10*10;
那么怎样安全的使用Cube宏呢?必须把可能产生副作用的操作移到宏调用的外面进行:
int num=8+2;
volume=Cube(num);
num++;
3.#
运算符
出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符例如:

#define PASTE(n) "adhfkj"#n

main()
{
printf("%s/n",PASTE(15));
}
宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15
4.##
运算符
##
运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子:

#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c

main()
{
printf("%d/n",NUM(1,2,3));
printf("%s/n",STR("aa","bb","cc"));
}

最后程序的输出为:
123
aabbcc
千万别担心,除非需要或者宏的用法恰好和手头的工作相关,否则很少有程序员会知道##运算符。绝大多数程序员从来没用过它。

三、条件编译指令
条件编译指令将决定哪些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
1.#if
指令
#if
指令检测跟在制造另关键字后的常量表达式。如果表达式为真,则编译后面的代码,直到出现#else#elif#endif为止;否则就不编译。
2.#endif
指令
#endif
用于终止#if预处理指令。

#define DEBUG 0
main()
{
#if DEBUG
printf("Debugging/n");
#endif
printf("Running/n");
}

由于程序定义DEBUG宏代表0,所以#if条件为假,不编译后面的代码直到#endif,所以程序直接输出Running
如果去掉#define语句,效果是一样的。
3.#ifdef
#ifndef
#define DEBUG

main()
{
#ifdef DEBUG
printf("yes/n");
#endif
#ifndef DEBUG
printf("no/n");
#endif
}
#if defined
等价于#ifdef; #if !defined等价于#ifndef
4.#else
指令
#else
指令用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码。#endif指令将中指上面的条件块。

#define DEBUG

main()
{
#ifdef DEBUG
printf("Debugging/n");
#else
printf("Not debugging/n");
#endif
printf("Running/n");
}

5.#elif
指令
#elif
预处理指令综合了#else#if指令的作用。

#define TWO

main()
{
#ifdef ONE
printf("1/n");
#elif defined TWO
printf("2/n");
#else
printf("3/n");
#endif
}
程序很好理解,最后输出结果是2

6.
其他一些标准指令
#error
指令将使编译器显示一条错误信息,然后停止编译。
#line
指令可以改变编译器用来指出警告和错误信息的文件号和行号。
#pragma
指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。


 

 
<think>好的,用户问的是C++为什么需要头文件保护。我需要先回忆一下相关的知识,然后组织一个清晰易懂的回答。 首先,头文件保护通常指的是使用#ifndef、#define和#endif这些预处理指令来防止头文件被多次包含。用户之前提到的例子中,有#ifndef _HOLLE_H这样的代码,这可能存在拼写错误,但基本结构是对的。用户可能已经了解这一点,所以问题应该集中在解释必要性上。 接下来,我要考虑用户的需求。他们可能正在学习C++,遇到了头文件重复包含的问题,或者老师提到了头文件保护,他们想深入了解原因。也有可能他们在自己的代码中遇到了链接错误,想知道如何解决。需要明确头文件保护的作用,以及没有它会导致的问题。 然后,我需要分点解释。首先,重复包含的问题。比如,如果多个源文件包含同一个头文件,或者头文件之间相互包含,会导致头文件的内容被多次编译。这会导致重复定义,比如类、函数、变量的重复声明,编译器会报错。例如,头文件中定义了一个类,如果没有保护,两次包含就会在同一个编译单元中出现两次类定义,这是不允许的。 其次,需要说明头文件保护如何解决这个问题。当第一次包含头文件时,宏没有被定义,所以#ifndef成立,接着定义宏,然后继续处理头文件内容。第二次包含时,宏已经定义,所以跳过头文件内容,避免了重复。 还要提到可能的后果,比如编译错误,比如“redefinition of class”或者“multiple definition”的错误。链接错误也可能出现,但更多是重复定义导致的。 另外,可能用户会混淆头文件保护和#pragma once的区别,但用户的问题没有提到这个,所以暂时不需要展开,除非有必要。但为了全面,可以稍微提一下,但重点还是在传统的#ifndef方式。 需要注意的陷阱,比如宏名称的唯一性。如果不同的头文件使用了相同的宏名称,保护就会失效,所以建议使用项目特定的命名约定,比如HELLO_H_,用文件名和项目名组合。 最后,举一个例子会有帮助。比如,如果没有保护,当两个文件包含同一个头文件,编译时会出错。而有了保护,就只会包含一次,避免错误。 总结起来,回答的结构应该是:问题产生的场景,头文件保护的作用,如何工作,可能的问题,以及最佳实践。这样用户能逐步理解为什么需要它,以及如何正确使用。 可能用户还会问到头文件保护和#pragma once的区别,但当前问题只涉及头文件保护,所以暂时不需要深入。但如果有余力,可以简要提到作为补充,但优先级不高。 检查是否有遗漏的点,比如预处理指令的具体语法,但用户之前已经看过例子,所以不需要再重复语法,重点放在原因和必要性上。 确保语言口语化,避免技术术语过于密集,用简单例子说明。比如用两个cpp文件包含同一个头文件的情况,导致类被重复定义,引发错误。 最终,回答应该结构清晰,分点说明,帮助用户逐步理解头文件保护的重要性。</think>在 C++ 中,**头文件保护**(Header Guard)是一种通过预处理指令(`#ifndef`、`#define`、`#endif`)防止头文件被重复包含的机制。以下是其必要性和工作原理的详细说明: --- ### **为什么需要头文件保护?** #### 1. **避免重复定义的编译错误** - **问题场景**: 当多个源文件(`.cpp`)包含同一个头文件,或者头文件之间互相嵌套包含时,头文件的内容可能会被多次复制到同一个编译单元中。 ```cpp // 假设头文件无保护,且被多个文件包含: // my_header.h class MyClass { ... }; // 类的定义 ``` 如果 `my_header.h` 被多次包含,编译器会看到多次 `class MyClass` 的定义,导致错误: ```cpp error: redefinition of 'class MyClass' ``` - **解决方案**: 头文件保护确保头文件内容仅被编译器处理一次,即使被多次包含。 --- #### 2. **头文件保护的工作原理** - **预处理指令的作用**: ```cpp #ifndef UNIQUE_MACRO_NAME // 如果未定义过 UNIQUE_MACRO_NAME #define UNIQUE_MACRO_NAME // 定义该宏 // 头文件内容(如类、函数声明等) #endif // 结束条件编译 ``` - **执行流程**: 1. 第一次包含头文件时,`UNIQUE_MACRO_NAME` 未定义,进入条件编译块。 2. 定义 `UNIQUE_MACRO_NAME` 并处理头文件内容。 3. 后续再包含该头文件时,`UNIQUE_MACRO_NAME` 已定义,直接跳过内容。 --- #### 3. **未使用头文件保护的后果** - **编译错误**:重复的类、函数、变量定义。 - **链接错误**:符号重复定义(例如全局变量多次声明)。 - **代码冗余**:多次展开头文件内容,增加编译时间和目标文件大小。 --- ### **示例对比** #### 无头文件保护的情况: ```cpp // my_header.h class MyClass { ... }; // 类的定义 // main.cpp #include "my_header.h" #include "my_header.h" // 重复包含,导致编译错误 ``` #### 添加头文件保护后: ```cpp // my_header.h #ifndef MY_HEADER_H_ // 唯一宏名 #define MY_HEADER_H_ class MyClass { ... }; // 仅处理一次 #endif ``` --- ### **注意事项** 1. **唯一宏名规则**: - 宏名称需全局唯一,通常以头文件名的大写形式命名(例如 `MY_HEADER_H_`)。 - 避免使用 `_HOLLE_H` 这类易拼写错误或非标准名称(以双下划线开头或包含保留标识符)。 2. **替代方案**: - 部分编译器支持 `#pragma once`,作用类似头文件保护,但依赖编译器支持,可读性稍差。 --- ### **总结** 头文件保护是 C++ 中防止代码重复包含的基础机制,**核心目的是确保头文件内容在单一编译单元中仅展开一次**,避免因重复定义导致的编译和链接错误。它是编写健壮、可复用代码的必要实践。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值