预处理符号
常见的五种预定义符号
有:
①__LINE__:当前源程序的行号
②__FILE__:正在编译的程序的文件
③__DATE__:编译的日期字符串,形如"Mmm dd yyyy"
④__TIME__:编译的时间字符串,形如"hh:mm:ss"
⑤__STDC__:如果__STDC__的内容是十进制常数1,则表示编译程序的实现符合标准C他们的值或者都是字符串常量,或者是十进制数字常量。
__FILE__和__LINE__
在确认调试输出的来源方面很有用处。
_DATE__和__TIME__
常常用于在被编译的程序加入版本信息。
__STDC__
用于那些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译。
#与##的区别和特性
首先我们使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起。
上一个代码来演示一下:
#define dprint(expr)printf(#expr"=%d\n",expr);
int main()
{
int a=20,b=10;
dprint(a/b);
return0;
}
上面的例子会打印出:
a/b=2
#include<stdio.h>
#include<Windows.h>
#define M "hello"
#define N ",world"
#define MN "hello,bit"
#define LINK(M, N) M##N
int main()
{
printf("%s\n", LINK(M,N));
system("pause");
return 0;
}
上面的例子很明显,答案是hello,bit 而不是hello,world。
宏与函数
宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找较大(或较小)的一个;
#define MAX(a,b) ((a)>(b)?(a):(b))
为什么不用函数来完成这个任务呢?
有两个原因。首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比试用函数在程序的规模和速度上都更胜一筹。
但是,更为重要的是,函数的参数必须声明为一种特定的类型,所以只能在类型合适的表达式上使用。反之,上面这个宏可以用于整型、长整型、单浮点型、双浮点型以为其他任何可以用>操作符比较大小的类型。换句话说,宏是与类型无关的。
和使用函数相比,使用宏的不利之处就在于每次使用宏时,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则宏可能会大幅度增加代码的长度。
还有一些任务根本无法用函数实现。仔细观察下面这个宏。这个宏的第2个参数是一种类型,它无法作为函数参数进行传递。
#define MALLOC(n,type) ((type *)malloc((n)*sizeof(type)))
你现在仔细观察一下这个宏确切的工作过程,下面这个例子中的第一条语句被预处理器转换为第二条语句。
pi=MALLOC(25,int);
pi=((int *)malloc((25)*sizeof(int)));
同样,请注意宏定义并没有用一个分号结尾。分号出现在调用这个宏的语句里。
编译链接过程
预处理(宏替换,去注释,头文件展开,条件编译)->编译->汇编->链接(动态与静态)
C语言的编译链接过程是把我们编写的C程序(源代码)转换成可以在硬件上运行的可执行程序,需要进行编译到链接的过程。编译就是把文本形式的源代码翻译成机器语言格式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终的可执行程序的过程。
下列以一个简单的程序做以说明(linux环境下编译):
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
- 生成的可执行文件的命令如下:
gcc test.c -o test
第一步:预编译 .c文件->.i文件
gcc -E test.c -o test.i
当然,在预处理阶段都做了哪些事呢?
1>宏替换:将#define删除并展开所有的宏。
2>去注释:删除程序中的所有注释。
3>头文件展开:将程序中所用的头文件用其内容来替换头文件名。
4>条件编译:处理预编译指令#if #inder #else #endif #include等。
- 条件编译语法
#if constant-expression
statements
#elif
other statements
#else
other statements
#endlf
#ifdef(判断是否定义)
....
#ifndef
第二步:编译 .i文件->.s文件
gcc -S test.i -o test.s
把用高级程序语言书写的源程序,翻译成等价的计算机汇编语言或机器语言书写的目标程序或翻译程序。
第三步:汇编 .s->.o目标二进制文件
gcc -c test.s -o test.o
注意:此处的.o文件并不是可执行文件,目标文件未经链接库,所以不能执行,而且ELF的具体内容也有差别。
第四步:链接 .o->可执行的二进制文件
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。根据开发人员指定的同库函数的链接方式的不同,
- 链接处理可分为两种:
静态链接
在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
编译链接过程的主要指令有以下四种:
预处理器:将.c 文件转化成 .i文件,使用的gcc命令是:gcc –E,对应于预处理命令cpp;
编译器:将.c/.h文件转换成.s文件,使用的gcc命令是:gcc –S,对应于编译命令 cc –S;
汇编器:将.s 文件转化成 .o文件,使用的gcc 命令是:gcc –c,对应于汇编命令是 as;
链接器:将.o文件转化成可执行程序,使用的gcc 命令是: gcc,对应于链接命令是 ld;