标准库与用户定义的头文件(.h)及其在C中的实现文件(.c)有何不同?

时间:2017-09-02 15:57:22

标签: c static-libraries header-files

如何在main.c中使用#include <stdio.h>包含的libc.a(静态库)等标准库与main.c中包含的用户定义头文件(cube.h)及其实现文件不同( cube.c)在C?

我的意思是两个都是头文件,但是一个实现是静态库(.a),其他是源文件(.c)。

你可以在cube.c中定义(实现)

#include "cube.h"

int cube( int x ) {
   return x * x * x;
}

然后我们将把函数声明放在另一个文件中。按照惯例,这是在头文件cube.h中完成的。

int cube( int x );

我们现在可以通过使用#include指令(它是C预处理器的一部分)从其他地方调用该函数,例如main.c。

#include "cube.h"
#include <stdio.h>


int main() {
  int c = cube( 10 );
  printf("%d", c);
  ...
}

此外,如果我在cube.h中包含了包含警戒,当我在main.c和cube.c中包含cube.h时会发生什么。它将被包括在哪里?

2 个答案:

答案 0 :(得分:3)

编程语言与其实现不同。

编程语言是一种规范(写在纸上;你应该阅读n1570,它实际上是C11标准),它不是一个软件。 C标准指定了C标准库,并将标头定义为#include - d。

(你可以运行你的C程序与一群人类奴隶,没有任何计算机;这将是非常不道德的;你也可以使用像Ch这样的解释器,并避免任何编译器或对象或可执行文件文件)功能

  

使用libc.a ...包含的#include <stdio.h>(静态库)等标准库与用户文件cube.c

的区别

上面的句子是完全错误的(没有意义)。 libc.a #include不包括<stdio.h> - 或/usr/include/stdio.h标题(即文件/usr/include/bits/stdio2.h和其他内部标题,例如main.c)。当编译 cube.c<stdio.h>时,就会发生这种情况。

原则上,#include <stdio.h>可能不是您计算机上的任何文件(例如/usr/include/stdio.h可能会在编译器中触发一些魔法)。在实践中,编译器在#include <stdio.h>时解析<setjmp.h>(以及其他包含的文件)。

