美文网首页c++C语言程序员
C语言编译预处理技术一本道来

C语言编译预处理技术一本道来

作者: PcDack | 来源:发表于2017-12-04 09:57 被阅读30次
    编译&&预处理.png

    一个.C程序,从人懂到计算机懂的流程

    编译流程.png

    分别简述

    预编译(不会去报错,没有真正的到达编译环境)

    • 处理所有的注释,以空格代替
    • 将所有的#define删除,并且展开所有的宏定义
    • 处理条件编译指令#if,#ifdef,#elif,#else,#endif
    • 处理#include,展开被包含的文件
    • 保留编译器需要的#pragma指令

    预处理指令(gcc)

    gcc -E file.c -o file.i
    

    判官编译(进行词法和语法分析)

    • 对预处理文件进行词法与语法分析,语意分析
      • 词法分析主要分析关键字,标识符,立即数等是否合法
      • 语法分析主要分析表达式是否遵循语法规则
      • 语义分析在语法分析的基础上进一步分析表达式是否合法
    • 分析结束后进行代码优化生成相应的汇编文件

    编译指令

    gcc -S file.c -o file.s
    

    汇编

    • 汇编器将汇编代码转变为机器可以执行的指令
    • 每个汇编命令几乎都对应着一条机器指令

    汇编指令:

    gcc -c file.s -o file.o
    

    链接器的意义

    • 调用操作系统里面内置一些动态连接库

    总结

    • 编译器将编译工作分为三步预处理,编译,汇编
    • 连接器的工作是把各个独立的模块连接为可执行程序
    • 静态连接在编译期完成,动态连接在运行期完成

    宏定义与使用分析

    定义宏常量

    • #define定义宏常量可以出现在函数的任何地方
    • #define从本行开始,之后的代码都可以使用这个宏常量

    宏表达式

    • #define表达式给人函数调用的假象,但是并不是函数
    • #define表达式可以比函数更加强大
    • #define表达式比函数更容易出错

    容易出错的宏表达式

    #define SUM(a,b)( (a)+(b))//不加括号会产生细节错误
    
    void main()
    
    {
    
        int a=3,b=4;
    
        int i=SUM(a,b)*SUM(a,b);
    
    }
    
    

    结果为49

    如果我们写成

    #include<stdio.h>
    #define SUM(a,b) (a)+(b)//不加括号会产生细节错误
    void main()
    {
        int a=3,b=4;
        int i=SUM(a,b)*SUM(a,b);
        printf("%d\n",i);
    }
    
    

    结果为19
    压死程序的最后一个括号

    产生错误,我们要分析他的缘由,通过预处理命令得到预处理结果,我们会发现程序变成:

    void main()
    {
        int a=3,b=4;
        int i=(a)+(b)*(a)+(b);
        printf("%d\n",i);
    }
    
    

    很显然,宏函数只是无脑替换.所以,宏函数虽好,可不要贪用哦

    好用的宏表达式

    求数组的个数

    #define DIM(array)(sizeof(array)/sizeof(*array))
    

    这样一个宏解决函数解决不了的问题

    最佳示例

    #include<stdio.h>
    #define MIN(b,c)((b)<(c)?(b):(c))
    int main()
    {
        int a=2,b=5;
        printf("%d\n",MIN(a++,b));
        return 0;
    }
    

    答案为,我们通过编译预处理,就知道为什么了

    最不像C语言的C语言

    #include<stdio.h>
    #include<malloc.h>
    #define MALLOC(type,n) (type*)malloc(sizeof(type)*n)
    #define FOREACH(b,e) for(i=b;i<e;i++)
    void main()
    {
        int i=0;
        int a[]={1,2,3,4,5};
        int *p=MALLOC(int,5);
        FOREACH(0,5)
        {
            p[i]=a[i];
        }
        FOREACH(0,5)
        {
            printf("%d\n",p[i]);
        }
            
    }
    

    这个例子主要表达了宏的作用

    宏表达式与函数的对比

    • 宏表达式在预编译期被处理,编译器不知道宏表达式的存在
    • 宏表达式用“实参”完全代替形参,不进行任何运算
    • 宏表达式没有任何的“调用”开销(具体在讲到函数时候,在讲)
    • 宏表达式不能出现递归调用

    内置的宏

    含义 示例
    __FILE__ 被编译的文件名 file1.c
    __LINE__ 当前行号 25
    __DATE__ 编译的时间日期 Jan 31 2017
    __TIME__ 编译时的时间 17:01:01
    __STDC__ 标准C

    最佳实践

    宏日志

    #include<stdio.h>
    #include<time.h>
    #define LOG(s) do                                                          \
    {                                                                          \
        time_t t;                                                              \
        struct tm* ti;                                                         \
        time(&t);                                                              \
        ti=localtime(&t);                                                      \
        printf("%s,%s:%d %s\n",asctime(ti),__FILE__,__LINE__,s);               \
    }while(0)
    void main()
    {
        LOG("ENTER the main");
        
    }
    

    这个可以直接放在一个头文件里面当做库来用,当然还可以优化加入一些自定义的东西.


    条件编译使用分析

    if...#else...#endif,,,在编译期之前就已经处理好了

    • 条件编译的行为类似于C语言中的if...else...
    • 条件编译是预编译指示指令,用于控制是否编译某段代码

    简单示例

    #include<stdio.h>
    #define D 1
    int main()
    {
    #if(D==1)
        printf("D==1\n");
    #else
        printf("D!=1\n");
    #endif
    }
    

    条件编译的用处

    判断头文件中是否有相同的变量

    程序1global .h

    #ifndef _GLPBAL_H_
    #define _GLPBAL_H_
    int global = 10;
    #endif
    

    程序2test.h

    #include <stdio.h>
    #include "global.h"
    
    const char* NAME = "Hello world!";
    
    void f()
    {
        printf("Hello world!\n");
    }
    

    程序3test.c

    #include <stdio.h>
    #include "global.h"
    
    const char* NAME = "Hello world!";
    
    void f()
    {
        printf("Hello world!\n");
    }
    

    头文件global.h调用了两次,是不是重复调用呢?很显然,我们通过条件编译技术,防止了重复调用。

    条件编译的意义

    • 条件编译使得我们可以按照不同的条件编译不同的代码段
    • if...#else,,#endif被预编译器处理,而if..else语句被编译器处理,必然被编译进入目标代码
    • 实际工程中条件编译主要用于以下两种情况:
      • 不同的产品线公用一份代码
      • 区分编译产品的调试版和发布版

    最佳示例,区分编译产品的调试版和发布版

    #include <stdio.h>
    
    #ifdef DEBUG
        #define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
    #else
        #define LOG(s) NULL
    #endif
    
    #ifdef HIGH
    void f()
    {
        printf("This is the high level product!\n");
    }
    #else
    void f()
    {
    }
    #endif
    
    int main()
    {
        LOG("Enter main() ...");
        
        f();
        
        printf("1. Query Information.\n");
        printf("2. Record Information.\n");
        printf("3. Delete Information.\n");
        
        #ifdef HIGH
        printf("4. High Level Query.\n");
        printf("5. Mannul Service.\n");
        printf("6. Exit.\n");
        #else
        printf("4. Exit.\n");
        #endif
        
        LOG("Exit main() ...");
        
        return 0;
    }
    

    同一份代码我们通过 DEBUG,或者HIGH,LOW来控制,不同的版本.

    小结

    • 通过命令行能够定义宏
    • 条件编译可以避免重复包含头文件
    • 条件编译是在工程中开发中可以去边不同产品线的代码
    • 条件编译可以定义产品的发布版和调试版

    #include的困惑

    • #include的本质是将已经存在的文件内容嵌入到当前文件中
    • #include的间接包含同样会产生嵌入文件内容的动作

    当然这一切动作都是在编译预处理之前完成的


    #error和#line

    # error

    • #error用于生成一个编译错误的消息,并停止编译
    • 用法
    #error message
    注:message不需要用双引号包围
    
    

    最佳实例

    #include<stdio.h>
    int main()
    {
        #ifndef COMMAND
        #warning you have not dingYi COMMAND
        #error No COMMAND
        #endif
        printf("%s\n",COMMAND);
    } 
    
    

    #line

    用法一

    • #line用于强制指定新的行号和编译文件名,并对源程序的代码从新编号
    
    #include<stdio.h>
    #line 14 "hello.c"
    void f()
    {
        return 0;
    }
    void main()
    {
        f();
    }
    

    报错信息

    ello.c: In function ‘f’:
    hello.c:16:9: warning: ‘return’ with a value, in function returning void
    

    这里将line所在的行号改为14行,所以return 0为16行

    用法二

    我们也可以用line来指定是谁写的

    格式

    #line 1 "傻帽写的"
    

    #的本质是重定义LINEFILE

    /#error编译指示字用于自定义程序员特有的编译错误消息

    类似的,#warning用于生成编译警告信息,不会停止编译


    #pragma预处理分析

    • #pragma是编译器指示字,用于指示编译器完成一些特定的操作
    • #pragma说定义的很多指示字是编译器和操作系统独有的
    • #pragma在不同的编译器将是不可移植的
    • 一般用法 #pragma parameter(不同的parameter参数语法有不同的意义)

    pragma message

    • message参数在大多数的编译器中都有相似的实现
    • message参数在编译输出消息到编译输出窗口中
    • message可用于代码的版本控制

    最佳实例

    #include<stdio.h>
    
    #if defined ANDROID20
        #pragma message("the version is 20..")
        #define VERSION "ANDROID20"
    #else
        #pragma message("hehe")
    #endif
    
    int main()
    
    {
    
        printf("%s,\n",VERSION);
    
        return 0;
    
    }
    

    #pragma pack

    • 什么是内存对齐

      • 不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个接一个的排放,这就是对齐
    • 为什么需要内存对齐?

      • CPU对内存的读取不是连续的,而是分层块读取的,块的大小只能是1,2,4,8,16字节
      • 当读取操作的数据未对齐,则需要腾出总线周期来访问内存,因此性能会大打折扣
      • 某些硬件平台只能从规定的地址处取某些特定类型的数据,否则抛出异常
    • pragma pack能够改变编译器的默认对齐方式

      • #pragma pack(2)
        struct Test1
        {
            char c1;
            short s;
            char c2;
            int i;
        }
        #pragma pack()
        
    • struct 占用的内存大小

      • 第一个成员起始于0的偏移处

      • 每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐

        • 偏移地址和成员占用大小均需对其
        • 结构体成员的对齐参数为其所有成员使用的对齐参数的最大值
      • 结构体总长度必须为对齐参数的整数倍

    最佳演算

    演算.png
    从图中可以看出,一开始char c1起始位置为0,大小为1。第二个是short2个字节,所以第一个块分配完成,第二个块从c2开始,但是,i的大小为4所以,第二个块剩余部分无法填充,只能开第三个块.三个块的大小就是3*4=12 个字节

    如果我们把程序换下位置

    #include<stdio.h>
    struct S1{
            char c1;
            char s;
            short c2;
            int i;
    };
    int main(){
            struct S1 s1;
            printf("%d\n",(int)sizeof(struct S1));
            return 0;
    }
    
    

    大小就变成8个字节

    最佳示例

    #include <stdio.h>
    #pragma pack(8)
    struct S1
    {
        short a;
        long b;
    };
    struct S2
    {
        char c;
        struct S1 d;
        double e;
    };
    #pragma pack()
    int main()
    {
        struct S2 s2;
        
        printf("%d\n", sizeof(struct S1));
        printf("%d\n", sizeof(struct S2));
    
        return 0;
    }
    

    注意

    gcc没有八个字节对齐


    #和##运算符使用解析

    • #预处理指令开始指令
    • #运算符号用于在编译期将宏参数转换为字符串

    重要技巧点

    • 转化成字符串的函数
    #include<stdio.h>
    #define CONVERS(x) #x
    int main()
    {
        printf("%s\n",CONVERS(helloworld!));
        printf("%s\n",CONVERS(100));
        return 0;
    }
    

    输出的结果为hello world100

    • #运算符在宏中的妙用
    #include<stdio.h>
    #define CALL(f,p) (printf("CALL function %s\n",#f),f(p))
    int square(int n)
    {
        return n*n;
    }
    int f(int x)
    {
        return x;
    }
    void main()
    {
        printf("1.%d\n",CALL(square,4));
        printf("2.%d\n",CALL(f,10));
    }
    

    ##运算符用于在编译期沾粘两个符号

    #include<stdio.h>
    #define NAME(n) name##n
    int main()
    {
        int NAME(1);
        int NAME(2);
        NAME(1)=1;
        NAME(2)=2;
        printf("%d\n",NAME(1));
        printf("%d\n",NAME(2));
        return 0;
    }
    

    编译预处理后NAME(1)就变成NAME1,NAME(2)就变成NAME2

    最佳用法

    利用##定义结构类型

    超偷懒

     #include<stdio.h>
    #define STRUCT(type) typedef struct _tag_##type type;\
    struct _tag_##type
    
    STRUCT(Student)
    {
        char * name;
        int score;
    };
    void main()
    {
        Student s1;
        s1.name="hehe";
        s1.score=10;
        printf("%s\n",s1.name);
        printf("%d\n",s1.score);
    }
    

    相比

    typedef struct Student 
    {
        char * name;
        int score;
    }Student;
    

    简单好多

    相关文章

      网友评论

        本文标题:C语言编译预处理技术一本道来

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