本节课讲解的主要是预处理器,编译和连接的异同。
#define有两个功能:一个是定义常量,一个是作为宏。
//定义常量
#define w 40
#define h 80
#define pere 2*(w+h)
//宏
#define MAX(a,b) (a>b?a:b)
MAX(10,40)//预处理替换为(10>40?10:40)
//int max = (fib(100) > fract(4000)) ? fib(100) : fract(4000);展开替换后的结果,编译器不会保留中间结果,函数被调用了两次,尤其是大规模的函数会导致性能下降
int max = MAX(fib(100, fract(4000)));
//int larger = ((m++) > (n++)) ? (m++) : (n++);//一共自增了3次
int larger = MAX(m++, n++);//最终会对大的两次自增,小的一次自增
注意:
1.这种宏定义市运行速度比函数快,而且不需要管参数的类型,例如:
MAX(40.2,"Hello");//预处理阶段没问题,但是编译阶段会报错。
2.在预处理阶段进行的都只是文本替换,结果作为数据传到下一阶段,预处理阶段并不进行类型检查,文本替换产生的问题会在之后的编译阶段进行识别。
3.为了避免define在预处理时不进行类型检查的缺点,应用static const定义全局变量。
//最好将重复代码写成函数或者是小段的宏,便于替换。
#define NthElemAddr(base, elemSize, index) ((char*)base + index * elemSize)
void* VectorNth(vector *v, int position)
{
assert(position > 0)
assert(position < v->logLength)
return NthElemAddr(Vector->Elem, Vector->Size, positon);
}
上面的define返回的是char*类型,而VectorNth函数是void*类型会出错吗?
没有问题。因为void*可以接受任何类型指针,就是所谓的上转型(upcasting),将一个更具体的指针转换成一个类型更泛化的指针。编译器知道这种类型转换并不会带来风险。如果进行下转型(downcasting),就告诉编译器,我现在有一个类型更加泛化的指针,我知道此指针具体类型是什么,但是如果涉及引用就想要进行强制转换。
#assert宏
#ifdef NDEBUG //是关于某个define是否存在的判断,如果定义了NDEDUG,那么程序中所有的assert都会替换成空操作语句
#define assert(cond) (void)0 //将数字0强制转换为void类型,不要把这个0用在任何地方,也不许被赋值,作为一个空操作(nop),在?和:中间占位。虽然看上去是一条语句,但是它不会被编译成任何一条汇编指令
#else
#define assert(cond) \
(cond) ? ((void)0) : fprintf(stderr,"..文件名..行号等.."),exit(0);
#endif
#include
#include <stdio.h>
#include "vector.h"
1.使用尖括号:认为是系统文件,应该是编译器提供的,预处理器可以通过默认路径找到这些文件。/usr/bin/include和/usr/include中查找。
2.使用双引号:编译器会假设这是用户编写的.h文件,会默认从当前工作目录查找该头文件。通过makefile可以设定一些选项告诉编译器从哪里寻找这些包含的头文件。
3.#include指令也是查找并替换。用文件的内容替换掉该行#include指令。对于#include的处理是递归的,如果#include的指定文件本身还包含#include行,那么预处理器会一直深入下去直到最底层,层层替换直到生成不包含#include和#define的文本流。所以预处理后的文本流中,所有的#include和#define都被移除了。
gcc xxx -c vector.c
-c表示编译源文件,但是不要生成可执行文件。编译阶段之后就停止,只生成.o文件,不进行链接。
gcc xxx -E vector.c
-E表示只进行预处理,然后将结果输出,但是不进行后续阶段的操作。在生成的文件中前半部分都是导入的其他代码,在快结尾部分是自己的代码。
避免循环包含头文件,预处理器也不会让某个头文件被包含两次:
#ifndef _vector_h_
#define _vector_h_
//列出所有vector.h中的原型
#endif
注意:
所有的.h文件都是定义某些原型的,不产生代码。好比定义某个结构类型,但是不会产生该结构体相应的代码。而且在.h文件中也不能申请任何存储空间,除非定义共享全局变量(很少使用)。但是.c和.cpp文件不同,它们定义了全局变量,全局函数和类方法等,都要被翻译成机器码(一系列01串),机器码可看作是汇编指令。包含.c和.cpp可认为是重复定义函数。声明一个函数和定义一个函数是不同的,对于函数实现而言,编译阶段会生成相应的代码,对于函数声明却不会产生任何代码。
1.vector.c文件包含了a.h,b.h,c.h文件。经预处理后去掉了#define和#include头。再经过编译得到.o文件(内容是汇编代码)。然后再经过链接得到可执行文件。(链接阶段将所有相关的.o文件组织到一起,连接器尝试使用这些文件创建可执行文件。这阶段需要有一个main函数,连接器才知道从哪里开始执行程序,对于每一个要被调用的函数都应该有定义,要求所有定义了的函数只被定义一次)。
2.如果在makefile或者gcc命令没有加额外选项的话,编译器会继续下面各个阶段并且生成可执行文件,默认情况下文件名为a.out。
3.链接会将.o文件和其他.o文件的各个部分进行混合重组(除了自己编写的模块,其他部分都是来自于编译器标准库或者是标准的.o代码)。
.o文件
#include<stdio.h>//printf
#include<stdlib.h>//malloc free
#include<assert.h>
int main(int argc, char* argv[])
{
void* memory=malloc(400);
assert(memory!=NULL);
printf("Year!\n");
free(memory);
return 0;
}
注意:
1.#include<stdio.h>负责malloc,free,realloc,将其注释掉,就没有malloc,free,realloc的函数原型了。函数在执行到第7行时,也会将malloc推测成一个函数,并且推测函数有一个int参数,且返回一个int值,编译器并不会查看赋值函数来推测返回值是什么类型。因此编译器会对这行给出两条警告:第7行根据推测的函数类型,会认为是对一个指针赋值,而赋值的类型却是一个普通的整型。第10行编译器同样不知道free是什么,并推测它的原型,free的参数是void*,并且返回值是int,产生的内容和没有注释该行的.o文件完全一样。只是会报出三个错误,其中两条是说明缺失原型的,而一条是左值和右值类型不兼容,但是还是生成.o文件。当链接的时候链接器会忘掉这些警告,它不会记录有没有包含某个头文件,也不会记录编译时存在的警告。但是生成的.o文件和代码的语义是完全一致的,所以当链接并运行程序是没问题的。
2.负责printf,将这一行注释掉之后,预处理器生成的翻译单元中将不会有printf函数的声明。有的编译器会在编译阶段报错(函数未声明),gcc则不会报错。gcc会在编译时刻分析源程序,看看哪部分像是函数调用,会根据函数调用推测函数原型。编译器看到了调用printf,printf只有一个字符串作为参数,发出未找到printf函数原型的警告,但是不会停下来,还是继续生成.o文件。gcc推测一个函数原型时,将返回类型推测为int。如果还有其他的printf函数,那么只能同样是只有一个字符串参数(推测出的原型,函数参数个数不可变)。推测出的函数原型会与实际的函数原型稍有区别,但是生成的.o文件实际上完全没变(因为.h头文件只是包含结构的定义以及一些原型,对头文件来说不会产生任何汇编代码,头文件的用处只是告诉编译器一些规则,让编译器判定程序的语法正确与错误)。ld命令用来链接,链接命令会根据编译过程中出现的警告查找标准库,printf对应的代码就在标准库中,因此在链接阶段会被加进来,虽然在链接阶段之前并没有见过printf的原型。因此include并不能保证相应的函数实现在连接时可用,如果某个函数定义在了标准库中,那么在链接时就可以被加进来,而无论我们是否声明了函数原型。
3.如果将前两条include都注释掉,那么会产生4条警告,但是依然会生成.o文件,并且会链接生成a.out文件并执行它。其实头文件做的全部事情就是告诉编译器有哪些函数原型。但是在.h文件中并没有说明这些函数的代码在哪里,链接阶段则负责去标准库中寻找这些代码,而malloc,free,printf正是在标准库中。只要被调用的函数在标准库中存在,那么无论编译时有没有警告,生成的.o文件都会没有区别(包含原先代码的语义),因为在链接的时候可以用到标准库的代码,并将调用到的函数的代码加到.o文件集合中,因此会在.o文件中出现相应标号的函数,生成可执行文件。
4.#include <assert.h>注释掉之后编译器遇到第8行,看到的只是assert这个符号,而不是宏替换后的代码,一次编译器猜测它是一个函数调用,会在.o文件中出现CALL<assert>,造成编译成功,但是链接失败了,原因是标准库中根本没有assert函数。