编译模块

image-20220220191405043

image-20220220191423699

预编译

  • 处理所有的注释,以空格代替
  • 将所有的#define删除,并且展开所有的宏定义
  • 处理条件编译指令#if, #ifdef, #elif, #else, #endif
  • 处理#include ,展开被包含的文件
  • 保留编译器需要使用的#pragma指令

预处理指令示例: gcc-E file.c -o file.i

范例

test.c与test.h如图所示

image-20220220114546049

执行gcc -E test.c -o test.i预处理过后,可以观察到

  1. 注释消失了。
  2. include头文件test.h里的东西(两个全局变量)原封不动的复制到test.i中了。
  3. 宏消失了并且宏直接展开全部被替换了。
  4. 多了#号开头的内容,这些内容是作为传递给后续的编译器的输入内容。

image-20220220114640567

编译

1.对预处理文件进行词法分析,语法分析和语义分析(详情请见编译原理)

  • 词法分析:分析关键字,标示符,立即数等是否合法
  • 语法分析:分析表达式是否遵循语法规则
  • 语义分析:在语法分析的基础上进一步分析表达式是否合法

2.分析结束后进行代码优化生成相应的汇编代码文件
编译指令示例: gcc -S file.c -o file.s

image-20220220115621246

汇编

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

汇编指令示例: gcc-c file.s -o file.o

1
2
3
4
5
fengyun@ubuntu:~/share$ gcc -E test.c -o test.i
fengyun@ubuntu:~/share$ gcc -S test.i -o test.s
fengyun@ubuntu:~/share$ gcc -C test.s -o test.o
fengyun@ubuntu:~/share$ ./test.o
fengyun@ubuntu:~/share$

test.o不是可执行文件,是一个二进制文件,无法执行。

链接

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

image-20220220191446745

静态链接

由链接器在链接时将库的内容直接加入到可执行程序中。a.out包含file1.o,file2.o,libc.a三个文件,运行的时候与原始的file1.o,file2.o,libc.a三个文件没有任何关系,不需要它们就可以直接运行。

image-20220220191456232

Linux下静态库的创建和使用

  • 编译静态库源码: gcc -c lib.c -o libo

  • 生成静态库文件: ar -q lib.a lib.o

  • 使用静态库编译: gcc main.c lib.a -o main.out

image-20220220191509821

1
2
3
4
5
6
7
8
fengyun@ubuntu:~/share$ gcc -c slib.c -o slib.o
fengyun@ubuntu:~/share$ ar -q slib.a slib.o
ar: 正在创建 slib.a
fengyun@ubuntu:~/share$ gcc test1.c slib.a -o test1.out
fengyun@ubuntu:~/share$ ./test1.out
Name: Static Lib
Result: 5
fengyun@ubuntu:~/share$

Linux ar命令用于建立或修改备存文件,或是从备存文件中抽取文件。

ar可让您集合许多文件,成为单一的备存文件。在备存文件中,所有成员文件皆保有原来的属性与权限。

动态链接

  • 可执行程序在运行时才动态加载库进行链接
  • 库的内容不会进入可执行程序当中

image-20220220191521275

lib1.so和lib2.so动态库生成的stub1和stub2,是最终生成的可执行程序可以使用的内容,程序看不到其它内容。

Linux下动态库的创建和使用

  • 编译动态库源码: gcc -shared dlib.c -o dlib.so
  • 使用动态库编译: gcc main.c -Idl -o main.out
  • 关键系统调用
    dlopen:打开动态库文件
    dIsym:查找动态库中的函数并返回调用地址
    dlclose:关闭动态库文件

image-20220220122258494

1
2
3
4
5
fengyun@ubuntu:~/share$ gcc -shared dlib.c -o dlib.so
fengyun@ubuntu:~/share$ gcc test2.c -ldl -o test2.out
fengyun@ubuntu:~/share$ ./test2.out
Name: Dynamic Lib
Result: 5

删除库文件之后,运行失败。

1
2
3
fengyun@ubuntu:~/share$ rm dlib.so
fengyun@ubuntu:~/share$ ./test2.out
Cannot open lib ...

动态链接和静态链接根据用户不同需求而产生的。比如部分更新需要动态链接,一些小程序通常静态链接。

总结

