关于包含保护的两个常见问题:



第一个问题:为什么不包含保护我的头文件免于相互递归包含的保护?每当我编写如下内容时,我都会不断收到关于显然不存在的符号的错误,甚至是奇怪的语法错误:

“ ah”

#ifndef A_H
#define A_H

#include "b.h"

...

#endif // A_H


“ bh”

#ifndef B_H
#define B_H

#include "a.h"

...

#endif // B_H


“ main.cpp”

#include "a.h"
int main()
{
    ...
}


为什么我要得到错误编译“ main.cpp”?我该怎么办才能解决我的问题?





第二个问题:

为什么不包括防止多重定义的警卫?例如,当我的项目包含两个包含相同标题的文件时,有时链接器会抱怨某个符号多次定义。例如:

“ header.h”

#ifndef HEADER_H
#define HEADER_H

int f()
{
    return 0;
}

#endif // HEADER_H


“ source1.cpp”

#include "header.h"
...


“ source2.cpp”

#include "header.h"
...


为什么会这样?我需要怎么做才能解决我的问题?



评论

我看不出这与stackoverflow.com/questions/553682/…和stackoverflow.com/questions/14425262/…有何不同?

@LuchianGrigore:第一个问题与A&D没有直接关系,或者至少是IMO,它没有解释为什么A&M会给依赖项带来麻烦。第二个确实解决了两个问题之一(第二个),但是所涉及的内容不够广泛和详细。我想将有关保镖的这两个问答集归为一类,因为在我看来,它们紧密相关。

@sbi:我可以删除标签,没问题。我只是因为那是有关C ++的一个常见问题,所以应该将其标记为faq-c ++。

@sbi:好吧,最近几天,我看到至少有4个关于SO的问题,这些问题让初学者感到困惑,因为它们有多个定义或相互包含,所以从我的观点来看,这是一个反复出现的问题。这就是为什么我一开始就写这整本书的原因:为什么我要为初学者写一个问答集呢?但是,当然,我知道每个人对什么是“频繁”都有主观的认识,而我的看法可能与您的看法不符。尽管我仍然认为应该将其标记为c ++-faq,但是我对具有较高经验的高级用户没有什么看法。

对我来说似乎是一个常见问题解答

#1 楼


第一个问题:

为什么不包括保护我的头文件不受相互递归包含的保护措施?


它们是。

他们没有帮助的是相互包含的标头中数据结构的定义之间的依赖关系。要了解这意味着什么,让我们从一个基本的场景开始,看看为什么包含保护确实有助于相互包含。

假设相互包含的a.hb.h头文件具有琐碎的内容,即代码中的省略号问题文本中的部分将替换为空字符串。在这种情况下,您的main.cpp将很高兴进行编译。这仅仅是感谢您的包含卫士!

如果您不相信,请尝试将其删除:

//================================================
// a.h

#include "b.h"

//================================================
// b.h

#include "a.h"

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}


您会注意到编译器达到包含深度限制时将报告失败。此限制是特定于实现的。根据C ++ 11标准的第16.2 / 6段:


#include预处理指令可能会出现在由于另一个文件中的#include指令而被读取的源文件中,直到实现定义的嵌套限制。


那是怎么回事?


解析main.cpp时,预处理器将满足指令#include "a.h"。该指令告诉预处理器处理头文件a.h,获取该处理的结果,并用该结果替换字符串#include "a.h"
处理a.h时,预处理器将满足指令#include "b.h",并且采用相同的机制:预处理程序应处理头文件b.h,获取其处理结果,并用该结果替换#include指令;
处理b.h时,指令#include "a.h"将告诉预处理器处理a.h并将其替换为结果;
预处理器将再次开始解析a.h,再次符合#include "b.h"指令,这将建立潜在的无限递归过程。当达到临界嵌套级别时,编译器将报告错误。

如果包含include保护,则不会在步骤4中设置无限递归。让我们看看原因:


(与以前相同)解析main.cpp时,预处理器将满足指令#include "a.h"。这告诉预处理器处理头文件a.h,获取处理结果,然后用该结果替换字符串#include "a.h"
在处理a.h时,预处理器将满足指令#ifndef A_H。由于尚未定义宏A_H,它将继续处理以下文本。后续指令(#defines A_H)定义了宏A_H。然后,预处理器将满足指令#include "b.h":预处理器现在应处理头文件b.h,获取其处理结果,并用该结果替换#include指令;
处理b.h时,预处理器将满足指令#ifndef B_H。由于尚未定义宏B_H,它将继续处理以下文本。后续指令(#defines B_H)定义了宏B_H。然后,伪指令#include "a.h"将告诉预处理器处理a.h,并用预处理#include的结果替换b.h中的a.h伪指令;
编译器将再次开始预处理a.h,并再次满足#ifndef A_H指令。但是,在先前的预处理过程中,已定义了宏A_H。因此,编译器这次将跳过以下文本,直到找到匹配的#endif伪指令为止,并且此处理的输出为空字符串(当然,假设#endif伪指令后面没有任何内容)。因此,预处理器将用空字符串替换#include "a.h"中的b.h伪指令,并将追溯执行,直到它替换#include中的原始main.cpp伪指令。因此,include防护措施可以防止相互包含。但是,它们不能帮助相互包含文件中的类定义之间的依赖关系:

