【问题标题】:When to use include guards?何时使用包括警卫?
【发布时间】:2018-01-13 13:47:21
【问题描述】:

我知道在头文件中使用包含保护是为了防止某些东西被定义两次。不过,使用此代码示例完全没问题:

foo.c

#include <stdio.h>
#include <string.h>
#include "bar.h"

int main() {
    printf("%d", strlen("Test String"));
    somefunc("Some test string...");
    return 0;
}

bar.h

#ifndef BAR_H_INCLUDED
#define BAR_H_INCLUDED
void somefunc(char str[]);
#endif

bar.c

#include <stdio.h>
#include <string.h>
#include "bar.h"

void somefunc(char str[]) {
    printf("Some string length function: %d", strlen(str));
}

上面的sn-ps是用gcc -Wall foo.c bar.c -o foo编译的,没有报错。但是,&lt;stdio.h&gt;&lt;string.h&gt; 都包含在没有包含保护的情况下。当我将 bar.h 剥离到单个语句 void somefunc(char str[]); 时,仍然没有错误。为什么没有错误?

【问题讨论】:

  • 你看过stdio.h里面了吗?
  • 我不知道为什么你认为那里应该是一个错误。
  • 你怎么知道stdio.hstring.h包含保护宏?你检查过你电脑上的文件吗?会出现问题,例如当您有另一个标题包含您的 bar.h 并且在其他文件中时,您包含 bar.h 这个其他标题。
  • 对于问题的第二部分:函数后跟 ; 而不是主体 { } 是函数的前向声明。只要真正的函数声明不与前向声明相矛盾,就没有错误。

标签: c macros c-preprocessor header-files include-guards


【解决方案1】:

首先,包含守卫的主要目的是防止某些内容在同一个翻译单元中被声明两次。 “在同一个翻译单元中”部分是这里的关键。您对两个不同翻译单元的实验与包含保护的目的无关。它没有展示任何东西。它甚至没有远程相关。

为了利用包含保护,您必须将相同的头文件两次包含(显式或隐式)到一个实现文件中。

其次,仅仅因为某些头文件没有包含保护并且仅仅因为您将该头文件两次包含到同一个翻译单元中并不意味着它一定会触发错误。为了导致错误,标头必须包含特定“不可重复”类型的声明。不是每个标题都包含这样的违规声明。从这个意义上说,并不是每一个声明都是冒犯性的。

您的bar.h(已发布)实际上是无害的。正式地,您不需要在 bar.h 中包含守卫。它有一个函数声明,可以在一个翻译单元中重复多次。因此,多次包含此标头不会导致错误。

但是在您的 bar.h 中添加类似的内容

struct SomeStruct
{
  int i;
};

然后在同一个实现文件中包含两次,你会得到一个错误。此错误是包含警卫旨在防止的错误。该语言禁止在同一个翻译单元中重复相同结构类型的完整声明。

包含保护通常无条件地放置在头文件中。我很确定,它们也存在于&lt;stdio.h&gt;&lt;string.h&gt; 中。目前尚不清楚您为什么声称这些标头“在没有包含保护的情况下被包含”。你检查过这些文件吗?无论如何,再一次,您对两个不同翻译单元的实验无论如何都没有证明任何相关性。

【讨论】:

  • @Antti Haapala:在 C++ 中会更像这样。但是在 C 中,术语是不同的。例如,C 不将结构类型的“完整”声明(即带有{...})作为定义。结构类型的完整声明是触发此错误的声明的一个很好的示例。
  • 虽然,[C11 6.7.2.3p1]:一个特定类型的内容定义最多一次,所以一个完整的声明struct定义它的内容,正是这个定义的行为不能重复。
  • @Antti Haapala:嗯,“定义”是一个相当通用的词,很难避免在正式文本中通俗地使用它。这并不意味着每次使用“定义”这个词时,它都是指定义的标准概念。无论如何,我一直站在那场辩论的双方...看来该文档的正式术语打算具有宏定义类型定义typedef) 和外部定义(函数和变量)。似乎无意将struct { ... } 声明引入为定义
  • 我认为你的解释很好。但我仍然想知道,“包含两次”是什么意思?这是否意味着“在'bar.cpp'中包含'bar.h'两次”?那太愚蠢了,我想没有人会这样做。那么“包含守卫”的意义何在?
【解决方案2】:

