C语言中编译预处理命令作用,C语言编译预处理技术一本道来

本文详细介绍了C程序从源代码到可执行文件的编译流程,包括预处理、编译、汇编和链接四个阶段。重点讲解了预处理阶段的宏定义、条件编译指令及其在实际编程中的应用,如实现日志记录、避免头文件重复包含、区分产品版本等。同时,文章探讨了内存对齐的概念和pragma指令在调整内存布局中的作用。
摘要由CSDN通过智能技术生成

2e5bbf63cd9b

编译&&预处理.png

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

2e5bbf63cd9b

编译流程.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

#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

#define MIN(b,c)((b)

int main()

{

int a=2,b=5;

printf("%d\n",MIN(a++,b));

return 0;

}

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

最不像C语言的C语言

#include

#include

#define MALLOC(type,n) (type*)malloc(sizeof(type)*n)

#define FOREACH(b,e) for(i=b;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

#include

#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

#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

#include "global.h"

const char* NAME = "Hello world!";

void f()

{

printf("Hello world!\n");

}

程序3test.c

#include

#include "global.h"

const char* NAME = "Hello world!";

void f()

{

printf("Hello world!\n");

}

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

条件编译的意义

条件编译使得我们可以按照不同的条件编译不同的代码段

if...#else,,#endif被预编译器处理,而if..else语句被编译器处理,必然被编译进入目标代码

实际工程中条件编译主要用于以下两种情况:

不同的产品线公用一份代码

区分编译产品的调试版和发布版

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

#include

#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

int main()

{

#ifndef COMMAND

#warning you have not dingYi COMMAND

#error No COMMAND

#endif

printf("%s\n",COMMAND);

}

#line

用法一

#line用于强制指定新的行号和编译文件名,并对源程序的代码从新编号

#include

#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 "傻帽写的"

#的本质是重定义LINE和FILE

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

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

#pragma预处理分析

#pragma是编译器指示字,用于指示编译器完成一些特定的操作

#pragma说定义的很多指示字是编译器和操作系统独有的

#pragma在不同的编译器将是不可移植的

一般用法 #pragma parameter(不同的parameter参数语法有不同的意义)

pragma message

message参数在大多数的编译器中都有相似的实现

message参数在编译输出消息到编译输出窗口中

message可用于代码的版本控制

最佳实例

#include

#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中较小的一个进行对齐

偏移地址和成员占用大小均需对其

结构体成员的对齐参数为其所有成员使用的对齐参数的最大值

结构体总长度必须为对齐参数的整数倍

最佳演算

2e5bbf63cd9b

演算.png

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

如果我们把程序换下位置

#include

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

#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

#define CONVERS(x) #x

int main()

{

printf("%s\n",CONVERS(helloworld!));

printf("%s\n",CONVERS(100));

return 0;

}

输出的结果为hello world和100

#运算符在宏中的妙用

#include

#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

#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

#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;

简单好多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值