7. 程序的编译(预处理)+链接
7.1 程序的翻译和执行环境
在ANSI C的实现中,存在翻译和执行两个环境。翻译环境,将源代码转换为可执行文件;执行环境执行可执行文件。
7.2 编译+链接
7.2.1 翻译环境
翻译包括编译和链接两个步骤。C语言的编译又包括预处理、编译和汇编。依次用到预处理器(preprocessor)、编译器(compiler)、汇编器(assembler)、链接器(linker)。
7.2.2 编译和链接步骤
gcc编译器为例。
-
预编译(预处理): — 文本操作
- 完成头文件的包含
#include
- 宏的替换
#define
- 注释删除
gcc -E test.c -o test.i # 对test.c预处理 并将结果报错到 test.i 文件中
- 完成头文件的包含
-
编译: — 将C代码转化为汇编代码
- 语法分词
- 词法分析
- 语义分析
- 符号汇总
gcc test.i -S # 汇编 生成test.s 文件
-
汇编: — 把汇编代码转化为机器指令(二进制可执行文件)
- 生成符号表
gcc test.s -c # 生成 test.o 或 test.obj 目标文件 # 文件格式为 elf (可以使用readelf工具打开)
objdump -h test.o # 查看 test.o 文件结构 # .text 代码段(存放函数的二进制机器指令) # .data 数据段(存已初始化的局部/全局静态变量、未初始化的全局静态变量) # .bss bss段(声明未初始化变量所占大小) # .rodata 只读数据段(存放 " " 引住的只读字符串) # .comment注释信息段 # .node.GUN-stack :堆栈提示段
-
链接: — 把多个目标文件和链接库进行链接
- 合成段表
- 符号表的合并和重定位(不同的对象文件之间保存了彼此之间的引用,所以需要在链接期间需要整合这些定位)
# 生成 test.out 文件 elf格式 # 链接阶段发现被调用的函数未定义
# 查看文件结构 readelf -h a.out objdump -h a.out
7.2.3 运行环境
- 程序的执行过程
- 加载器(loader)将程序载入内存。
- 有操作系统的环境中:一般由操作系统完成程序载入内存操作。
- 独立的环境(裸机)中:程序手工载入内存,或通过可执行代码置入只读内存中。
- 程序运行。调用
main
函数。 - 开始执行程序代码。程序使用运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储与静态内存中的变量在程序的整个执行过程一直存在。
- 终止程序。终止main函数,或异常结束。
- 加载器(loader)将程序载入内存。
- 《程序员的自我修养》
7.3 预处理
7.3.1 预处理器
C 预处理器不是编译器的组成部分,而是编译过程中一个单独的步骤。C 预处理器只是一个文本替换工具,会指示编译器在实际编译之前完成所需的预处理。 C 预处理器(C Preprocessor)简写为 CPP。预处理器命令以
#
开头,从第一列开始。
- 重要的预处理指令
指令 描述 #define
定义宏 #include
包含一个源代码文件 #undef
取消已定义的宏 #ifdef
如果宏已经定义,则返回真 #ifndef
如果宏没有定义,则返回真 #if
如果给定条件为真,则编译下面代码 #else
#if 的替代方案 #elif
如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 #endif
结束一个 #if……#else 条件编译块 #error
当遇到标准错误时,输出错误消息 #pragma
使用标准化方法,向编译器发布特殊的命令到编译器中
7.3.2 宏
- 定义宏
预处理过程中替换。目的是提高代码的可读性。
// 使用示例
#define MAX 1024
// 定义宏 告诉C预处理器 将源文件中所有的MAX替换成1024
#undef MAX
// 取消已定义宏
#ifdef MAX // 判断是否定义
#undef MAX // 判断为真的操作...
#endif // 结束判断
// #ifdef 和 #endif是一对 上面代码表示 如果定义了MAX 则取消MAX的定义
#ifndef MAX // 判断是否没有定义
#define MAX 1024 // 判断为真的操作...
#endif // 结束判断
// 上面代码表示 如果没有定义MAX 则定义MAX为1024
// 特殊的
#ifdef DEBUG
/* Your debugging statements here */
#endi
// 该指令告诉 CPP 如果定义了 DEBUG 则执行处理语句
// 在编译时 如果向 gcc 编译器传递了 -DDEBUG 开关量 这个指令就非常有用
// 它定义了 DEBUG 您可以在编译期间随时开启或关闭调试
#define
定义的标识符是否需要在最后加上;
即:#define MAX 1024;
// 答案:不需要
// #define定义的标识符完全替换
// 比如:
#define MAX 1024
int main()
{
printf("%d\n", MAX); // 程序正常运行 打印1024
return 0;
}
// 而如果有分号 ;
#define MAX 1024;
int main()
{
printf("%d\n", MAX);
// 替换之后 printf语句为
// printf("%d\n", 1024;);
// 显然程序异常
return 0;
}
-
预定义宏
C语言中的标识符分为预定义标识符和用户标识符。用户标识符即通常使用的变量名,而预定义标识符在C语言中有特定的含义。
__FILE__ // 进行编译的源文件 __LINE__ // 文件当前的行号 __DATE__ // 文件被编译的日期 __TIME__ // 文件被编译的时间 __STDC__ // 如果编译器遵循ANSI C,则其值为1,否则未定义 __func__ // 当前运行的函数 有些编译器中是 __FUNCTION__ // 预定义标识符可用于书写日志
-
实例
#include <stdio.h> int main() { printf("%s\n", __FILE__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); printf("%s\n", __TIME__); //printf("%s\n", __STDC__); // VS中未定义 return 0; }
-
-
参数化的宏
可以使用参数化的宏模拟函数。在使用带有参数的宏之前,必须使用
#define
指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。// 例 // 函数求和 int Add(int x, int y) { return x + y; } // 宏求和 #define ADD(X, Y) ((X)+(Y))
宏是完全替换的。
// 例 #include <stdio.h> #define MUL(x, y) x * y int main() { printf("%d\n", MUL(5, 4)); // 20 printf("%d\n", MUL(2 + 3, 4)); // 14 } // MUL(5,4) 与 MUL(2+3,4) 的结果并不相同 因为宏是完全替换的 // MUL(5,4) 相当于 5*4 // MUL(2+3,4) 相当于 2+3*4 // 所以修改MUL宏如下 #define MUL(x,y) ((x) * (y)) // 通过给参数添加适当的小括号 提高优先级 从而避免这种问题
带副作用的参数。
// 何为副作用 // 例如: a = b++; // 将b的复制给a 之后b又自增 则称该语句具有副作用
// 例(带副作用的参数) #define MAX(a, b) ((a)>(b)?(a):(b)) int main() { int a = 3; int b = 4; printf("%d\n", MAX(a++, b++)); // 5 printf("%d\n", a); // 4 printf("%d\n", b); // 6 return 0; } // MAX宏 返回a、b中较大值 // 本来只a、b只分别自增一次 // 而通过宏替换 使得b自增了两次 // 如果之后再使用a、b变量 则出现错误的结果
-
#define
** 替换规则**- 调用宏时,首先对参数进行检查,确定参数是否包含任何由
#define
定义的符号,如果有则先替换参数。 - 替换后再将程序放到原位置。对于宏,参数名被值替换。
- 最后,再对结果文件进行扫描,确定文件中没有任何由
#define
定义的符号。如果有,重复上述操作。
- 调用宏时,首先对参数进行检查,确定参数是否包含任何由
-
宏和函数对比
属性 **#define**
定义宏函数 代码长度 每次使用,宏代码都会插入程序中。如果宏较大,则程序会增长。 每次调用函数,去函数的地址处,执行函数。 执行速度 较快。 函数调用和函数返回会有额外的开销(内联函数除外)。 参数 完全替换。 实参赋值给形参。 参数类型 宏的参数与类型无关。 函数参数与类型有关,对于不同类型的参数需要不同的函数。 调试 宏在预编译时完成替换,而调试是在程序编译完成后运行时的,所以宏不便于调试。 可逐语句调试。 递归 不支持。 支持。 -
补充
- 通常定义宏时,习惯上将标识符全大写;
- 宏参数和
#define
定义中可以出现其他#define
定义的变量; - 宏中不能出现递归;
- CPP搜索
#define
定义的符号时,字符串常量的内容并不会被搜索; - 使用宏时应避免使用自增、自减操作,因为宏是替换,而不像函数将实参赋值给形参,所以也就无法预测自增、自减操作的执行次数(即宏的带副作用的参数)。
7.3.3 预处理器运算符
C语言提供3个运算符
\
、#
、##
辅助创建宏。显然,这些运算符只能在创建宏时使用。
-
宏延续运算符
\
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符
\
。// 例 #include <stdio.h> #define print(a) \ printf(#a) int main() { print(1); return 0; } // 输出: // 1
-
字符串常量化运算符
#
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符
#
。在宏中使用的该运算符有一个特定的参数或参数列表。// C语言中的字符串 printf("hello" " " "world"); // hello world // 并不会产生错误 而是将三个字符串常量拼接并输出 // 例 #include <stdio.h> #define print(a) printf("输出:"#a"\n") int main() { print(hello c!); // 输出:hello c! // 相当于 // printf("输出:""hello c!"\n"); // 相当于 // printf("输出:hello c!\n"); return 0; } // print中 可以任意传参 // print 可将要打印的数据 有输出提示 并自动换行
-
标记粘贴运算符
##
宏定义内的标记粘贴运算符(
##
)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。// 例 #include <stdio.h> #define i(n) printf("i"#n"=%d\n", i##n) int main() { int i1 = 8; int i2 = 1024; int i3 = i1 * i2; i(1); // 相当于 // printf("i""1""=%d\n", i1); // i##n 使 i和n 拼接后标识符i1 // 注意:在该标识符出现之前 i1必须是已定义的 否则报错 i(2); i(3); return 0; } // 输出结果 // i1=8 // i2=1024 // i3=8192
-
defined()
运算符用在常量表达式中的,用来确定一个标识符是否已经使用
#define
定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。// 例 #if !defined(MAX) #define MAX 1024 #endif // #if和#endif是一对 // 该段代码表示 如果没有定义MAX 则定义MAX为1024 // 预处理指令 #elif、 #else 等使用同理
7.3.4 头文件
头文件是扩展名为
.h
的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的用户头文件和编译器自带的系统头文件。使用头文件需要使用预处理指令#include
。引用头文件相当于复制头文件中的内容。建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
-
语法
使用
#include
可以引用系统头文件和用户头文件。// 形式1:用于引用系统头文件 在系统目录的标准列表中搜索名为 file.h 的文件 #include <file.h> // 形式2:用于引用用户头文件 在包含当前文件的目录中搜索名为 file.h 的文件 #include "file.h"
-
区别
#include <file.h>
和#include "file.h"
的查找策略不同。#include <file.h>
在类库目录中查找linux
系统在目录/user/include
中windows
系统在vs
安装目录中#include "file.h"
先在项目目录下查找file.h
头文件,如果没有找到,再去类库目录下查找
显然,应该根据不同的使用场景选择使用哪种包含方式。包含系统头文件使用
#include <>
显然查找更快,也可以避免用户头文件和系统头文件重名的情况。 -
多次引用
如果一个头文件被引用两次,编译器会处理两次头文件的内容。所以在源文件中就会有两份头文件,尽管程序有时可能正常运行,但这显然是没有必要的。C语言中可以通过条件编译语句和
#pragma
两种方法解决这种问题。-
条件编译语句
#ifndef HEADER_FILE #define HEADER_FILE the entire header file #endif // 这种结构即包装器#ifndef // 当再次引用头文件时 因为 HEADER_FILE 已定义 条件为假 // 预处理器会跳过文件的整个内容 编译器会忽略它
-
#pragma
#pragma once // 文件开头写入 // 即 相同的文件(物理上)只能被包含一次
**对比:**条件编译语句由C标准提供支持,依赖于宏名字不能冲突,不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被同时包含;
#pragma once
由编译器提供支持,不会出现宏名碰撞引发的奇怪问题,如果某个头文件有多份拷贝,该方法不能保证他们不被重复包含。 -
-
有条件引用
从多个不同的头文件中选择一个引用到程序中。比如需要指定在不同的操作系统上使用的配置参数,可以通过一系列条件实现,即有条件引用。
#if SYSTEM_1 # include "system_1.h" #elif SYSTEM_2 # include "system_2.h" #elif SYSTEM_3 ... #endif
-
补充
-
使用宏定义头文件的名称。
#define test "test.h" #include "test.h" // 注:只能使用宏定义用户头文件 而不能使用定义系统头文件
-
头文件中一般写:(好的编程习惯)
- 头文件的包含
- 类型的定义
- 函数的声明
变量声明和定义一般写在
.c
文件中。如果多次包含一个有变量定义的头文件,将产生异常。
-
7.3.4 命令行定义
许多C编译器提供在命令行中定义符号,用于启动编译过程。
- 代码
// 例
#include <stdio.h>
int main()
{
int arr[ARR_SIZE];
int i = 0;
for(i = 0; i < ARR_SIZE; i++)
{
arr[i] = i;
}
for(i = 0; i < ARR_SIZE; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
// 代码中并没有ARR_SIZE的值,而是在程序编译时指定
- 编译
gcc -D ARR_SIZE=10 test.c # 指定ARR_SIZE为10 进行编译 gcc -D ARR_SIZE=100 test.c # 指定ARR_SIZE为100 进行编译
- 《C语言深度解剖》
7.3.5 习题1
写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。
// 函数实现如下:
#include <stdio.h>
int swap(int x)
{
int ret = ((x & 0x55555555) << 1) + ((x & 0xaaaaaaaa) >> 1);
return ret;
}
int main()
{
printf("%d\n", swap(10));
return 0;
}
// 解析:
// 获取奇数位 按位与 01010101010101010101010101010101 = 0x55555555(十六进制)
// 获取偶数位 按位与 10101010101010101010101010101010 = 0xaaaaaaaa(十六进制)
// 将奇数位右移 偶数位左移 并相加 得数的二进制位的奇数位和偶数位交换的结果
// 宏实现如下:
#define SWAP(X) ((X & 0x55555555) << 1) + ((X & 0xaaaaaaaa) >> 1)
#include <stdio.h>
int main()
{
printf("%d\n", SWAP(10));
return 0;
}
7.3.6 习题2
offsetof
宏的实现。结构成员相对于结构开头的字节偏移量。
-
offsetof
描述// 头文件 <stddef.h> 中 offsetof (type,member) // 参数: // type -- 类型 // member -- 成员 // 返回值: // size_t 类型 成员偏移量
-
offsetof
使用#include <stddef.h> #include <stdio.h> struct S { char c; short s; int i; float f; double d; }; int main() { printf("%d\n", offsetof(struct S, c)); // 0 printf("%d\n", offsetof(struct S, s)); // 2 printf("%d\n", offsetof(struct S, i)); // 4 printf("%d\n", offsetof(struct S, f)); // 8 printf("%d\n", offsetof(struct S, d)); // 16 return 0; }
-
模拟实现
offsetof
#include <stdio.h> #define OFFSETOF(type, member) (int)(&(((type *)0)->member)) struct S { char c; short s; int i; float f; double d; }; int main() { printf("%d\n", OFFSETOF(struct S, c)); // 0 printf("%d\n", OFFSETOF(struct S, s)); // 2 printf("%d\n", OFFSETOF(struct S, i)); // 4 printf("%d\n", OFFSETOF(struct S, f)); // 8 printf("%d\n", OFFSETOF(struct S, d)); // 16 return 0; }
-
补充
C语言库中提供的
offsetof
宏,采用全小写,以伪装成函数。