当我使用这样的头文件时,如何停止#include冗余头?

时间:2012-07-18 17:25:42

标签: c compiler-construction

所以我仍然习惯于模块化编程,并希望确保我遵守最佳实践。如果我有下面的两个模块头文件,那么每个文件(例如“mpi.h”)的标题#included是否会被多次包含?是否有适当的方法来解释这个问题?

此外,我的模块标题通常看起来像这些示例,因此任何其他批评/指针都会有所帮助。

/* foo.h */
#ifndef FOO_H
#define FOO_H

#include <stdlib.h>
#include "mpi.h"

void foo();

#endif

/* bar.h */
#ifndef BAR_H
#define BAR_H

#include <stdlib.h>
#include "mpi.h"

void bar();

#endif

使用示例程序:

/* ExampleClient.c */
#include <stdlib.h>
#include <stdio.h>
#include "mpi.h"
#include "foo.h"
#include "bar.h"

void main(int argc, char *argv[]) {
    foo();
    MPI_Func();
    bar();
    exit(0)
}

4 个答案:

答案 0 :(得分:5)

'include'是什么意思?预处理程序语句#include file复制file的内容,并用这些内容替换该语句。无论如何都会发生这种情况

如果通过'include'表示“这些文件中的语句和符号将被多次解析而导致警告和错误”,那么不,包含警卫会阻止它。

如果通过'include'表示“编译器的某些部分将读取这些文件的某些部分”,那么是的,它们将被多次包含。预处理器将读取文件的第二个包含并用空行替换它,因为包含保护,这会产生很小的开销(文件已经在内存中)。然而,现代编译器(海湾合作委员会,不确定其他人)可能会进行优化以避免这种情况,并注意到该文件在第一次通过时包含警卫并简单地丢弃未来的内容,消除开销 - 不要担心速度,清晰度和模块化更重要。编译是一个耗时的过程,当然,#include是您最不担心的问题。

为了更好地理解包含警卫,请考虑以下代码示例:

#ifndef INCLUDE_GUARD
#define INCLUDE_GUARD
// Define to 1 in first block
#define GUARDED 1
#endif

#ifndef INCLUDE_GUARD
#define INCLUDE_GUARD
// Redefine to 2 in second block
#define GUARDED 2
#endif

在预处理(第一次通过)之后,将GUARDED定义为什么?如果确实定义了它们的参数,则预处理器语句#ifndef或其等效的#if !defined()将返回false。因此,我们可以得出结论,第二个#ifndef将返回false,因此在预处理器的第一次传递之后,只有第一个GUARDED定义将保留。程序中剩余的GUARDED的任何实例将在下一次传递中替换为1。

在你的例子中,你有一些(但不是很多)更复杂的东西。扩展ExampleClient.c中的所有#include语句将产生以下源:(注意:我缩进了它,但这不是标头的正常样式,预处理器也不会这样做。我只是想让它更多可读的)

/* ExampleClient.c */
//#include <stdlib.h>
  #ifndef STDLIB_H
    #define STDLIB_H
    int abs (int number); //etc.
  #endif

//#include <stdio.h>
  #ifndef STDLIB_H
    #define STDLIB_H
    #define NULL 0 //etc.
  #endif

//#include "mpi.h"
  #ifndef MPI_H
    #define MPI_H
    void MPI_Func(void);
  #endif

//#include "foo.h"
  #ifndef FOO_H
    #define FOO_H
    //#include <stdlib.h>
      #ifndef STDLIB_H
        #define STDLIB_H
        int abs (int number); //etc.
      #endif
    //#include "mpi.h"
      #ifndef MPI_H
        #define MPI_H
        void MPI_Func(void);
      #endif
    void foo(void);
  #endif


//#include "bar.h"
  #ifndef BAR_H
    #define BAR_H
    //#include <stdlib.h>
      #ifndef STDLIB_H
        #define STDLIB_H
        int abs (int number); //etc.
      #endif
    //#include "mpi.h"
      #ifndef MPI_H
        #define MPI_H
        void MPI_Func(void);
      #endif
    void bar(void);
#endif

void main(int argc, char *argv[]) {
    foo();
    MPI_Func();
    bar();
    exit(0); // Added missing semicolon
}

浏览该代码并记下执行各种定义的时间。结果是:

#define STDLIB_H
int abs (int number); //etc.
#define STDLIB_H
#define NULL 0 //etc.
#define MPI_H
void MPI_Func(void);
#define FOO_H
void foo(void);
#define BAR_H
void bar(void);

关于你对其他批评/指针的请求,你为什么在所有的标题中#include stdlib.h和mpi.h?我理解这是一个精简的示例,但一般来说,头文件应该只包含其内容的声明所必需的文件。如果您使用stdlib中的函数或在foo.c或bar.c中调用MPI_func(),但函数声明只是void foo(void),则不应在头函数中包含这些文件。例如,请考虑以下模块:

foo.h:

#ifndef FOO_H
#define FOO_H
void foo(void);
#endif

foo.c:

#include <stdlib.h>  // Defines type size_t
#include "mpi.h"     // Declares function MPI_func()

#include "foo.h"     // Include self so type definitions and function declarations
                     // in foo.h are available to all functions in foo.c

void foo(void);
  size_t length;
  char msg[] = "Message";

  MPI_func(msg, length);
}

在这个例子中,foo()的实现需要来自stdlib和mpi的东西,但定义却没有。如果foo()返回或需要size_t值(stdlib中的typedef'),则需要在.h文件中#include stdlib。

答案 1 :(得分:1)

大多数情况下没有,有点'是'。您的头文件将被多次“读取”,但在第二次及以后,预处理器将切断所有内容。这意味着它不会浪费编译器的时间,#include块中的#ifdef只会被执行一次(每个头文件)。

这是一个很好的做法。我自己,我还在#ifdef s之前添加了以下行:

#pragma once

当特定编译器支持时,它保证文件实际上只读取一次。我认为这种方式更加优化。

所以,总结一下:

  1. 标题保护,就像你使用prevent编译器多次解释标题内容一样,但可能导致预处理器多次解析它(这不是一个大问题),
  2. #pragma once导致特定的头文件只能读取一次。
  3. 当使用两者时,如果编译器支持,#pragma once应该有效;如果没有,将使用标题保护。

答案 2 :(得分:0)

1)好的:你有"include guard"。 “stdlib.h”,“mpi.h”和“void foo()”只能由编译器看到第一次时#include“foo.h”

/* foo.h */
#ifndef FOO_H
#define FOO_H

#include <stdlib.h>
#include "mpi.h"

void foo();

#endif

2)坏:这将#include 整个内容“foo.h”每次时使用它:

/* foo.h */
#include <stdlib.h>
#include "mpi.h"

void foo();

3)#include“,我的意思是”每个编译单元一次“(即相同的 .c源文件)。

这主要是“保护”一个标题(foo.h)调用另一个标题(“bar.h”),它可以递归调用第一个标题。

#includes foo.h的每个不同的编译单元总是会得到“stdlib.h”,“mpi.h”和“void foo()”。重点是它们只能在同一个编译单元中看到一次 - 而不是多次。

4)这都是“编译时”。它与库无关(即“链接时间”)。

答案 3 :(得分:0)

是的,mpi.h将被多次包含(stdlib.h也是如此);如果mpi.h包含类似于foo.hbar.h的警卫,则不会出现问题。