编译过程分为预处理,编译,汇编和链接四个阶段

  • 预处理:处理注释,宏以及已经以#开头的符号

  • 编译:进行词法分析,语法分析和语义分析等

  • 汇编:将汇编代码翻译为机器指令的目标文件

  • 链接是指将目标文件最终链接为可执行程序
    根据链接方式的不同,链接过程可以分为:1.静态链接:目标文件直接链接进入可执行程序。2.动态链接:在程序启动后才动态加载目标文件

宏定义与使用分析

  • #define是预处理器处理的单元实体之一
  • #define定义的宏可以出现在程序的任意位置
  • #define定义之后的代码都可以使用这个宏
  • #define定义的宏常量可以直接使用
  • #define定义的宏常量本质为字面量
1
2
3
4
5
#define ERROR -1
#define PATH1 "D:\test\test.c"
#define PATH2 D:\test\test.c
#define PATH3 D:\test\
test.c

以上四个宏定义:对对错错

define字面量不占用内存,const常量是一个变量占用内存,,本质区别。

1
2
fengyun@ubuntu:~/share$ gcc -E test.c -o test.i
fengyun@ubuntu:~/share$

预编译过程预处理器并未报错,预处理器并不会进行语法检查。

image-20220220142441456

如果直接编译那么会出错,字面量不符合C语言的语法规则。

image-20220220142208189

  • #define表达式的使用类似函数调用
  • #define表达式可以比函数更强大
  • #define表达式比函数更容易出错
1
2
3
#define _SUM_(a, b) (a) + (b)
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))
#define _DIM_(a) sizeof(a)/sizeof(*a)

求解数组的大小,这个用函数很难做到。

image-20220220143317299

1
2
3
4
5
fengyun@ubuntu:~/share$ ./test
s1 = 3
s2 = 5
m = 2
d = 4

宏表达式与函数比对

  • 宏表达式被预处理器处理,编译器不知道宏表达式的存在
  • 宏表达式用“实参”完全替代形参,不进行任何运算
  • 宏表达式没有任何的“调用”开销
  • 宏表达式中不能出现递归定义
1
2
#define _SUM_(n) ((n > 0)?( _SUM_(n-1) + n):0)
ints = _SUM_(10);//错误

宏没有作用域

image-20220220144003905

1
2
3
4
fengyun@ubuntu:~/share$ gcc -E test.c -o test.i
fengyun@ubuntu:~/share$ gcc test.c -o test
fengyun@ubuntu:~/share$ ./test
fengyun@ubuntu:~/share$

一个本应报错的程序却并没有报错,因为宏不存在定义域。

强大的内置宏

image-20220220191633452

宏应用

对于C语言的函数无法办到以下宏实现的功能。

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
#include <stdio.h>
#include <malloc.h>

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

#define FREE(p) (free(p), p=NULL)

#define LOG(s) printf("[%s] {%s:%d} %s \n", __DATE__, __FILE__, __LINE__, s)

#define FOREACH(i, m) for(i=0; i<m; i++)
#define BEGIN {
#define END }

int main()
{
int x = 0;
int* p = MALLOC(int, 5);

LOG("Begin to run main code...");

FOREACH(x, 5)
BEGIN
p[x] = x;
END

FOREACH(x, 5)
BEGIN
printf("%d\n", p[x]);
END

FREE(p);

LOG("End");

return 0;
}
1
2
3
4
5
6
7
8
fengyun@ubuntu:~/share$ ./test
[Feb 20 2022] {test.c:19} Begin to run main code...
0
1
2
3
4
[Feb 20 2022] {test.c:33} End

小结

  • 预处理器直接对宏进行文本替换
  • 宏使用时的参数不会进行求值和运算
  • 预处理器不会对宏定义进行语法检查
  • 宏定义时出现的语法错误只能被编译器检测
  • 宏定义的效率高于函数调用
  • 宏的使用会带来一定的副作用

条件编译使用分析

条件编译的行为类似于C语言中的if..else…

条件编译时预编译指示命令,用于控制是否编译某段代码

执行gcc -E test.c -o test.i并且查看test.i

image-20220220150220349

条件编译的本质

  • 预编译器根据条件编译指令有选择的删除代码
  • 编译器不知道代码分支的存在
  • if…else…语句在运行期进行分支判断
  • 条件编译指令在预编译期进行分支判断
  • 可以通过命令行定义宏
1
2
gcc -Dmacro=value file.c
gcc -Dmacro file.c

我把上面的test.c文件中#define C 1语句删除。改为命令行定义宏,观察输出结果仍然相同。

