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

How can I stop #including redundant headers when I use header files like these?

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

另外,我的模块头通常看起来像这些例子,所以任何其他的批评/指针都是有用的。

1
2
3
4
5
6
7
8
9
10
/* foo.h */
#ifndef FOO_H
#define FOO_H

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

void foo();

#endif

1
2
3
4
5
6
7
8
9
10
/* bar.h */
#ifndef BAR_H
#define BAR_H

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

void bar();

#endif

并使用示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 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)
}


"包括"是什么意思?预处理器语句#include file复制file的内容,并用这些内容替换该语句。不管发生什么事

如果"include"的意思是"这些文件中的语句和符号将被多次解析,从而导致警告和错误",那么"include"保护将阻止这种情况发生。

如果"include"的意思是"编译器的某些部分将读取这些文件的某些部分",那么是的,它们将被包含多次。预处理器将读取文件的第二个包含内容,并将其替换为空行,因为包含保护会产生很小的开销(文件已经在内存中)。但是,现代编译器(gcc,不确定其他编译器)可能会进行优化以避免出现这种情况,并注意到该文件在第一次传递时包含了保护,只需丢弃将来的包含内容,消除开销-不要担心这里的速度,清晰性和模块性更为重要。当然,编译是一个耗时的过程,但是#include是您最不担心的。

为了更好地理解包括防护装置,请考虑以下代码示例:

1
2
3
4
5
6
7
8
9
10
11
#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的任何实例都将替换为1。

在您的示例中,您得到了稍微复杂一些(但不太复杂)的内容。扩展exampleclient.c中的所有EDOCX1[2]语句将导致以下源:(注意:我缩进了它,但这不是头的正常样式,预处理器不会这样做。我只是想让它更可读)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* 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
}

在执行各种定义时,请仔细阅读该代码并注意。结果是:

1
2
3
4
5
6
7
8
9
10
#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);

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

福:

1
2
3
4
#ifndef FOO_H
#define FOO_H
void foo(void);
#endif

Fo.C:

1
2
3
4
5
6
7
8
9
10
11
12
#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'ed),则需要在.h文件中包含stdlib。


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

这是一个很好的练习。我自己也在#ifdef前加了一行:

1
#pragma once

当特定编译器支持时,它保证文件实际上只读取一次。我觉得这样比较理想。

所以,总结一下:

  • 像您使用的头保护一样,阻止编译器多次解释头内容,但可能会导致预处理器多次解析头内容(这不是大问题)。
  • #pragma once使特定的头文件只读一次。
  • 同时使用时,如果编译器支持,#pragma once应该有效;如果不支持,将应用头保护。


    是的,mpi.h将被包括多次(和stdlib.h一样);如果mpi.h包括类似于foo.hbar.h的警卫,那么这将不是问题。


    1)好:你有一个"包括警卫"。stdlib.h、"mpi.h"和"void foo()"只有在首次包含"foo.h"时才会被编译器看到。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* foo.h */
    #ifndef FOO_H
    #define FOO_H

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

    void foo();

    #endif

    2)坏:每次使用"foo.h"时,它都会包含"foo.h"的全部内容:

    1
    2
    3
    4
    5
    /* foo.h */
    #include <stdlib.h>
    #include"mpi.h"

    void foo();

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

    这主要是针对一个头(foo.h)调用另一个头("bar.h")进行"保护",该头可能递归调用第一个头。

    包括foo.h的每个不同编译单元都将始终获得"stdlib.h"、"mpi.h"和"void foo()"。关键是它们只能被看到一次,而不是在同一个编译单元中被多次看到。

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