//================================================
// a.h

#ifndef A_H
#define A_H

#include "b.h"

struct A
{
};

#endif // A_H

//================================================
// b.h

#ifndef B_H
#define B_H

#include "a.h"

struct B
{
    A* pA;
};

#endif // B_H

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}


鉴于上述标头,main.cpp无法编译。


为什么会发生这种情况?


要查看发生了什么,再次执行步骤1-4就足够了。

很容易看出,前三步和大部分第四步不受此更改的影响(请通读它们以使自己确信)。但是,在步骤4的末尾会发生一些变化:用空字符串替换#include "a.h"中的b.h伪指令后,预处理器将开始解析b.h的内容,尤其是B的定义。不幸的是,B的定义提到了A类,正是由于包含防护,才曾遇到过它!

声明一个成员变量的类型以前没有声明,这当然是一个错误,编译器会礼貌地指出这一点。


我该怎么做才能解决我的问题?


您需要前向声明。

实际上,定义类A不需要定义类B,因为将指向A的指针声明为成员变量,而不是类型为A的对象。由于指针具有固定的大小,因此编译器将不需要知道A的确切布局,也不需要计算其大小即可正确定义类B。因此,足以在A中声明类b.h并使编译器知道其存在:

//================================================
// b.h

#ifndef B_H
#define B_H

// Forward declaration of A: no need to #include "a.h"
struct A;

struct B
{
    A* pA;
};

#endif // B_H


您的main.cpp现在肯定可以编译了。一些注意事项:


通过在#include中用前向声明替换b.h指令不仅打破了相互包含,还足以有效地表达BA的依赖关系:只要在任何时候都使用前向声明可能的/实用的也被认为是良好的编程习惯,因为它有助于避免不必要的包含,从而减少了总体编译时间。但是,在消除了互斥之后,必须将main.cpp#include都修改为a.h(如果完全需要后者),因为b.h不再通过b.h间接地成为#include
尽管类a.h的前向声明足以使编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),但取消引用A的指针(例如,调用成员函数)或计算其指针size是对不完整类型的非法操作:如果需要,编译器需要使用A的完整定义,这意味着必须包括定义它的头文件。这就是为什么将类定义及其成员函数的实现通常拆分为该类的头文件和实现文件(类模板是该规则的例外)的原因:实现文件,它们永远不会被其他文件中的文件所占用。项目,可以安全地A所有必需的标头以使定义可见。另一方面,头文件将不会#include其他头文件,除非它们确实需要这样做(例如,使基类的定义可见),并且将在可能/可行的情况下使用前向声明。 />

第二个问题:

为什么不包括防止多个定义的防护?


它们是。

他们不能保护您的是单独翻译单元中的多个定义。在StackOverflow上的此问答中也对此进行了说明。

也看到了,尝试删除包含保护并编译以下#include(或#include的修改版本)(<< />)
//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}


编译器肯定会在这里抱怨source1.cpp被重新定义。显而易见:其定义被两次包含!但是,如果source2.cpp包含适当的包含保护,则上面的f()可以毫无问题地进行编译。那是意料之中的。

尽管如此,即使存在include防护,并且编译器将不再打扰您,并显示错误消息,链接器仍将坚持这样一个事实,即合并从source1.cppheader.h的编译获得的目标代码时,会找到多个定义,并且拒绝生成可执行文件。


为什么会发生这种情况?


基本上,每个source1.cpp文件(此处的技术术语为翻译单元)都位于您的项目是分别独立编译的。解析source2.cpp文件时,预处理器将处理所有.cpp指令并扩展其遇到的所有宏调用,并且此纯文本处理的输出将在输入中提供给编译器,以将其转换为目标代码。一旦编译器完成了为一个翻译单元生成目标代码的工作,它将继续进行下一个翻译单元,并且在处理前一个翻译单元时遇到的所有宏定义都将被遗忘。

