美文网首页c/c++
基础回顾之C编译过程及预处理器

基础回顾之C编译过程及预处理器

作者: 纸简书生 | 来源:发表于2017-05-06 14:01 被阅读72次

    编译过程

    如果需要弄清整个编译过程,那还得好好复习下编译原理。这里只是通过一个小例子讨论大致过程。

    准备好一个helloworld的c文件。内容最好简单如下:

    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        printf("Hello World!\n");
        return 0;
    }
    

    编译命令

    $ gcc helloworld.c // 编译
    $ ./a.out // 执行
    Hello World!
    

    gcc命令其实依次执行了四步操作:

    1. 预处理(Preprocessing)

      • 预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。
      • 命令: gcc -E -I./ helloworld.c -o helloworld.i 或者直接用cpp helloworld.c -I./ -o helloworld.i
      • 参数说明:
        • -E是让编译器在预处理之后就退出,不进行后续编译过程;
        • -I指定头文件目录,这里指定的是我们自定义的头文件目录;
        • -o指定输出文件名。
      • 经过预处理之后代码体积会大很多。如下是预处理之后的部分内容。
      # 1 "helloworld.c"
      # 1 "<built-in>" 1
      # 1 "<built-in>" 3
      # 330 "<built-in>" 3
      # 1 "<command line>" 1
      # 1 "<built-in>" 2
      # 1 "helloworld.c" 2
      typedef unsigned char __uint8_t;
      typedef short __int16_t;
      typedef unsigned short __uint16_t;
      typedef int __int32_t;
      typedef unsigned int __uint32_t;
      typedef long long __int64_t;
      typedef unsigned long long __uint64_t;
      typedef struct _opaque_pthread_attr_t __darwin_pthread_attr_t;
      typedef struct _opaque_pthread_cond_t __darwin_pthread_cond_t;
      typedef struct _opaque_pthread_condattr_t __darwin_pthread_condattr_t;
      typedef unsigned long __darwin_pthread_key_t;
      typedef struct _opaque_pthread_mutex_t __darwin_pthread_mutex_t;
      typedef struct _opaque_pthread_mutexattr_t __darwin_pthread_mutexattr_t;
      FILE *fopen(const char * restrict __filename, const char * restrict __mode) __asm("_" "fopen" );
      
      int fprintf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3)));
      int fputc(int, FILE *);
      int fputs(const char * restrict, FILE * restrict) __asm("_" "fputs" );
      size_t fread(void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream);
      FILE *freopen(const char * restrict, const char * restrict,
                       FILE * restrict) __asm("_" "freopen" );
      int fscanf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__scanf__, 2, 3)));
      int fseek(FILE *, long, int);
      int fsetpos(FILE *, const fpos_t *);
      long ftell(FILE *);
      size_t fwrite(const void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream) __asm("_" "fwrite" );
      int getc(FILE *);
      int getchar(void);
      char *gets(char *);
      void perror(const char *);
      int printf(const char * restrict, ...) __attribute__((__format__ (__printf__, 1, 2)));
      int putc(int, FILE *);
      int putchar(int);
      int puts(const char *);
      int remove(const char *);
      int rename (const char *__old, const char *__new);
      void rewind(FILE *);
      int scanf(const char * restrict, ...) __attribute__((__format__ (__scanf__, 1, 2)));
      void setbuf(FILE * restrict, char * restrict);
      int setvbuf(FILE * restrict, char * restrict, int, size_t);
      int sprintf(char * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3))) __attribute__((__availability__(swift, unavailable, message="Use snprintf instead.")));
      int sscanf(const char * restrict, const char * restrict, ...) __attribute__((__format__ (__scanf__, 2, 3)));
      FILE *tmpfile(void);
      
      __attribute__((__availability__(swift, unavailable, message="Use mkstemp(3) instead.")))
      
      __attribute__((deprecated("This function is provided for compatibility reasons only.  Due to security concerns inherent in the design of tmpnam(3), it is highly recommended that you use mkstemp(3) instead.")))
      ... 中间很多内容这里省略
      
      extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
             const char * restrict, va_list);
      # 499 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h" 2 3 4
      # 2 "helloworld.c" 2
      int main(){
          printf("Hello World!\n");
          return 0;
      }
      
    2. 编译(Compilation),

      • 这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
      • 命令:gcc -S -I./ helloworld.c -o helloworld.s
      • 参数:-S为了编译之后停止。后面的两个参数含义和预处理的时候一样
      • 经过编译之后的内容如下。
          .section    __TEXT,__text,regular,pure_instructions
          .macosx_version_min 10, 12
          .globl  _main
          .p2align    4, 0x90
      _main:                                  ## @main
          .cfi_startproc
      ## BB#0:
          pushq   %rbp
      Ltmp0:
          .cfi_def_cfa_offset 16
      Ltmp1:
          .cfi_offset %rbp, -16
          movq    %rsp, %rbp
      Ltmp2:
          .cfi_def_cfa_register %rbp
          subq    $16, %rsp
          leaq    L_.str(%rip), %rdi
          movl    $0, -4(%rbp)
          movb    $0, %al
          callq   _printf
          xorl    %ecx, %ecx
          movl    %eax, -8(%rbp)          ## 4-byte Spill
          movl    %ecx, %eax
          addq    $16, %rsp
          popq    %rbp
          retq
          .cfi_endproc
      
          .section    __TEXT,__cstring,cstring_literals
      L_.str:                                 ## @.str
          .asciz  "Hello World!\n"
      .subsections_via_symbols
      
    3. 汇编(Assemble),

      • 汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。如果有多个文件需要为每一个源文件产生一个目标文件。
      • 命令:as helloworld.s -o helloworld.o 或者 gcc -c helloworld.s -o helloworld.o
    4. 链接(Linking)。

      • 链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
      • 命令:ld -o helloworld.out helloworld.o **.o **.o。格式其实就是 ld(选项)(参数)参数就是需要连接的目标文件。由于这里没有生成其他目标文件,所以这段不会连接成功的。具体的命令可以看这里ld命令

    走完上面的步骤可以得到如下几个文件。


    其实我们平时写代码的到得到可执行文件的整个过程可以用下图来概括。


    特别需要注意的是宏参数和函数参数的区别,宏参数是进行严格的特换。这如果使用不懂就会出现非常严重的错误。

    使用#参数:宏参数创建字符串

    宏定义 调用 结果
    #define TESTPF(x) printf("test "#x" * "#x"=%d\n",(x)*(x)); TESTPF(5 + 5) test 5 + 5 * 5 + 5=100
    #define TESTPF(x) printf("test x * x=%d\n",(x)*(x)); TESTPF(5 + 5); test x * x=100

    可以看到#参数的作用就是把字符串中的x也进行了替换。

    使用##参数:预处理粘合剂

    ##作用是把两个语言符号组合为单个语言符号。
    例子:

    #define XNAME(n) x##n
    #define PRINT_XN(n) printf("x"#n" = %d \n");
    
    int XNAME(1) = 1;
    int XNAME(2) = 2;
    PRINT_XN(1);
    PRINT_XN(2);
    
    结果:
    x1 = 1606416096 
    x2 = 4352 
    
    

    ...__VA_ARGS__:可变宏

    这个其实在iOS开发中还是用得挺多的。

    例子

    #define PR(...) printf(__VA_ARGS__)
    
    PR("DD");
    PR("D=%d,F=%d\n",12,22);
    
    结果:
    DDD=12,F=22
    

    特别注意。省略号必须在最后一个参数位置。根据这个道理,有些同学可能就能联想到某些语言可变参数的位置为什么一定要在最后把。比如python

    文件包含

    预处理器发现#include指令后,会寻找跟在后面的文件,把这个文件中的内容包含到当前文件中。

    头文件

    OC中有.h和.m文件,这和C里面的.h和.c是同一个道理。所以这里就不多说了。具体看图。


    其他预处理指令

    • #undef取消已定义的宏
    • #if如果给定条件为真,则编译下面代码
    • #elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
    • #endif结束一个#if……#else条件编译块
    • #ifdef如果宏已经定义,则编译下面代码
    • #ifndef如果宏没有定义,则编译下面代码
    • #pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。

    上面这些预处理指令,用得比较频繁。大家应该不陌生。还多一些平时用得不多的。

    • #line指令可以改变编译器用来指出警告和错误信息的文件号和行号。
    • #error停止编译并显示错误信息

    预处理宏

    C标准制定的一些预处理宏。


    额外补充一个__func__预定义标识符。这个是C99标准提供的。用于标识当前函数。

    上面这些预处理宏经常用于打印一些日志信息。

    扩展阅读

    C programming Tutorial Introduction to C Programming (for Novices & First-Time Programmers)
    C/C++预处理指令#define,#ifdef,#ifndef,#endif…

    相关文章

      网友评论

        本文标题:基础回顾之C编译过程及预处理器

        本文链接:https://www.haomeiwen.com/subject/opgetxtx.html