【C语言】第十四章-程序的编译

第十四章 程序的编译

程序的编译过程

  在我们使用ide或者gcc的时候,编译器总是自动帮我们直接生成了可执行文件,但是在编一个过程那种还可细分为几个步骤,这几个步骤的说明则是这一章的重点。

预处理

  预处理是编译器对我们的代码进行的第一道处理 ,在这个过程中编译器会做以下几件事情。

  1、拷贝头文件。

  2、去掉注释。

  3、对宏展开。

  4、处理条件编译。

  我们在Linux中可以通过指令让gcc生成预处理后的文件。这里假设我有一个hello.c文件。

        gcc -E hello.c -o hello.i

  之后就会生成一个预处理后的文件,我们可以用vim打开看看其中的内容,会发现代码一下多了好几百行,这就是因为编译器将头文件中的内容全部拷贝了进来所导致的,同时会发现编译器在预处理期间确实就做了上述的4件事。

编译

  在预处理结束后编译器才真正开始进行编译,编译器会对整合的代码进行语义和语法分析处理,使我们的源码变成汇编代码。当然我们也有语句可以让gcc生成编译过后的文件。

        gcc -S hello.i -o hello.s

  编译后打开.s文件会发现我们的代码会转换为汇编语言,但是计算机也并不直接看得懂汇编语言,于是就有了下一步编译。

汇编

  在这一步里编译器会进行汇编将生成的汇编文件转换为机器码也就是二进制指令,这样计算机就看得懂了,我们可以用以下这段代码生成汇编结束的文件。

        gcc -c hello.s -o hello.o

  汇编结束后会发现代码我们就真的一个字符都看不懂了,这就是因为文件已经变成了二进制的指令,但是到此编译过程还没有结束,我们还差最后一步生成可执行文件。

链接

  在最后一步中会进行各个文件的链接,因为我们的各个文件都是分开进行编译的,就需要最后一步来将他们合并,因此最后一步往往有两个步骤,合并段表,合并符号表和符号表的重定位,具体可以理解为将项目中各个源文件进行合并统一管理。之后就可以生成可执行文件了。

        gcc hello.o -o Hello

  至此编译过程就结束了。

预处理详解

预处理符号

  在程序编写中有一些语言自带的在预处理中会进行处理的特殊的宏供我们使用,这里列举出一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
printf("当前的文件:%s\n", __FILE__);//__FILE__表示当前的文件
printf("当前的行号:%d\n", __LINE__);//__LINE__表示当前的行号
printf("文件被编译的日期:%s\n", __DATE__);//打印文件被编译的日期
printf("文件被编译的时间:%s\n", __TIME__);//打印文件被编译的时间
printf("是否遵循ANSI C标准:%s\n", __STDC__ != 0 ? "是" : "否");//如果编译器遵循ANSI C标准,它就是个非零值
}



[misaki@localhost 程序的编译]$ ./Main
当前的文件:main.c
当前的行号:5
文件被编译的日期:Mar 18 2019
文件被编译的时间:00:28:03
是否遵循ANSI C标准:是

  这些符号在预处理期间就会进行替换从而可以使用在各个场景下。

define详解

  宏定义是在预处理期间就被处理的宏,用提十分广泛,不光是定义常量,定义函数,定义别名都有着不可小觑的作用。

  但是在使用宏定义的过程中我们始终要记住宏就是文本替换,不过是将上面的代码替换了下来罢了,由这一特性宏既有优势也有劣势。

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
#include <stdio.h>
/* 续行符 \ */
//用宏定义函数想要换行的话再行尾+'\'
#define CHECK(fp) if (fp == NULL) \
{ \
printf("fopen failed! %s:%d\n", __FILE__, __LINE__); \
}
void Check(FILE* fp)
{
if(fp == NULL)
{
printf("fopen failed! %s:%d\n", __FILE__, __LINE__);
}
}
int main()
{
FILE* fp1 = fopen("./test.txt", "r");
CHECK(fp1);
Check(fp1);
FILE* fp2 = fopen("./test.txt", "r");
CHECK(fp2);
Check(fp2);
}


[misaki@localhost 程序的编译]$ ./a.out
fopen failed! hello.c:42
fopen failed! hello.c:17
fopen failed! hello.c:45
fopen failed! hello.c:17

  这个例子可以看出用宏定义写的函数会明确的将代码出错的行号准确的返回,让我们知道是哪个位置出现了错误,但是用普通的函数却做不到这一点,只会返回函数定义的地方,这也是得益于宏定义的特性。同时我们还要注意宏定义是没有参数检查的,因此我们再宏定义中的fp参数其实可以是任意类型的。

  同样宏定义也有着不少的缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#define ADD(x, y) x + y
#define MUL(x, y) x * y
int main()
{
int a = ADD(10, 20) * 10 + 20;
printf("a = %d\n", a);//纯文本替换这里就会出错
int a2 = MUL(10, 10 + 10);//也会出错
printf("a = %d\n", a2);
}



[misaki@localhost 程序的编译]$ ./a.out
a = 230
a = 110

  以上这两个例子我们都没有的到预期想要的结果,都是因为宏定义直接将文本拷贝至此因此没有优先顺序,导致计算没有按照预期进行,为此我们不得不多加几个括号。
  综上所述宏定义有利有弊,我们总结一下哪些情况一定要使用宏定义。

  1、打印日志的行号和文件。

  2、没有参数类型检查。

  3、要求开销更小。

‘#’和’##’

‘#’

  当我们使用printf()打印时不同的字符串是可以拼接的,我们使用以下语句printf("Hello""Misaki")会出现HelloMisaki的结果,我们利用这一点可以使用宏定义模拟实现printf()。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("the value = " FORMAT " is format\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i + 1);
}



[misaki@localhost 程序的编译]$ ./a.out
the value = 11 is format

  这样自然可以,但是这样只有当参数是字符串的时候才可以完成拼接,呢有没有什么方式可以将不是字符串的参数在宏中转换为字符串呢?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("the value = " #VALUE " is format\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i + 1);
//宏的参数 # 能把参数变成一个字符串,然后这个字符串就可以再代码中进行文本拼接
}

  这样即可将不是字符串的参数转换为字符串。

‘##’

  ##的作用是符号拼接,这个操作符十分强大,甚至允许拼接变量。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define ADD_TO_NUM(num , value) \
sum##num += value;//## 拼接可以拼接变量,这里变成了sum(num) => sum1
int main()
{
int sum1 = 10;
ADD_TO_NUM(1, 10);//等于给num1 + 10
printf("%d\n", sum1);
}

其他预处理指令

undef

  undef用于移除一个已经有了的宏定义。

1
2
3
#define SIZE 10
#undef SIZE//清除原来的宏定义
#define SIZE 5

条件编译

  当满足某个条件的时候进行编译,否则不编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
#if 0 //条件为真就编译,为假就不编译
printf("hehe\n");
#else
printf("haha\n");
#endif
}



[misaki@localhost 程序的编译]$ ./a.out
haha

  另一种条件编译。

1
2
3
#ifdef SIZE//如果SIZE被宏定义就编译以下代码
printf("haha\n");
#endif

pragma once

  头文件在预处理阶段会进行问呗拷贝合并到一个文件中,因此如果一个头文件多次引用就会导致重复定义从而报错,因此我们往往想要一个头文件只编译一次,因此我们就会使用pragma once告诉编译器这个文件只编译一次。我们还可以使用以下的代码替代它,不过效果并不如它,在一些特定的情况下也会出错。

1
2
3
4
#ifndef time
#define time
... ...
#endif

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助