vlambda博客
学习文章列表

c语言里面预处理的用法



一、预处理:


1、什么是预处理?


      想必每个稍微写过一点c语言程序的都会写到如下面代码所示,这个就是表示预处理(主要是这个"#"符号):


#include  <stdio.h>


2、理解一个我们自己写的一个程序到可执行程序的详细过程:


(1)源码.c->(编译)->elf可执行程序


(2)源码.c->(编译)->目标文件.o->(链接)->elf可执行程序


(3)源码.c->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序


(4)源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序


说明:


      预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。上面第四个是最为详细的编译过程。


3、gcc中只预处理不编译的方法:


  • gcc编译时可以给一些参数来做一些设置,譬如 gcc xx.c -o xx 可以指定可执行程序的名称;譬如 gcc xx.c -c -o xx.o 可以指定只编译不连接,也可以生成.o的目标文件。


  • gcc -E xx.c -o xx.i  可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序(下面的常见的预处理里面我会演示预处理过程中到底发生了什么事情。)。


4、C语言预处理代码实战:


(1)、#include(#include <>和#include ""的区别),我们先来看下面的代码演示,我先在root@ubuntu-virtual-machine:/mnt/hgfs/day#下创建了两个文件:


     root@ubuntu-virtual-machine:/mnt/hgfs/day# ls
     hello.c  test.h
     root@ubuntu-virtual-machine:/mnt/hgfs/day


然后我们在test.h里面定义的了两个数据类型int a 和float b,接着我在hello,c里面引用它,分别用"test.h"和<test.h>,然后你会发现自己写的这个头文件包含进去使用"test.h"可以行得通,但是使用<test.h>这种形式是行不通的:


     int a;
 float b;


 #include <stdio.h>
 #include <test.h>

 int main(void)
 
{

       a=54;
       printf("the a is %d\n",a);

       return 0;

 }


编译结果:


    root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc hello.c
    hello.c:2:10: fatal error: test.h: 没有那个文件或目录
    #include <test.h>
      ^~~~~~~~
    compilation terminated.


上面实验现象分析:

  • #include <> 和 #include""的区别:<>专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的,所以上面我写的那个就会报错),""用来包含自己写的头文件;更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器中配置的或者操作系统配置的寻找目录,譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含意思就是不会找当前目录下),如果找不到就会提示这个头文件不存在。

  • "  "包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。

  • 总结:规则虽然允许用双引号来包含系统指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的在当前目录下放着用"",如果是自己写的但是集中放在了一起专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。头文件包含的真实含义就是:在#include的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句。过程在预处理中进行。

(2)、注释:


  • 注释是给人看的,不是给编译器看的。


  • 编译器既然不看注释,那么编译时最好没有注释的。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了。


下面是实现过程现象:


#include <stdio.h>
int main(void)
 
{

    // 强制类型转换
    int a;
    char b;
    a = 1;
    b = (char)a;
    printf("b = %d.\n", b);  // b=1

    return 0;

 }


编译过程:


  root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc -E hello.c -o hello.i
  root@ubuntu-virtual-machine:/mnt/hgfs/day# ls
  '\'   a.out   hello.c   hello.i   test.h
  root@ubuntu-virtual-machine:/mnt/hgfs/day# 
  root@ubuntu-virtual-machine:/mnt/hgfs/day# cat hello.i


然后我们可以看到这里面发生的现象了:


extern void funlockfile (FILE *__stream) __attribute__ 
((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2
 # 4 "hello.c"
 int main(void)
 {

      int a;
      char b;
      a = 1;
      b = (char)a;
      printf("b = %d.\n", b);

      return 0;

 }


通过上面验证,读者可以看到这些注释全没了。


(3)、条件编译:


  • 有时候我们希望程序有多种配置,我们在源代码编写时写好了各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果。


  • 条件编译中用的两种条件判定方法分别是#ifdef 和 #if:

    区别:#ifdef XXX判定条件成立与否时主要是看XXX这个符号在本语句之前有没有被定义,只要定义了(我们可以直接#define XXX或者#define XXX 12或者#define XXX YYY)这个符号就是成立的。它的格式是:#if (条件表达式),它的判定标准是()中的表达式是否为true还是flase,跟C中的if语句有点像:


     #include "stdio.h"

     #define NUM 

     int main(void)

     
{


             int a = 0;
             #ifdef NUM     // 如果前面有定义NUM这个符号,成立
             a = 111;
             printf("#ifdef NUM.\n");
             #else          // 如果前面没有定义NUM这个符号,则执行下面的语句
             a = 222;
             printf("#elif.\n");
             #endif

             printf("a = %d.\n", a);

             return 0;
 }


输出结果:


  root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc hello1.c
 root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out
 #ifdef NUM.
 a = 111.


我们也来看一下这个过程中到底发生了什么:


 root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc -E hello1.c -o hello1.i
 root@ubuntu-virtual-machine:/mnt/hgfs/day# ls
 '\'   a.out   hello1.c   hello1.i   hello.c   hello.i   
 test.h


编译过程:


   # 868 "/usr/include/stdio.h" 3 4
   # 2 "hello1.c" 2
   # 6 "hello1.c"
   int main(void)
   
{

          int a = 0;

          a = 111;
          printf("#ifdef NUM.\n");
          printf("a = %d.\n", a);
          return 0;
   }


这个代码演示是if形式:


 #include  <stdio.h>

 #define NUM 1

 int main(void)
 
{

      int a = 0;
      #if (NUM == 0)        // 如果前面有定义NUM这个符号,成立
      a = 111;
      printf("#ifdef NUM.\n");
      #else         // 如果前面没有定义NUM这个符号,则执行下面的语句
      a = 222;
      printf("#elif.\n");
      #endif
      return 0;
 }


输出结果:


root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc hello2.c
root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out
#elif.


二、总结:


   好了今天的分享就到这里了,这里我分享的重点是要知道这个预处理到底发生了什么,这是自己以前没弄明白的东西,虽然有些书上也写的比较明白,但是实际具体细节,自己还真不明白,通过这次的总结学习,是彻底弄明白了这里面的发生了什么。其实在stm32里面,我们经常都会跟这个预处理打交道,特别是多个文件重复被包含的问题(这里面有点跟宏定义一样,宏定义明天会分享的):


c语言里面预处理的用法


‘理论’是你知道是这样,但它却不好用。


‘实践’是它很好用,但你不知道是为什么。