编译预处理

来源:互联网 发布:一搜网络 健身 编辑:程序博客网 时间:2024/06/08 05:55
转载前注明出处,欢迎转载分享
程序执行前
程序执行前经历了:
预处理器->编译器->汇编器->链接器
编译预处理
 
1)预处理过程:
  • 处理所有注释,以空格代替
  • 将所有#define删除,并展开所有宏定义
  • 处理条件编译指令#if,#ifdef,#elif,#else,#endif
  • 处理#include,展开被包含的文件
  • 保留编译器需要使用的#pragma指令

预处理指令:
1
gcc file.c hello.i


2)编译过程:
  • 对预处理文件进行一系列词法分析,语法分析和语义分析
                1)词法分析主要分析关键字,标示符,立即数等是否合法
                2)词法分析主要分析表达式是否遵循语法规则
                3)语义分析在语法分析的基础上进一步分析表达式是否合法
  • 分析结束后进行代码优化生成相应的汇编代码文件

编译指令: 
1
 
gcc file.c hello.s //gcc -S hello.i -o hello.s或由生成的hello.i生成hello.s


3)汇编过程:
  • 汇编器将汇编代码转变为机器可以执行的指令
  • 每个汇编语句几乎都对应一条机器指令

汇编指令:
1
gcc hello.s hello.o //gcc -c file.c -o hello.o

注意:因为.o文件内部为机器码(即二进制文件),所以生成的.o文件无法打开


4)链接器:
  • 链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。

静态链接:将库文件与.o文件绑定在一起(可能一个库拷贝给多个可执行程序)
缺点:占内存

动态链接:库文件在内存中存放,是共享的而不是程序的一部分,生成的.o文件在内存中找库文件然后生成可执行程序
缺点:每次执行程序需要找库文件,虽然节省内存,但是比静态链接耗时很多


小结:
编译器将编译工作主要分为预处理,编译和汇编三部
链接器的工作是把各个独立的模块链接为可执行程序
静态链接在编译期完成,动态链接在运行期间完成

---------------------------------------------------------------------------------------------------
宏表达式
  • 宏表达式在预编译期被处理,编译器不知道宏表达式的存在
  • 宏表达式用“实参”完全替代形参,不进行任何运算
  • 宏表达式没有任何的“调用”开销
  • 宏表达式不能出现递归定义
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include stdio.h >

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

int dim (int  array[])
{
    
return sizeof(array) sizeof(*array);
}

int main()
{
    
int a[] {12345};
    printf(
"%d\n"dim(a));
    printf(
"%d\n"DIM(a));

    
return 0;
}
上述代码则体现该宏定义的优势是函数无法实现的,函数形参intarray[]在接收参数的时候被当做是指针的形式,所以函数返回的结果为1,但是宏只是简单的替换,计算出的结果就是数组元素的个数。


宏定义的常量或表达式是否有作用域的限制?看如下代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include stdio.h >

int f1(int a, int b)
{
#define MIN(a,b) ((a) (b) b)
    
return MIN(a, b);
    
//#undef MIN(a,b)
}

int f2(int a, int b, int c)
{
    
return MIN(MIN(a, b), c);
}

int main()
{
    printf(
"%d\n"f1(21));
    printf(
"%d\n"f2(532));

    
return 0;
}
通过运行结果可知该代码说明宏定义不存在作用域的限制,如果我们想要对上述代码中宏定义进行作用范围的限制,则在上述代码中去掉注释,加入#undefMIN,即可限定该宏在f1中可用,此时运行上述代码时会提示f2中MIN未定义。


强大的内置宏:
编译预处理

宏日志示例代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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);  \
}

void func()
{
    LOG(
"Enter func()...");
    LOG(
"Exit  func()...");
}

int main()
{
    LOG(
"Enter main()...");
    func();
    LOG(
"Exit  main()...");
    
return 0;
}

头文件time.h所包含的库函数和库宏:
参考资料:http://wiki.jikexueyuan.com/project/c/c-standard-library-time-h.html 

1
#define   (x) ((x)-1)
对于上面的定义所代表的意思:
宏是简单的替换,实际上只有紧挨#define的那一块被替换为后面的部分即是将f替换成(x)((x)-1),尽管后面由多个空格隔开也不要紧,因为当宏经过预处理被替换到代码中时,我们知道空格不影响代码的运行。
------------------------------------------------------------------------------------------------
条件编译
条件编译的基本概念
  • 条件编译的行为类似于C语言中的if..else
  • 条件编译是预编译指示命令,用于控制是否编译某段代码

如下代码即为条件编译:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
#include stdio.h >

int main()
{
#if(C == 1)
    printf(
"this is first printf...\n");
#else
    printf(
"this is second printf...'\n");
#endif
    
return 0;
}
我们可以用gcc -E test.c -otest.i生成预编译文件,然后查看这段代码时main函数只有第一个printf,上述代码与普通的if..else语句不同点在于其处理在预编译时期即改变。但是如果用gcc-DC=0 -E test.c -otest.c时我们会发现预编译时保留的是第二个printf语句,这里说一下-DC=0的意思,即是将C定义为0。

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

那么间接包含同一个头文件就会产生编译错误,因为可能某个头文件声明的全局变量被声明多次,例如在global.h中声明int a=10;然而test.h头文件中也包含了global.h头文件,那么当test.c中既包含了global.h又包含了test.h的时候,则相当于调用了两次global.h,声明了两次全局变量a,那么肯定会出现编译错误,改进格式如下:
 C Code 
