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

编译&&预处理.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;

简单好多

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容