C语言编译预处理技术一本道来
2017-12-04 10:46
281 查看
一个.c
程序,从人懂到计算机懂的流程
预编译(不会去报错,没有真正的到达编译环境)
处理所有的注释,以空格代替将所有的#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; }
答案为
3,我们通过编译预处理,就知道为什么了
最不像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 | 1 |
最佳实践
宏日志#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 }
条件编译的用处
判断头文件中是否有相同的变量程序1
global .h
#ifndef _GLPBAL_H_ #define _GLPBAL_H_ int global = 10; d4f5 #endif
程序2
test.h
#include <stdio.h> #include "global.h" const char* NAME = "Hello world!"; void f() { printf("Hello world!\n"); }
程序3
test.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 "傻帽写的"
#的本质是重定义LINE和FILE
/#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中较小的一个进行对齐
偏移地址和成员占用大小均需对其
结构体成员的对齐参数为其所有成员使用的对齐参数的最大值
结构体总长度必须为对齐参数的整数倍
最佳演算
从图中可以看出,一开始
char c1起始位置为0,大小为1。第二个是
short占
2个字节,所以第一个块分配完成,第二个块从
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 world和
100
#运算符在宏中的妙用
#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语言——条件编译及编译预处理阶段
- C语言条件编译及编译预处理阶段
- Objective--C语言预处理命令之条件编译(#ifdef,#else,#endif,#if等)
- C语言条件编译及编译预处理阶段
- C语言预处理 编译 汇编 链接四个阶段
- c语言笔记3-编译预处理
- C语言编译预处理
- 【C语言】预处理指令—条件编译
- C语言和c++预处理命令之条件编译(#ifdef,#else,#endif,#if等)
- C语言条件编译及编译预处理阶段
- c语言的预处理指令分3种 1> 宏定义 2> 条件编译 3> 文件包含
- C语言预处理命令之条件编译
- C语言条件编译及编译预处理阶段
- C语言条件编译及编译预处理阶段
- C语言预处理命令之条件编译(#ifdef,#else,#endif,#if等)
- C语言预处理命令之条件编译(#ifdef,#else,#endif,#if等)
- 【转】C语言条件编译及编译预处理阶段
- C语言预处理命令之条件编译(#ifdef,#else,#endif,#if等)
- C语言的预处理、编译、汇编、链接
- 【C语言】14 预处理指令--条件编译