某些标准标头(特别是<stdreturn.h><stdarg.h>gcc,....)由标准指定,但是在特殊builtins的帮助下实施或attributes编译器的GCC(即&#34;魔法&#34;事物)。

C标准知道translation units

您的GCC编译器进程source files(严格来说,实现翻译单元)并以preprocessing阶段(处理#include和其他指令以及扩展宏)开始。并且cc1不仅运行编译器(某些as),还运行汇编程序ldlinker cube.h(阅读Levine&#39; s {{ 3}}预订更多)。

有充分理由,您的标头文件gcc -Wall -Wextra -g实际上应该以Linkers and Loaders开头。在你的简单例子中,它们可能没用(但你应该养成这种习惯)。

您几乎总是应该使用-v(以获取所有警告和调试信息)。阅读有关include guards

的章节

您也可以将gcc传递给cc1,以了解实际运行的程序(例如ldas-H)。

您可以将gcc传递给cube.c,以了解在预处理阶段包含哪些源文件。您还可以获取cube.i的预处理形式作为gcc -C -E cube.c > cube.i获取的cube.i文件,然后使用某个编辑器或寻呼机查看该gcc文件。

你 - 或cube.c - 需要(在你的例子中)将#include(由该文件给出的翻译单元和每个头文件cube.o - )编译到main.c Invoking GCC(假设是Linux系统)。您还可以将main.o编译为gcc。最后cube.o会链接main.olibc.so,一些启动文件(阅读object file)和main.c共享库(实现POSIX C标准库规范)还有一点)产生crt0executable目标文件,共享Relocatable(和静态库,如果你使用的话)和可执行文件在Linux上使用libraries文件格式。

如果您使用多个源文件(和翻译单元)编写C程序,您几乎应该使用ELFbuild automation工具。

  

如果我在cube.h中包含了包含警卫,那么当我在main.c和cube.c中包含cube.h时会发生什么?

这些应该是两个不同的翻译单元。你会分几步编译它们。首先,使用

main.o编译为 gcc -Wall -Wextra -g -c main.c
main.o

以上命令正在生成cc1目标文件(借助ascube.c

然后使用

编译(另一个翻译单元)gcc -Wall -Wextra -g -c cube.c
cube.o

因此获得cube.h

(请注意,在cube.c中添加包含警卫不会改变它将被读取两次的事实,一次编译main.c时另一次编译时yourprog 1}})功能

最后,使用

将两个目标文件链接到gcc -Wall -Wextra -g cube.o main.o -o yourprog 可执行文件
gcc -v

(我邀请您尝试所有这些命令,并尝试使用gcc代替上面的gcc -Wall -Wextra -g cube.c main.c -o yourprog

请注意gcc -v正在执行上述所有步骤(请与make一起检查)。你真的应该写一个GNU make来避免输入所有这些命令(并且只使用make -j进行编译,或者甚至更好./yourprog来并行运行编译。)

最后,您可以使用gdb运行可执行文件(但请阅读Makefile),但您应该了解如何使用gdb ./yourprog并尝试cube.h

  

gcc -Wall -Wextra -g -c main.c包含在哪里?

它将包括在两个翻译单位;运行gcc -Wall -Wextra -g -c cube.c时运行一次,运行cube.o时再运行一次。请注意,目标文件(main.oprintf)不包含所包含的标头。他们的调试信息(PATH格式)保留了包含(例如包含的路径,而不是头文件的内容)。

顺便说一句,看看现有的DWARF项目(并研究他们的一些源代码,至少是为了灵感)。您可以查看GNU free softwareglibc以了解C标准库在Linux上真正包含的内容(它构建在musl-libc上面,system calls中列出,由提供和实现syscalls(2))。例如,{{1}}最终有时使用Linux kernel,但它是write(2)(请参阅buffering)。

PS。也许您梦想编程语言(如Ocaml,Go,...)了解fflush(3)。 C不是一个。

答案 1 :(得分:2)

TL; DR: C标准库 库函数之间最重要的区别在于编译器可能会非常清楚标准库函数的作用而不会看到它们的作用定义

首先,有两种库:

  1. C标准库(以及可能属于C实现的其他库,如libgcc

  2. 任何其他库 - 包括/usr/lib/lib等中的所有其他库,或项目中的所有库。

  3. 类别1中的库类别2中的库库之间最重要的区别是允许编译器假设每个您在类别1库中使用的单个标识符就像它是标准库函数一样,并且表现得像在标准中一样,并且可以使用此事实来优化事物,因为它认为合适 - 即使没有它实际上链接与标准库中的相关例程,或者在运行时执行它。看看这个例子:

    % cat foo.c 
    #include <math.h>
    #include <stdio.h>
    
    int main(void) {
        printf("%f\n", sqrt(4.0));
    }
    

    我们编译它,然后运行:

    % gcc foo.c -Wall -Werror
    % ./a.out 
    2.000000
    %
    

    并打印出正确的结果。

    那么当我们询问用户的号码时会发生什么:

    % cat foo.c 
    #include <math.h>
    #include <stdio.h>
    
    int main(void) {
        double n;
        scanf("%lf\n", &n);
        printf("%f\n", sqrt(n));
    }
    

    然后我们编译程序:

    % gcc foo.c -Wall -Werror
    /tmp/ccTipZ5Q.o: In function `main':
    foo.c:(.text+0x3d): undefined reference to `sqrt'
    collect2: error: ld returned 1 exit status
    

    惊喜,它没有​​链接。这是因为sqrt位于数学库-lm中,您需要链接它以获取定义。但它是如何起作用的呢?因为 C编译器可以自由地假设标准库中的任何函数的行为就像它在标准中所写的那样,所以它可以优化对它的所有调用;即使我们没有使用任何-O开关,也可以这样做。

    请注意,甚至不需要包含标题。 C11 7.1.4p2允许这样:

      

    如果可以在不引用标头中定义的任何类型的情况下声明库函数,则允许声明该函数并使用它而不包括其关联的标头。

    因此,在以下程序中,编译器仍然可以假设sqrt是标准库中的那个,并且此处的行为仍然符合:

    % cat foo.c
    int printf(const char * restrict format, ...);
    double sqrt(double x);
    
    int main(void) {
        printf("%f\n", sqrt(4.0));
    }
    % gcc foo.c -std=c11 -pedantic -Wall -Werror
    % ./a.out
    2.000000
    

    如果删除sqrt的原型,并编译程序,

    int printf(const char * restrict format, ...);
    
    int main(void) {
        printf("%f\n", sqrt(4));
    }
    

    符合C99,C11编译器必须诊断隐式函数声明的约束违例。该程序现在是无效程序,但它仍然可以编译(C标准允许)。 GCC仍然在编译时计算sqrt(4)。请注意,我们在这里使用int而不是double,因此如果没有正确的普通函数声明,它甚至不会在运行时工作,因为没有原型,编译器就不会知道参数必须是double而不是传入的int(没有原型,编译器不知道int必须转换为{{1} }} 的)。但它仍然有效

    double

    这是因为隐式函数声明是带有外部链接的函数声明,C标准说明(C11 7.1.3)

      

    [...]以下任何子条款中包含外部链接的所有标识符(包括未来的库方向)和% gcc foo.c -std=c11 -pedantic foo.c: In function ‘main’: foo.c:4:20: warning: implicit declaration of function ‘sqrt’ [-Wimplicit-function-declaration] printf("%f\n", sqrt(4)); ^~~~ foo.c:4:20: warning: incompatible implicit declaration of built-in function ‘sqrt’ foo.c:4:20: note: include ‘<math.h>’ or provide a declaration of ‘sqrt’ % ./a.out 2.000000 始终保留用作具有外部链接的标识符。 [...]

    Appendix J.2.明确列出为未定义的行为:

      

    [...]程序声明或定义保留标识符,而不是7.1.4(7.1.3)允许的标识符。

    即。如果程序确实有自己的errno,那么行为就是未定义的,因为编译器可以假设sqrt是符合标准的行为。