1
2
3
4
5
6
#ifndef _TEST_H_
#define _TEST_H_

//头文件内容

#endif
保持如上格式后即可随心所欲调用头文件多次而不产生重复和报错。


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

用于不同生产线的示例代码如下:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include stdio.h >

#ifndef 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模式: 
1
gcc DDEBUG test.c
运行结果:
[test.c:22] enter main()...
1.query information.
2.record information.
3.delete information.
4.exit.
[test.c:30] exit main()...

如果切换至普通模式:
1
gcc test.c
运行结果:
1.query information.
2.record information.
3.delete information.
4.exit.

切换至高级模式: 
1
gcc -DHIGH test.c
运行结果:
this is the high level product!
1.query information.
2.record information.
3.delete information.
4.high level query.
5.mannul service.
6.exit.

还可以切换至高级DEBUG模式:
[test.c:22] enter main()...
this is the high level product!
1.query information.
2.record information.
3.delete information.
4.high level query.
5.mannul service.
6.exit.
[test.c:38] exit main()...

小结:
  • 通过编译器命令行能够定义预处理器使用的宏
  • 条件编译可以避免重复包含头同一个头文件
  • 条件编译是在工程开发中可以区别不同产品线的代码
  • 条件编译可以定义产品的发布版和调试版
--------------------------------------------------------------------------------------------------
#error
用于生成一个编译错误消息,并停止编译

用法:
1
#error message

:message不需要用双引号包围
#error编译指示字用于自定义程序员特有的编译错误消息
类似的,#warning用于生成编译警告,但不会停止编译

示例代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
#include stdio.h >

int main()
{
#ifndef COMMAND
#warning Compilation be stoped...
#error No defined Constant Symbol COMMAND
#endif

    printf(
"%s\n"COMMAND);
    
return 0;
}
如果直接编译:gcc test.c,则会报错停止。
若果编译时进行宏定义: 
1
gcc DCOMMAND \"TestCommand\" test.c
即意思是定义宏COMMAND为字符串"TestCommand"(我们可以在预处理.i文件中查看其变化),此时编译运行正常。


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

用法:
1
#line number filename
:filename可省略

#line编译指示字的本质是重定义__LINE__和__FILE__

示例代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include stdio.h >

#line 14 "Hello.c"

void f()
{
    
return 0;
}

int main()
{
    printf(
"%d\n"__LINE__);
    printf(
"%d\n"__FILE__);
    f();
    
return 0;
}
代码结果为:
20
Hello.c

因为从#line的下一行开始本来应该为第4行,但我们指定下一行为14行,所以打印__LINE__的行号原本为第10行,现在则变为第20行。并且__FILE__打印出来的是我们用#line宏所指定的文件名。
-----------------------------------------------------------------------------------------------
#和##运算符的使用解析

#运算符
我们知道#是预处理指令的开始符,但是它还有另一种作用,#运算符还可以用于在预编译期将宏参数转换为字符串。
示例代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
#include stdio.h >

#define CONVERS(x) #x

int main()
{
    printf(
"%s\n"CONVERS(Hello world!));
    printf(
"%s\n"CONVERS(100));
    printf(
"%s\n"CONVERS(while));
    printf(
"%s\n"CONVERS(return));

    
return 0;
}
输出结果:
Hello world!
100
while
return

分布编译的话查看.i文件时,main中输出语句为:
printf("%s\n", "Hello world!");
printf("%s\n", "100");
printf("%s\n", "while");
printf("%s\n", "return");


宏还可以在调用函数之前打印所调用函数的名称(利用#打印字符串的作用来实现)
代码如下:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include stdio.h >

#define CALL(f, p) (printf("Call function %s\n"#f), f(p))  //逗号表达式的使用

int square(int n)
{
    
return n;
}

int f(int x)
{
    
return x;
}

int main()
{
    printf(
"%s\n"CALL(square, 4));
    printf(
"%s\n"CALL(f, 10));

    
return 0;
}

生成.i文件后主函数两条输出语句变为:
printf("1. %s\n", (printf("Call function %s\n", "square"),square(4)));
printf("2. %s\n", (printf("Call function %s\n", "f"),f(10)));

##运算符
用于在预编译期粘连两个符号,示例代码:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include stdio.h >

#define NAME(n) name##n

int main()
{
    
int NAME(1);
    
int NAME(2);

    NAME(
11;
    NAME(
22;

    printf(
"%d\n"NAME(1));
    printf(
"%d\n"NAME(2));

    
return 0;
}

输出结果:
1
2

通过单步编译生成.i文件查看其预处理时发现主函数如下:
 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
    
int name1;
    
int name2;

    name1 
1;
    name2 
2;

    printf(
"%d\n"name1);
    printf(
"%d\n"name2);

    
return 0;
}

以上为##粘连符的使用方法,看如下代码巧妙宏定义结构体

 C Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include stdio.h >

#define STRUCT(type) typedef struct _tag_##type type;\
struct _tag_##type

STRUCT(Student)
{
    
char *name;
    
int id;
}

int main()
{
    Student s1;
    Student s2;

    s1.name 
"s1";
    s1.id 
0;

    s2.name 
"s2";
    s2.id 
1;

    printf(
"%s\n"s1.name);
    printf(
"%s\n"s1.id);
    printf(
"%s\n"s2.name);
    printf(
"%s\n"s2.id);

    
return 0
;
}


预处理后的结构体实际是: 
1
2
3
4
5
6
typedef struct _tag_Student Student;
struct _tag_Student
{
    
char *name;
    
int id;
}



0 0
原创粉丝点击