1
2
3
fengyun@ubuntu:~/share$ gcc -DC=1 test.c -o test
fengyun@ubuntu:~/share$ ./test
This is first printf...

#include的本质

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

例如#include<stdio.h>告诉预处理器要将当前代码调整,将stdio.h文件所有内容都复制到当前文件里面来。

可能存在重复定义的情况比如我有这样三个文件:

test.c如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "test.h"
#include "global.h"

int main()
{
const char* s = hello_world();
int g = global;

// printf("%s\n", NAME);
// printf("%d\n", g);

return 0;
}

global.h如下

1
int global = 10;

test.h如下:

1
#include "global.h"const char* NAME = "test.h";char* hello_world(){    return "Hello world!\n";}

注意test.h已经包含了global.h,test.c也包含了global.h,这样的话test.c预编译结果中会有两条int global = 10;

image-20220220191701144

如果编译运行会产生错误。

解决方法是添加条件编译
global.h中添加宏判断,第一次处理的时候int global = 10;会保留下来,第二次又来读取的时候就会删除。

1
2
3
4
5
#ifndef _GLOBAL_H_
#define _GLOBAL_H_
int global = 10;

#endif
1
2
3
4
5
#ifndef _TEST_H_
#define _TEST_H_
#include "global.h"const char* NAME = "test.h";char* hello_world(){ return "Hello world!\n";}

#endif

条件编译的意义

  • 条件编译使得我们可以按照不同的条件编译不同的代码段,删除或保留我们想要的代码
  • #if…#else…#endif被预编译器处理,而if…else…语句被编译器处理会被编译进目标代码
  • 实际工程条件编译用途:

​ 不同产品线共用同一份代码
​ 区分编译产品的调试版和发布版

工程开发模型

product.h如下,设置产品是发布版或调试版

1
2
#define DEBUG 1
#define HIGH 1
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
41
42
#include <stdio.h>
#include "product.h"

#if DEBUG
#define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
#else
#define LOG(s) NULL
#endif

#if 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");

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

#error

#error用于生成一个编译错误消息

用法: #error message
message不需要用双引号包围

#error编译指示字用于自定义程序员特有的编译错误消息
类似的,#waring用于生成编译警告

#error是一种预编译指示字,可用于提示编译条件是否满足

例如,__cplusplus是C++特有的一个宏,如果我们用的编译器不支持C++那么是不会预定义这个宏,运行的时候将会打印提示信息

1
2
3
#ifndef __cplusplus
#error This file should be processed with C++ compiler
#endif

image-20220220162037889

如图所示,gcc编译器报了许多错误。

而我们加上__cplusplus宏定义判断后,会打印我们自己定义的错误信息—即This file should be processed with C++ compiler.

image-20220220162143593

在实际工程开发中,如下所示,未定义宏PRODUCT直接gcc test.c -o test如果缺少#error打印信息那么会导致功能不完备,添加打印信息后会提示用户定义PRODUCT。

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
#include <stdio.h>

void f()
{
#if ( PRODUCT == 1 )
printf("This is a low level product!\n");
#elif ( PRODUCT == 2 )
printf("This is a middle level product!\n");
#elif ( PRODUCT == 3 )
printf("This is a high level product!\n");
#else
#error The "PRODUCT" is NOT defined!//未定义PRODUCT
#endif
}

int main()
{
f();

printf("1. Query Information.\n");
printf("2. Record Information.\n");
printf("3. Delete Information.\n");

#if ( PRODUCT == 1 )
printf("4. Exit.\n");
#elif ( PRODUCT == 2 )
printf("4. High Level Query.\n");
printf("5. Exit.\n");
#elif ( PRODUCT == 3 )
printf("4. High Level Query.\n");
printf("5. Mannul Service.\n");
printf("6. Exit.\n");
#endif

return 0;
}

如果将#error改为#warning后,编译仍然会产生warning信息但是仍然会生成可执行文件。

#line

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

用法:#line number filename
filename可省略

#line 编译指示字的本质是重定义__LINE____FILE__

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
// The code section is written by A
int main()
{
printf("%s : %d\n",__FILE__,__LINE__);

#line 1 "fengyun.c"

printf("%s : %d\n",__FILE__,__LINE__);

return 0;
}
1
2
3
4
fengyun@ubuntu:~/share$ gcc test.c -o test
fengyun@ubuntu:~/share$ ./test
test.c : 5
fengyun.c : 2

