目录
1. 从源文件到可执行文件的过程

1. 预处理:test.c→test.i
预处理器将源文件(.c文件)进行预处理,生成预处理后的文件(.i文件)。
- 宏替换
- 头文件展开
- 条件编译
- 处理其他预处理指令#pragma等
- 去注释
2. 编译:test.i→test.s
编译器将预处理后的文件(.i文件)进行词法分析、语法分析、语义分析以及优化,生成汇编文件(.s文件)。
3. 汇编:Linux:test.s→test.o / Windows:test.s→test.obj
汇编器将汇编文件(.s文件)根据汇编指令与机器指令的对照表进行翻译,生成目标文件(.o/.obj文件),即机器可以识别的二进制文件。
4. 链接:Linux:test.o→test.out / Windows:test.obj→test.exe
链接器将多个目标文件(.o/.obj文件)和库文件进行链接,生成可执行文件(.out/.exe文件)。
库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
库文件有两种,静态库和动态库(共享库),区别是:静态库在程序的链接阶段被复制到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
静态库、动态库区别来自链接阶段如何处理,链接成可执行程序。分别称为静态链接方式和动态链接方式。
库的好处:1. 代码保密;2. 方便部署和分发。
静态库:
命名规则:
- Linux:libxxx.a
- Windows:libxxx.lib
Linux下静态库的制作:
- gcc获得.o文件
gcc -c a.c b.c
- 将.o文件打包,使用ar工具(archive)
ar rcs libxxx.a a.o b.o优点:
- 静态库被打包到应用程序中加载速度快
- 发布程序无需提供静态库,移植方便
缺点:
- 消耗系统资源,浪费内存
- 更新、部署、发布麻烦
动态库:
命名规则:
- Linux:libxxx.so
- Windows:libxxx.dll
Linux下动态库的制作:
- gcc获得.o文件,得到和位置无关的代码
gcc -c -fpic/-fPIC a.c b.c
- gcc得到动态库
gcc -shared a.o b.o -o libxxx.so优点:
- 可以实现进程间资源共享(共享库)
- 更新、部署、发布简单
- 可以控制何时加载动态库
缺点:
- 加载速度比静态库慢
- 发布程序时需要提供依赖的动态库
2. 预处理指令
- 宏定义:#define指令定义一个宏,#undef指令删除一个宏定义
- 文件包含:#include指令导致一个指定文件的内容被包含到程序中
- 条件编译:#if、#ifdef、#ifndef、#elif、#else和#endif指令可以根据预处理器可以测试的条件来确定是将一段文本块包含到程序中还是将其排除在程序之外
- 其他预处理指令:#error、#line、#pragma
适用于所有指令的规则:
1. 指令都以#开始
#符号不需要在一行的行首,只要它之前只有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。
#define N 100 // ok
2. 在指令的符号之间可以插入任意数量的空格或水平制表符
# define N 100 // ok
3. 指令总是在第一个换行符处结束,除非明确地指明要延续
如果想在下一行延续指令,我们必须在当前行的末尾使用'\'字符。例如,下面的指令定义了一个宏来表示硬盘的容量,按字节计算:
#define DISK_CAPACITY (SIDES * \
TRACKS_PER_SIDE * \
SECTORS_PER_TRACK * \
BYTES_PER_SECTOR)
4. 指令可以出现在程序中的任何地方
但我们通常将#define和#include指令放在文件的开始,其他指令则放在后面,甚至可以放在函数定义的中间。
5. 注释可以与指令放在同一行
实际上,在宏定义的后面加一个注释来解释宏的含义是一种比较好的习惯。
#define FREEZING_PT 32.0f /* freezing point of water */
3. 宏定义
3.1 简单的宏(对象式宏)
#define 标识符 替换列表
宏的替换列表可以包括标识符、关键字、数值常量、字符常量、字符串字面量、操作符和排列。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在哪里出现,预处理器都会用替换列表代替它。
#define MAX 100
#define STR "abcdef"
#include <stdio.h>
int main()
{
printf("%d\n", MAX); // 100
int a = MAX;
printf("%d\n", a); // 100
printf("%s\n", STR); // abcdef
return 0;
}
下面代码中,a、b分别是什么类型?
#define INT_PTR int*
INT_PTR a, b; // a是int*类型,b是int类型
#define是宏定义,仅仅是直接替换,宏替换后代码为:
int* a, b;
不是a和b都是int*类型的意思,而是a是int*类型,b是int类型。可以看成是如下代码:
int *a, b;
3.2 带参数的宏(函数式宏)
#define 标识符(x1, x2, …, xn) 替换列表
其中x1, x2, …, xn是宏的参数,参数列表可以为空。宏名和左括号之间不能有空格,如果有空格,预处理器会认为是在定义一个简单的宏,其中(x1, x2, …, xn)是替换列表的一部分。
带参数的宏经常用来作为简单的函数使用。
#define ADD(x, y) ((x) + (y))
// ADD:宏名
// x y:宏的参数,参数是无类型的
// ((x) + (y)):宏体
#define ADD2(x, y) (x) + (y) // err
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d\n", ADD(a, b)); // ((a) + (b)) = 10 + 20 = 30
printf("%d\n", 10 * ADD2(a, b)); // 10 * (a) + (b) = 10 * 10 + 20 = 120
return 0;
}
如#define ADD(x, y) ((x) + (y)),用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。
#define PRINT_INT(n) printf("%d\n", n)
PRINT_INT(i / j);
进行宏替换后代码为:
printf("%d\n", i / j);
3.3 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
int x = 1;
int y = x + 1; // y=2 x=1 不带副作用
int y = ++x; // y=2 x=2 带有副作用(x自身也改变了)
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#include <stdio.h>
int main()
{
int a = 3;
int b = 4;
int m = MAX(++a, ++b);
printf("m=%d a=%d b=%d\n", m, a, b); // m=6 a=4 b=6
return 0;
}
如何计算((++a)>(++b)?(++a):(++b))
- ++a:a先自增,a=4,然后++a本身=4
- ++b:b先自增,b=5,然后++b本身=5
- 4>5为假,不执行++a,直接执行++b
- ++b:b先自增,b=6,然后++b本身=6
- ((++a)>(++b)?(++a):(++b))的结果为6
3.4 #和##
宏定义可以包含两个专用的运算符:#和##。编译器不会识别这两种运算符,它们会在预处理时被执行。
#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符有许多用途,这里只来讨论其中的一种。假设我们决定在调试过程中使用PRINT_INT宏作为一个便捷的方法来输出整型变量或表达式的值。#运算符可以使PRINT_INT为每个输出的值添加标签。下面是改进后的PRINT_INT:
#define PRINT_INT(n) printf(#n " = %d\n", n)
n之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字符串字面量。因此,调用
PRINT_INT(i / j);
会变为
printf("i / j" " = %d\n", i / j);
C语言中相邻的字符串字面量会被合并。因此上边的语句等价于:
printf("i / j = %d\n", i / j);
当程序执行时,printf函数会同时显示表达式i / j和它的值。
#include <stdio.h>
#define PRINT_INT(n) printf(#n " = %d\n", n)
int main()
{
int i = 11;
int j = 2;
PRINT_INT(i / j);
return 0;
}
// 输出结果:
// i / j = 5
##运算符可以将两个记号(如标识符)“粘合”在一起,成为一个记号。如果其中一个操作数是宏参数,“粘合”会在形式参数被相应的实际参数替换后发生。考虑下面的宏:
#define MK_ID(n) i##n
当MK_ID被调用时(比如MK_ID(1)),预处理器首先使用实际参数(这个例子中是1)替换形式参数n。接着,预处理器将i和1合并成为一个记号(i1)。下面的声明使用MK_ID创建了3个标识符:
int MK_ID(1), MK_ID(2), MK_ID(3);
预处理后这一声明变为:
int i1, i2, i3;
3.5 宏的通用属性
1. 宏的替换列表可以包含对其他宏的调用
例如,我们可以用宏PI来定义宏TWO_PI:
#define PI 3.14159
#define TWO_PI (2 * PI)
当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2 * PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止。
2. 预处理器只会替换完整的记号,而不会替换记号的片断
因此,预处理器会忽略嵌在标识符、字符常量、字符串字面量之中的宏名。例如,假设程序含有如下代码行:
#define SIZE 256
int BUFFER__SIZE;
if (BUFFER_SIZE > SIZE)
puts("Error : SIZE exceeded");
预处理后这些代码行会变为
int BUFFER__SIZE;
if (BUFFER_SIZE > 256)
puts("Error : SIZE exceeded");
尽管标识符BUFFER_SIZE和字符串"Error : SIZE exceeded"都包含SIZE,但是它们没有被预处理影响。
3. 宏定义的作用范围通常到出现这个宏的文件末尾
由于宏是由预处理器处理的,它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。
4. 宏不可以被定义两遍,除非新的定义与旧的定义是一样的
小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。
5. 宏可以使用#undef指令“取消定义”
#undef指令有如下形式:
#undef 标识符
其中标识符是一个宏名。例如,指令
#undef N
会删除宏N当前的定义,如果N没有被定义成一个宏,#undef指令没有任何作用。#undef指令的一个用途是取消宏的现有定义,以便于重新给出新的定义。
3.6 宏定义中的圆括号
在前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗?答案是绝对需要。如果我们少用几个圆括号,宏有时可能会得到意想不到的(而且是不希望有的)结果。
对于在一个宏定义中哪里要加圆括号有两条规则要遵守。首先,如果宏的替换列表中有运算符,那么始终要将替换列表放在括号中:
#define TWO_PI (2 * 3.14159)
其次,如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号中:
#define SCALE(x) ((x) * 10)
没有括号的话,我们将无法确保编译器会将替换列表和参数作为完整的表达式。编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。
为了展示为替换列表添加圆括号的重要性,考虑下面的宏定义,其中的替换列表没有添加圆括号:
#define TWO_PI 2 * 3.14159
/* 需要给替换列表加圆括号 */
在预处理时,语句
conversion_factor = 360 / TWO_PI;
变为
conversion_factor = 360 / 2 * 3.14159;
除法会在乘法之前执行,产生的结果并不是期望的结果。
当宏有参数时,仅给替换列表添加圆括号是不够的。参数的每一次出现都要添加圆括号。例如,假设SCALE定义如下:
#define SCALE(x) (x * 10)
/* 需要给x添加圆括号 */
在预处理过程中,语句
j = SCALE(i + 1) ;
变为
j = (i + 1 * 10);
由于乘法的优先级比加法高,这条语句等价于
j = i + 10;
当然,我们希望的是
j = (i + 1) * 10;
3.7 创建较长的宏
在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字符串显示出来:
#define ECHO(s) (gets(s), puts(s))
gets函数和puts函数的调用都是表达式,因此使用逗号运算符连接它们是合法的。我们甚至可以把ECHO宏当作一个函数来使用:
ECHO(str); /* 替换为 (gets(str), puts(str)); */
如果不想在ECHO的定义中使用逗号运算符,我们还可以将gets函数和puts函数的调用放在花括号中形成复合语句:
#define ECHO(s) { gets(s); puts(s); }
遗憾的是,这种方式并未奏效。假如我们将ECHO宏用于下面的if语句:
if (echo_flag)
ECHO(str);
else
gets(str);
将ECHO宏替换会得到下面的结果:
if (echo_flag)
{ gets(str); puts(str); };
else
gets(str);
编译器会将头两行作为完整的if语句:
if (echo_flag)
{ gets(str); puts(str); };
编译器会将跟在后面的分号作为空语句,并且对else子句产生出错消息,因为它不属于任何if语句。记住永远不要在ECHO宏后面加分号我们就可以解决这个问题。但是这样做会使程序看起来有些怪异。
逗号运算符可以解决ECHO宏的问题,但并不能解决所有宏的问题。假如一个宏需要包含一系列的语句,而不仅仅是一系列的表达式,这时逗号运算符就起不了作用了,因为它只能连接表达式,不能连接语句。解决的方法是将语句放在do循环中,并将条件设置为假(因此语句只会执行一次):
do { ... } while (0)
注意,这个do语句是不完整的——后面还缺一个分号。为了看到这个技巧的实际作用,让我们将它用于ECHO宏中:
#define ECHO(s) do \
{ \
gets(s); \
puts(s); \
} while (0)
当使用ECHO宏时,一定要加分号以使do语句完整:
ECHO(str);
/*
do
{
gets(str);
puts(str);
} while (0);
*/
3.8 预定义宏
C语言有一些预定义宏,每个宏表示一个整数常量或字符串字面量,这些宏提供了当前编译或编译器本身的信息。
| 名字 | 描述 |
|---|---|
| __LINE__ | 被编译的文件中的行号 |
| __FILE__ | 被编译的文件名 |
| __DATE__ | 编译的日期(格式"mm dd yyyy") |
| __TIME__ | 编译的时间(格式"hh:mm:ss") |
| __STDC__ | 如果编译器符合C标准(C89或C99),那么值为1 |
#include <stdio.h>
int main()
{
printf("编译的日期:%s, 时间:%s\n", __DATE__, __TIME__);
return 0;
}
![]()
3.9 宏和函数的区别
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(x, y) ((x) > (y) ? (x) : (y))
为什么不用函数来完成这个任务?
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整型、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
宏的缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏不能调试。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程序容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
// 使用
MALLOC(10, int); // 类型作为参数
// 预处理器替换之后:
(int*)malloc(10 * sizeof(int));
宏和函数的对比:
| 属性 | 宏 | 函数 |
|---|---|---|
| 代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码。 |
| 执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些。 |
| 操作符 优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
| 带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
| 参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
| 调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
| 递归 | 宏是不能递归的 | 函数是可以递归的 |
一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。习惯是把宏名全部大写,函数名不要全部大写。
3.10 宏和typedef的区别
- 宏替换是字符替换;typedef用于类型重命名。
- 宏替换发生在预处理阶段;typedef是编译的一部分。
- 宏不检查数据类型;typedef检查数据类型。
- 宏不是语句,不在最后加分号;typedef是语句,要加分号标识结束。
4. 文件包含
#include指令告诉预处理器打开指定的文件,并且把此文件的内容插入到当前文件中。因此,如果想让几个源文件可以访问相同的信息,可以把此信息放入一个文件中,然后利用#include指令把该文件的内容带进每个源文件中。把按照此种方式包含的文件称为头文件(有时称为包含文件),头文件的扩展名为.h。
#include指令主要有两种书写格式。
第一种格式用于属于C语言自身库的头文件:
#include <文件名>
第二种格式用于所有其他头文件,也包含任何自己编写的文件:
#include "文件名"
这两种格式间的细微差异在于编译器定位头文件的方式。大多数编译器遵循的规则:
- #include <文件名>:搜寻系统头文件所在的目录(或多个目录)
- #include "文件名":先搜寻当前目录,然后搜寻系统头文件所在的目录(或多个目录)
5. 条件编译
C语言的预处理器可以识别大量用于支持条件编译的指令。条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片断。
5.1 #if指令和#endif指令
#if 常量表达式
// 代码段
#endif
如果常量表达式为真,保留代码段。如果常量表达式为假,删除代码段。
#define DEBUG 1
#if DEBUG
// 代码段1
#endif
#if !DEBUG
// 代码段2
#endif
// 保留代码段1,删除代码段2
// 省略DEBUG的定义,#if指令会把没有定义过的标识符当作是值为0的宏对待
#if DEBUG
// 代码段1
#endif
#if !DEBUG
// 代码段2
#endif
// 删除代码段1,保留代码段2
5.2 defined运算符
除了#和##,还有一个专用于预处理器的运算符——defined。当defined应用于标识符时,如果标识符是一个定义过的宏则返回1,否则返回0。defined运算符通常与#if指令结合使用:
#if defined(标识符)
// 代码段
#endif
如果标识符被定义为宏,保留代码段。如果标识符没有被定义为宏,删除代码段。
#define DEBUG // 由于defined运算符仅检测DEBUG是否有定义,所以不需要给DEBUG赋值
#if defined(DEBUG)
// 代码段1
#endif
#if !defined(DEBUG)
// 代码段2
#endif
// 保留代码段1,删除代码段2
5.3 #ifdef指令和#ifndef指令
#ifdef 标识符
// 代码段
#endif
// 等价于
#if defined(标识符)
// 代码段
#endif
#ifndef 标识符
// 代码段
#endif
// 等价于
#if !defined(标识符)
// 代码段
#endif
5.4 #elif指令和#else指令
#if 常量表达式1
// 代码段1
#elif 常量表达式2
// 代码段2
...
#elif 常量表达式n
// 代码段n
#else
// 代码段n+1
#endif
如果常量表达式1为真,保留代码段1,删除其他代码段。
如果常量表达式1为假且常量表达式2为真,保留代码段2,删除其他代码段。
如果常量表达式1为假且常量表达式2为假且常量表达式3为真,保留代码段3,删除其他代码段。
……
如果常量表达式1~n都为假,保留代码段n+1,删除其他代码段。
此外,#elif指令和#else指令也可以与#ifdef指令、#ifndef指令结合使用。
文章详细阐述了C语言从源文件到可执行文件的编译过程,包括预处理、编译、汇编和链接四个阶段。重点介绍了预处理指令,如宏定义、文件包含和条件编译,并深入探讨了宏定义的不同类型和用法,包括对象式宏、函数式宏、副作用、#和##运算符。同时,文章讲解了静态库和动态库的制作、区别以及在程序中的应用。
933

被折叠的 条评论
为什么被折叠?