实际上,用.cpp转换单元(#include文件)编译项目就像执行同一个程序(编译器)n一样,每次都有不同的输入:同一程序的不同执行不会t共享先前程序执行的状态。因此,每个翻译都是独立执行的,并且在编译其他翻译单元时将不会记住在编译一个翻译单元时遇到的预处理器符号(如果您仔细考虑一下,您将很容易意识到这实际上是一种理想的行为)。 br />
因此,即使包含保护有助于您防止一个翻译单元中同一标头的递归相互包含和冗余包含,它们也无法检测到同一定义是否包含在不同的翻译单元中。

但是,当合并从项目的所有.cpp文件的编译生成的目标代码时,链接程序将看到多次定义同一符号,因为这违反了“一个定义规则”。根据C ++ 11标准的第3.2 / 3段:


每个程序应准确地包含该程序中使用的每个非内联函数或变量的一个定义;无需诊断。该定义可以显式出现在程序中,可以在标准库或用户定义的库中找到,或者(在适当的情况下)可以隐式定义(请参见12.1、12.4和12.8)。内联函数应在使用过的每个转换单元中定义。


因此,链接器将发出错误并拒绝生成程序的可执行文件。


如果需要将函数定义保存在头文件n d中,我该怎么办才能解决我的问题?


多个翻译单元(请注意,如果您的标头仅以一个翻译单元为.cpp d,就不会出现问题),您需要使用#include关键字。

否则,您只需要在#include中保留函数的声明,就将其定义(主体)放入一个单独的inline文件中(这是经典方法)。

关键字header.h代表对编译器的非绑定请求,以直接在调用站点内联函数主体,而不是为常规函数调用设置堆栈框架。尽管编译器不必满足您的请求,但是.cpp关键字确实可以成功告诉链接程序允许多个符号定义。根据C ++ 11标准的3.2 / 5段:


一个类类型(第9章),枚举类型(7.2),具有外部链接的内联函数(7.1.2),类模板(第14章),非静态函数模板(14.5.6)可以有多个定义。 ,类模板的静态数据成员(14.5.1.3),类模板的成员函数(14.5.1.1)或在程序中未指定某些模板参数(14.7、14.5.5)的模板专门化,前提是每个定义出现在不同的翻译单元中,并且提供的定义满足以下要求:上面的段落基本上列出了通常放在头文件中的所有定义,因为它们可以安全地包含在多个翻译单元中。带有外部链接的所有其他定义都属于源文件。

使用inline关键字而不是inline关键字还可以通过给您的函数内部链接来抑制链接器错误,从而使每个转换单元都拥有该函数(及其局部静态变量)的私有副本。 。但是,这最终会导致更大的可执行文件,通常应首选使用static

实现与inline关键字相同的结果的另一种方法是将函数inline放在未命名的位置命名空间。根据C ++ 11标准的第3.5 / 4段:


未命名的名称空间或在未命名的名称空间中直接或间接声明的名称空间具有内部链接。所有其他名称空间都有外部链接。如果名称空间范围的名称是以下名称,则该名称空间范围未与上面的内部链接相同,即与封闭的名称空间具有相同的链接:

—变量;或

—一个函数; or

—一个命名类(第9条),或在typedef声明中定义的未命名类,其中该类具有用于链接目的的typedef名称(7.1.3);或者或

—在typedef声明中定义的命名枚举(7.2)或未命名的枚举,其中该枚举具有用于链接目的的typedef名称(7.1.3);或

—属于具有链接的枚举的枚举数;或

—一个模板。出于上述相同的原因,应首选static关键字。

评论


真好在讨论ODR的两种形式的某个地方,我要补充指出,引用的3.2 / 3列出了我们通常放在头文件中的定义,而所有其他具有外部链接的定义都在源文件中。然后是“哪种ODR适用于我的定义?”的普通语言清单。

–骗子
13年2月19日在13:43

@aschepler:您的意思是3.2 / 4(“如果...,则必须完成T类型”)还是3.2 / 5(“类类型可以有多个定义(第9条),枚举类型(7.2)” ),具有外部链接的内联函数(7.1.2),类模板(第14条),并且提供的定义满足以下要求[...]“)?我认为同时提及这两者将是有用的,另一方面,很难在短时间内做到这一点,并且在经过长时间的解释之后,重点将转移到包含警卫队,这是本问答的主题。也许有与此相关的新的常见问题解答条目?

–安迪·普罗(Andy Prowl)
13年2月19日在13:53

@AndyProwl-通常的答案是社会病。是不是让你失望。好帖子... +1

–吉姆·巴尔特(Jim Balter)
13年3月16日在6:49

@Andrew:谢谢你,我很高兴你找到了能量:D

–安迪·普罗(Andy Prowl)
15年6月3日在21:37

@AndyProwl感谢您抽出宝贵的时间,并撰写了如此有用且详尽的解释,+ 1

–v.tralala
17年5月7日23:47

#2 楼

fiorentinoing的答案在Git 2.24(2019年第四季度)中得到了回应,在Git代码库中也进行了类似的代码清理。
请参阅RenéScharfe(rscharfe)的commit 2fe4439(2019年10月3日)。(由Junio C Hamano合并) -gitster-在a4c5d9f提交中,2019年10月11日)

全树:删除重复的#include指令
发现于:
git grep '^#include ' '*.c' | sort | uniq -d



#3 楼

首先,您应该100%确保“ include guards”中没有重复项。

使用此命令

grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print  " " }' | grep -v "^1\ "


,您将1)突出显示所有包含防护,针对每个包含名称获得带有计数器的唯一行,对结果进行排序,仅打印计数器和包含名称,然后删除真正唯一的计数器。

提示:这等效于获取重复的包含名称列表