#progma

  • #progma用于指示编译器完成一些特定的动作
  • #progma所定义的很多指示字是编译器特有的
  • #progma在不同的编译器间是不可移植的
    预处理器将忽略他不认识的#progma指令
    不同的编译器可能以不同方式解释同一条#progma指令
    

一般用法:#progma parameter
不同的parameter参数语法和意义各不相同

#progma message

  • message参数在大多数的编译器中都有相似的实现
  • message参数在编译时输出消息到编译输出窗口中
  • message用于条件编译中可提示代码的版本信息
1
2
3
4
#if define(ANDROID20)
#progma message("Compile Android SDK2.0...")
#define VERSION "Android2.0"
#endif

与#error和#warning不同,#progma message仅仅代表一条编译消息,不代表代码有任何问题。

image-20220220165830091

image-20220220165748506

image-20220220165805231

#progma once

#progma once用于保证头文件制备编译一次

#progma once时编译器相关的,不一定被支持。

image-20220220191725620

第一种方式是被C语言支持的,通过宏来控制头文件内容只嵌入一次,预处理器仍然处理了多次。

第二种方式#pragma once告诉编译器只需要编译一次即可,预处理器只需会处理一次,效率更高。

既保证一次性又保证效率

1
2
3
4
5
6
7
8
#ifndef _GLOBAL_H_
#define _GLOBAL_H_

#pragma once

int g_value = 1;

#endif

#progma pack

什么是内存对齐?

  • 不同类型的数据在内存中按照一定的规则排列
  • 而不一定是顺序的一个接一个的排列

image-20220220191752080

执行sizeof打印两个test结构体的大小的程序

1
2
3
fengyun@ubuntu:~/share$ ./test
sizeof(Test1) = 12
sizeof(Test2) = 8

image-20220220191813342

为什么内存对齐?

  • CPU对内存的读取不是连续的,而是分块读取的,块的大小只能是1,2,4,8,16…字节
  • 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣
  • 某些硬件平台只能从规定相对地址处读取特定类型的数据,否则产生硬件异常

image-20220220191821008

**注意是#pragma pack (1)**。

1
2
3
fengyun@ubuntu:~/share$ ./test
sizeof(Test1) = 8
sizeof(Test2) = 8

stuct占用内存大小

1.第一个成员起始于 0偏移处

2.每个成员按照类型大小和pack参数中较小的一个对齐
偏移地址必须能够被对齐参数整除
结构体成员的大小取其内部长度最大的数据成员作为其大小

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

4.编译器在默认情况下按照4字节对齐 (即默认#progma pack(4)

image-20220220172537252

一道微软面试题

先算对齐参数min(pack,size),再计算偏移地址(能够被对齐参数整除)

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(8)

struct s1{ //对齐参数 偏移地址 大小
short a; //2 0 2
long b; //4 4 4
} //8字节

struct s2{ //对齐参数 偏移地址 大小
char c; //1 0 1
struct s1 d;//4 4 8
double e; //8 16 8
} //24字节
#progma pack()

s1和s2大小分别为8字节和24字节。

#运算符和##运算符

#运算符

  • #运算符用于在预处理期将宏参数转换为字符串
  • #的转换作用是在预处理期完成的,因此只在宏定义中有效
  • 编译器是不知道#的转换作用的。
  • 用法:
1
2
#define STRING(x) #x
printf("%s\n",STRING(Hello World!));

image-20220220174211004

动态的知道函数的函数名,用#运算符转换。

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
#include <stdio.h>

#define CALL(f, p) (printf("Call function %s\n", #f), f(p))

int square(int n)
{
return n * n;
}

int func(int x)
{
return x;
}

int main()
{
int result = 0;

result = CALL(square, 4);

printf("result = %d\n", result);

result = CALL(func, 10);

printf("result = %d\n", result);

return 0;
}
1
2
3
4
5
6
fengyun@ubuntu:~/share$ gcc test.c -o test
fengyun@ubuntu:~/share$ ./test
Call function square
result = 16
Call function func
result = 10

image-20220220174628149

##运算符

  • ##运算符用于在预处理器粘连两个标识符
  • ##的连接作用是在预处理期完成的,因此只在宏定义中有效
  • 编译器不知道##的连接作用
  • 用法image-20220220191835929

image-20220220192440276

image-20220220175125799

##运算符解决在工程代码里定义成百上千个结构体。

创建一个结构体,并且用typedef给结构起别名。将这两个操作合二为一,非##宏定义莫属。

image-20220220175617858