重复声明不是问题;重复(类型)定义是。如果包含bar.h 标头,例如:

enum FooBar { FOO, BAR, BAZ, QUX };

然后在单个 TU(翻译单元 - 源文件加上包含的标题)中包含两次通常会产生错误。

此外,多重包含的场景并不是您所展示的。假设头文件中没有标头保护,可能导致问题的原因如下:

  • bar.h

    enum FooBar { FOO, BAR, BAZ, QUX };
    void somefunc(char str[]);
    
  • quack.h

    #include "bar.h"
    extern enum FooBar foobar_translate(const char *str);
    
  • main.c

    #include "bar.h"
    #include "quack.h"
    
    …
    

请注意,GCC 有一个选项 -Wredundant-decls 来识别冗余声明 — 其中相同的声明多次出现(通常来自多个文件,但如果相同的声明在单个文件中出现两次)。

在 C11 之前,您不能重复 typedef(在文件范围内;您始终可以在块范围内隐藏外部 typedef)。 C11 放宽了这个限制:

§6.7 声明

¶3 如果标识符没有链接,则标识符的声明不得超过一个 (在声明符或类型说明符中)具有相同的范围和相同的名称空间,除了 那:

  • 可以重新定义 typedef 名称以表示与当前相同的类型, 前提是该类型不是可变修改的类型;
  • 标签可以按照 6.7.2.3 中的规定重新声明。

但是,您仍然不能在一个 TU 的单个范围内定义两次结构类型,因此使用如下符号:

typedef struct StructTag { … } StructTag;

必须使用标头保护来保护。如果您使用不透明(不完整)类型,则不会出现此问题:

typedef struct StructTag StructTag;

至于为什么可以包含标准标头,那是因为标准要求您可以:

§7.1.2 标准标题

¶4 标准标头可以按任何顺序包含;每一个都可以被多次包含在 一个给定的范围,与只包含一次没有任何不同,除了 包含&lt;assert.h&gt; 的效果取决于NDEBUG 的定义(见7.2)。如果 使用时,标题应包含在任何外部声明或定义之外,并且它 应首先包含在第一次引用它的任何功能或对象之前 声明,或者它定义的任何类型或宏。但是,如果声明了标识符 或定义在多个标题中,第二个和后续的相关标题可能是 包含在对标识符的初始引用之后。该程序不得有任何 名称在词法上与包含之前当前定义的关键字相同的宏 标题或标题中定义的任何宏被展开时。

使用标头保护可以使您的标头符合与标准标头相同的标准。

另见关于一般主题的其他谩骂(答案),包括:

【讨论】:

    【解决方案3】:

    您的代码中没有错误的原因是您的头文件正在声明但未定义somefunc()。对某事物进行多次声明是可以的,只要它们不是定义——编译器可以接受多次声明的事物(当然,只要声明是兼容的)。

    一般来说需要include守卫来避免头文件之间的循环依赖,比如

    • 在某些情况下,标头 A 和标头 B 相互包含。至少在一个标头中需要包含保护,以防止预处理器中的无限循环。
    • 头文件 A 包含头文件 B,因为它依赖于其中的定义(例如,内联函数的定义,typedef),但其他头文件或编译单元可能在不同情况下包含头文件 A、头文件 B 或两者。需要包含保护以防止在包含这两个标头的编译单元中出现多个定义。至少,包含定义的头文件需要有一个包含保护。

    由于头文件可以以任何顺序相互包含,因此上述问题可能会变得相当复杂。

    防止某些类型的多重定义是上述的副作用,但不是包含保护的主要目的。

    尽管有上述所有情况,但我使用头文件的经验法则是“在所有头文件中使用包含保护,除非我有特殊理由不这样做”。通过这样做,避免了与不提供包括警卫相关的所有潜在问题。需要避免包含保护的情况(例如头文件声明或定义不同的东西,取决于在编译单元中定义/未定义的宏)在实践中相对较少。而且,如果您正在使用需要这些东西的技术,您应该已经知道您不应该在受影响的标头中使用包含防护。

    【讨论】:

      【解决方案4】:

      为什么?因为:

      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      int char_to_int(char* value, int *res);
      
      int char_to_int(char* value, int *res)
      {
          // do something
      }
      

      函数原型不会出错,因为它们是相同的并且函数

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-06-19
        • 1970-01-01
        • 1970-01-01
        • 2016-04-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多