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

时间:2018-04-03 16:40:35

标签: c header-files

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

1 个答案:

答案 0 :(得分:1)

当你说"它将在图书馆中搜索功能细节"你离我不远,但这不是很正确。函数声明,即函数原型只包含足够的信息,编译器可以做两件事:

  • 首先,编译器会将该函数注册为已知标识符,以便它在您调用它时知道您正在采取什么,而不是带括号的随机字母串(对于编译器,如果没有函数原型,它们本质上是相同的 - 一个错误)。

  • 其次,编译器使用函数原型来检查代码的正确性。在这个意义上的正确性意味着函数调用将匹配arity和type中的原型。换句话说,对int square(int a, int b);的函数调用将有两个参数,都是整数。

该计划没有搜索图书馆,"虽然。没有括号的函数名称不是函数调用,而是函数的地址。因此,当您调用函数时,处理器会跳转到函数的内存位置。 (这假设函数没有内联。

这个功能位于哪里?这取决于。如果你在同一个模块中编写了这个函数,即将一个.c文件编译成一个与main.c文件链接的对象到一个可执行文件中,那么该函数的位置将在.TEXT部分的某处。可执行文件。换句话说,它只是略微偏离主要功能的入口点。在一个巨大的项目中,这个偏移量不会那么轻微,但它会比单独物体的偏移量短。

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

  1. 您是否会生成.lib / .a? (取决于您是否在Windows或Linux上)包含功能声明和地址的文件,或者:
  2. 您将使用运行时链接,主程序在将.dll / .so加载到其地址空间时将计算函数地址。首先,它将确定加载它的位置。您可以将DLL设置为具有首选偏移以优化加载时间。否则,库将从可用的第一个段开始加载,并且任何其他库将需要使用此新地址重新计算其功能地址,从而妨碍初始加载时间。一旦将它们加载到程序的内存中,之后就不会有任何性能命中。
  3. 回到预处理器,注意两件事情很重要。首先,它在任何编译发生之前运行。这个很重要。由于该程序并未真正被编译和#34;当预处理器正在做它的事情时,宏不是类型安全的。 (插入关于C&#34的Haskell笑话;键入安全" )这就是为什么你不应该 - 或者不应该在C ++中看到宏。 C中的宏可以通过const和C ++中的内联函数完成,并且具有类型安全性的额外好处。

    其次,预处理器几乎只是一个搜索和替换引擎。例如,在以下代码中,没有任何反应,因为预处理器if语句的计算结果为false,因为我从未定义过任何内容。预处理器删除了本节中的代码。请记住,由于编译器尚未认真运行,因此不会编译此删除的代码。这个事实通常用于实现调试或登录调试版本的函数。在发布版本中,然后操纵预处理器定义,以便不包括调试代码。

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        #if TRUE
        printf("Hello, World!");
        #endif
    
    return EXIT_SUCCESS;
    }
    

    实际上,我使用的EXIT_SUCCESS宏在stdlib.h中定义,并由0替换。 (EXIT_FAILURE = 1)。

    在当天,预处理器被用作胶带,基本上是为了补偿C中的故障。

    例如,由于const值不能用作数组大小,因此使用了宏,如下所示:

    // 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];
    

    预处理器的另一个重要用途是代码可移植性,例如:

    #ifdef WIN32 
    // Do windows things
    #elif
    // Handle other OS
    #endif
    

    一个技巧是定义一个泛型函数并将其设置为适当的依赖于OS的函数(请记住,没有括号的函数代表函数的地址,而不是实际的函数调用),如下所示:

    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 */
    

    我有这个main.c文件:

    #define SRC_INCLUDES_TEST_H
    #include "test.h"
    
    int main()
    {
        int n = square(4);
    }
    

    此程序无法编译。 main.c将不知道square函数,因为虽然我确实包含了声明square的头文件,但我的#define SRC_INCLUDES_TEST_H语句告诉预处理器将所有头文件内容复制到main,除了那些在定义SRC_INCLUDES_TEST_H的块中,即......没有。

    这些预处理器命令可以嵌套,有几种,我强烈建议您查看,如果仅出于历史或教学原因。

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