目录
1、程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境
(1)、翻译环境,在这个环境中源代码被转换为可执行的机器指令(机器指令就是二进制指令)
(2)、执行环境,它用于实际执行代码
2、详解编译+链接
2.1、翻译环境
如图:
一个工程里面可以包含多个源文件(即.c文件) ,分模块化。由图我们可知道,每个源文件都是单独经过编译器处理生成目标文件(方便理解画了3个编译器,实际上只有一个编译器)。
实例:我们创建了3个.c文件:
将代码运行一下,结束后去相应文件下,进入Debug路径下会看到对应的三个.obj文件,说明每个源文件确实都是单独经过编译器处理生成目标文件。
在这里注意,对于.h文件,在下面我们会了解到,.h文件是会拷贝到源文件里面去的。
然后呢,所有的目标文件再加上链接库通过链接器的处理生成可执行程序,也就是.exe文件。
而链接库是什么呢?就是除了我们自己所编写的代码之外,由编译器提供的其他东西(如:我们编写代码时,所包含的头文件所依赖的库,都是需要链接库来提供)
2.2 编译本身也分为几个阶段
如图:
说明:
符号表:
在编译时,会进行符号汇总,后续在汇编时,就会将汇总出来的符号,放入到符号表中。
继续使用最上面的例子:
我们会发现,add.o中add的地址和test.o中add的地址不同,因为我们在上述有提到过,每个源文件都是独立的经过编译器处理的,也就是说,当test.c在进行处理时,它是不知道还有add.c及sub.c的存在的,所以生成的符号表中,他们俩的地址时不同的,所以在链接时,需要重新定位和整合有效的符号地址。比如:你在test.c中使用了add函数,但并没有定义它如何使用,那它在编译时一样会生成符号表,但是在链接时对符号重定位时,无法找到有效地址,就会出现链接错误,我们将上述的add函数注释掉,看图:
使用VC可能更容易理解,当你按下第一个按钮时,不会报错,按下第二个按钮时,就会出现错误,也就是链接性错误。
2、3运行环境
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 找到程序的入口,程序的执行便开始,(调用main函数)。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)【函数栈帧】,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
3、预处理详解
3.1、预定义符号
看代码运行:
对于上述的代码,如果我们想要知道它是在哪个文件路径下打印的,我们就可以使用__FILE__,如果想知道是在我们代码中的哪一行打印的,就可以使用__LINE__,等等,看运行截图:
3.2、#define
3.2.1、#define定义标识符
语法:
#define name stuff
举例1:
#define PI 3
int main()
{
printf("圆的周长为:%d",2 * PI * 4);
return 0;
}
上述的与处理中提到,会#define定义的符号进行替换,所以经过预处理后,代码变成了:
例2:
例2我们可以详细看一下,上面我们说,预处理会生成(.i)文件,通过VS,我们可以观察一下,
设置一下项目属性:
更改完成之后,运行代码,代码会报错:
不影响,运行后,我们去文件路径下Debug下,会看到(.i)文件,找到后,打开它。
打开后,我们会发现会很多很多我们看不懂的代码,没关系,直接拉到最后,会看到经过预处理后,我们的代码。
提问:在define 定义标识符的时候,要不要在最后加上 ( ; ) ?
建议不要加上分号( ; ) ,这样容易导致问题。
如:
3.2.2#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
宏的申明方式:
#define name(parament-list) stuff
其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
警告:
这个宏存在一个问题:
观察下列的代码:
#define SQUARE(x) x*x
int main()
{
int a=5;
printf("%d\n",SQUARE(a));
printf("%d\n",SQUARE(a+1));
return 0;
}
思考,打印结果。
正确结果:
25
11
输出25,原因:
置于程序中,预处理就会用下面这个表达式替换上面的表达式:
5*5
输出11:
原因如图:
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
所以定义宏时,加上该加的括号
这样,我们再做一个测试:
#define SQUARE(x) (x)+(x)
int main()
{
printf("%d\n", 10 * SQUARE(5));
}
思考输出
正确输出:55
原因:
所以在定义宏时,不要吝啬括号:
#define SQUARE(x) ((x)+(x))
小结:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
3.2.3 #define 替换规则
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先 被替换。2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上 述处理过程。
注意:1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
3.2.4 宏和函数对比
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹 。2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。宏与类型无关的 。
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。2. 宏是没法调试的。
3.3、 条件编译
1.#if 常量表达式//...#endif// 常量表达式由预处理器求值。如:#define __DEBUG__ 1#if __DEBUG__//..#endif2. 多个分支的条件编译#if 常量表达式//...#elif 常量表达式//...#else//...#endif3. 判断是否被定义#if defined(symbol)#ifdef symbol#if !defined(symbol)#ifndef symbol4. 嵌套指令#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1 ();#endif#ifdef OPTION2unix_version_option2 ();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2 ();#endif#endif
3.4、文件包含
C语言结束啦!Java学习,正式开始!!!对于库文件也可以使用 " " 的形式包含但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。