目录
一、程序环境
(一)程序的翻译环境
我们平常写的程序都是用计算机语言写的,比如C语言、C++、Java等高级语言。这些语言并不能被计算机直接执行,而是依赖程序的翻译环境将源代码转换为可执行的机器指令,然后再进行执行。
1.编译
编译可分为三个阶段:预编译、编译、汇编
1.1预编译
预编译是做些代码文本的替换工作。处理以# 开头的指令 , 比如拷贝 #include 包含的文件代码,#define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段。主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
作用:
(1)头文件的包含
(2)注释的删除
(3)#define的替换
1.2编译
编译(compilation , compile) 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译语言是一种以编译器来实现的编程语言。它不像直译语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。理论上,任何编程语言都可以是编译式,或直译式的。它们之间的区别,仅与程序的应用有关。
作用:
把C语言代码转换为汇编代码
(1)语法分析
(2)词法分析
(3)语义分析
(4)符号汇总
1.3.汇编
把汇编指令转换为二进制指令/机器码
作用:
把汇编指令转换为二进制指令
(1)形成符号表
2.链接
C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o。
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
作用:
(1)合并段表(elf文件的合并)
(2)符号表的合并和重定位
(二)程序的运行环境
程序执行的过程(双击打开):
1.程序必须载入内存当中,在有操作系统的环境中:一般这个由操作系统完成.在独立的环境中(如:单片机),程序的载入必须手工安排(下载器),也可能是通过可执行代码置入只读内存来完成.
2.程序执行开始后,紧接着调用主函数
3.开始执行代码,这个时候程序将使用一个运行的堆栈,存储函数的局部变量和返回地址.程序同时也可以使用静态(静态)内存,存储于静态内存的变量在程序的整个执行过程一只保留他们的值.
4.终止程序,主函数正常返回(返回0);也可能错误返回(返回1)。
(三)总结
二、预处理
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
预处理主要是处理以#开头的命令,例如#include <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.c一样,.i也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
(一)预处理指令
1.#define定义无参宏
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAX 100
#include <stdio.h>
int main()
{
int a = MAX;
printf("%d\n", a);//100
return 0;
}
2.#define定义有参宏
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAXVALUE(a,b) ((a)>(b)?(a):(b))
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d\n", MAXVALUE(a,b));//20
return 0;
}
3.#defien替换规则
步骤:
1. 在调用有参宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
4. #undef指令
这条指令用于移除一个宏定义。
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAX 100
#include <stdio.h>
void test()
{
int a = MAX;
printf("%d\n", a);//100
}
int main()
{
test();//100
#undef MAX
int a = MAX;//未声明
printf("%d\n", a);
return 0;
}//error C2065: “MAX”: 未声明的标识符
5.宏和函数对比
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
x+1;//不带副作用
x++;//带有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
…
x = 5;
y = 8;
z = MAX(x++, y++);
printf(“x=%d y=%d z=%d\n”, x, y, z);//输出的结果是什么?
z = ( (x++) > (y++) ? (x++) : (y++));
x=6 y=10 z=9
#define MAX(a, b) ((a)>(b)?(a):(b))
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
(二)条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。(满足条件参与编译,不满足条件不参与编译)
比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
1.#if
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAX
#include <stdio.h>
int main()
{
#if 1
printf("打印!\n");
//如果条件为真(C语言中0假非0真) 就执行#if与#endif之间的代码 否则不执行
#endif
return 0;
}
2.#elif
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAX
#include <stdio.h>
int main()
{
#if 1>1
printf("不打印!\n");
#elif 1==1
printf("打印!\n");//打印
#else
printf("哈哈哈哈\n");
#endif
return 0;
}
3.#ifdef
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#define MAX
#include <stdio.h>
int main()
{
//写法一
#ifdef MAX
printf("MAX已定义\n");//MAX已定义
#endif
#ifndef MIN
printf("MIN未定义\n");//MIN未定义
#endif
//写法二
#if defined(MAX)
printf("MAX已定义\n");//MAX已定义
#endif
#if !defined(MIN)
printf("MIN未定义\n");//MIN未定义
#endif
return 0;
}
(三)文件包含
1.本地文件包含
#include “filename.h”
查找策略:
使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。如果找不到就提示编译错误。
2.库文件包含
#include <filename.h>
使用尖括号< >,编译器会到系统路径下查找头文件,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同
3.头文件重复包含解决方案
头文件一般只包含函数或变量的声明,不要包含定义,否则会出现重定义的问题;一般将函数实现或变量的定义放在C或cpp中;另外在c或cpp中头文件的重复包含也会出现重定义的问题。
为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。
下图是间接导致头文件重复包含的原因:
3.1条件编译解决头文件重复包含
3.2预处理指令解决头文件重复包含
在头文件当中添加预处理指令#pragma once