在C主体中包含头文件的机制

在C主体中包含头文件的机制,c,header-files,C,Header Files,对于C开发的大型软件,我们首先在单独的头文件中声明所有自定义函数(例如,myfun.h)。之后,一旦我们编写了使用myfun.h中列出的函数的代码(例如main.c),我们就必须#包括“myfun.h”。我想知道它是如何工作的,因为即使我在主体之前包含头文件中声明的函数名,代码也无法在main.c中看到函数的详细信息。我猜它会搜索函数库以获取函数详细信息……我说得对吗?当你说“它会搜索函数库以获取函数详细信息”时,你说的并不遥远,但这并不完全正确。函数声明,即函数原型仅包含编译器执行两件事所需的

对于C开发的大型软件,我们首先在单独的头文件中声明所有自定义函数(例如,
myfun.h
)。之后,一旦我们编写了使用
myfun.h
中列出的函数的代码(例如
main.c
),我们就必须
#包括“myfun.h”
。我想知道它是如何工作的,因为即使我在主体之前包含头文件中声明的函数名,代码也无法在
main.c
中看到函数的详细信息。我猜它会搜索函数库以获取函数详细信息……我说得对吗?

当你说“它会搜索函数库以获取函数详细信息”时,你说的并不遥远,但这并不完全正确。函数声明,即函数原型仅包含编译器执行两件事所需的足够信息:

  • 首先,编译器将函数注册为一个已知标识符,这样它就知道你调用它时要做什么,而不是一个带括号的随机字母串(对于编译器来说,它们本质上是相同的,没有函数原型——一个错误)

  • 其次,编译器使用函数原型检查代码的正确性。从这个意义上讲,正确性意味着函数调用将在算术和类型上与原型匹配。换句话说,函数调用
    intsquare(inta,intb)将有两个参数,都是整数

不过,该程序并不“搜索库”。没有括号的函数名不是函数调用,而是函数的地址。因此,当调用函数时,处理器跳转到函数的内存位置。(假设函数尚未内联。)

但是这个函数位于哪里?视情况而定。如果在同一个模块中编写函数,即编译成与主.c文件链接的对象的.c文件,则函数的位置将位于可执行文件的.TEXT部分的某个位置。换句话说,它只是主函数入口点的一个微小偏移。在大型项目中,此偏移不会很小,但会比单独对象的偏移短

话虽如此,如果您将这个假设函数编译成从主程序调用的DLL,那么函数的地址将通过以下两种方式之一确定:

  • 或者您将生成一个.lib/.a?(取决于您是在Windows还是Linux上)包含函数声明和地址的文件,或者:
  • 您将使用运行时链接,当主程序将.dll/.so加载到其地址空间时,主程序将在其中计算函数地址。首先,它将决定在哪里加载它。您可以将DLL设置为具有首选偏移量,以优化加载时间。否则,库将从第一个可用段开始加载,任何其他库都需要使用此新地址重新计算其函数地址,从而妨碍初始加载时间。一旦它们被加载到程序的内存中,之后就不会有任何性能问题了

  • 回到预处理器,需要注意两件事。首先,它在编译之前运行。这很重要。由于在预处理器执行任务时,程序并没有真正被“编译”,因此宏不是类型安全的。(插入Haskell关于C类型安全的笑话)这就是为什么你不应该或者不应该看到C++中的宏。在C中可以用宏实现的任何东西都可以通过代码> > const 和C++中的内联函数来完成,同时也增加了类型安全性的好处。 其次,预处理器几乎只是一个搜索和替换引擎。例如,在下面的代码中,由于预处理器
    if
    语句的计算结果为false,所以不会发生任何事情,因为我从未定义过任何内容。预处理器将删除此部分中的代码。请记住,由于编译器尚未认真运行,因此不会编译删除的代码。这一事实通常用于实现调试或登录调试构建的功能。在发布版本中,预处理器定义随后被操纵,这样就不包括调试代码

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        #if TRUE
        printf("Hello, World!");
        #endif
    
    return EXIT_SUCCESS;
    }
    
    预处理器的另一个重要用途是代码可移植性,例如:

    #ifdef WIN32 
    // Do windows things
    #elif
    // Handle other OS
    #endif
    
    一个技巧是定义一个通用函数并将其设置为适当的依赖于操作系统的函数(请记住,没有括号的函数表示函数的地址,而不是实际的函数调用),如下所示:

    // Not valid C89, possibly even C99
    const int DEFAULT_BUFFER_SIZE = 128;
    char user_input[DEFAULT_BUFFER_SIZE]; 
    
    // Legal since the dawn of time
    #define DEFAULT_BUFFER_SIZE 128
    char user_input[DEFAULT_BUFFER_SIZE];
    
    void RequestSomeKernelAction();
    
    #ifdef WIN32
    RequestSomeKernelAction = WindowsVersion;
    #else
    RequestSomeKernelAction = OtherOSFunction;
    #endif
    
    这就是说您在头文件中看到的代码遵循相同的规则。如果我有以下头文件:

    #ifndef SRC_INCLUDES_TEST_H
    #define SRC_INCLUDES_TEST_H
    
    int square(int a);
    
    #endif /** SRC_INCLUDES_TEST_H */
    
    我有一个主.c文件:

    #define SRC_INCLUDES_TEST_H
    #include "test.h"
    
    int main()
    {
        int n = square(4);
    }
    
    这个程序不会编译。main.c不知道square函数,因为虽然我包含了声明
    square
    的头文件,但我的
    \define SRC\u INCLUDES\u TEST\u H
    语句告诉预处理器将所有头文件内容复制到main,除了定义
    SRC\u INCLUDES\u TEST\u H
    的块中的内容,也就是说,什么都没有

    这些预处理器命令可以嵌套,并且有几个,我强烈建议您查找,如果只是出于历史或教学原因的话


    我要说的最后一点是,虽然C预处理器有它的缺点,但它是右手中的一个强大工具,事实上,Bjarne Stroustroup编写的第一个C++编译器本质上只是一个预处理器。

    你想读函数/变量声明和定义……你知道链接是什么意思吗?如果你正在编写一个大型软件,你应该考虑学习——至少是灵感——一些与你的领域相似或雄心勃勃的源代码。你会发现其中很多,例如,在标题上声明了其他地方可用的内容。例如,
    告诉编译器标准C中可用的函数等