文章目录
- 一.程序环境
- 1.翻译环境
- 编译器
- 1.预处理
- 2.编译
- 3.汇编
- 链接器
- 2.运行环境
- 总图解
- 二.预处理
- 1.预定义符号
- 2.define
- 1.define的定义
- 2.替换规则
- 3.定义的建议和使用的缺点
- 1.加括号
- 2.避免使用带有副作用的符号
- 3.命名约定
- 4.#和##
- 1.#
- 2.##
- 5.宏和函数的对比
- 6.undef
- 3.条件编译
- 1.常量表达式
- 2.多分支
- 3.判断是否被定义
- 4.嵌套指令
- 5.文件的包含——inlcude
一.程序环境
引入:我所使用的VS2019——IDE(集成开发环境)
集成开发环境:一般包括代码编辑器、编译器、调试器和图形用户界面等工具。
1.编辑器:程序编辑器是指用来进行编辑程序的软体程序。俗话就是写代码的工具
2.编译器:是一个将代码转换成二进制指令的可执行程序
3.链接器:将二进制指令进行链接生成可执行程序
4.调试器:查找代码错误的工具
思考:
1.我们写的代码能直接在计算机里面运行吗?
答案:NO,计算机只能读懂二进制指令,我们写的代码(如果正确)是能转换成二进制指令的。
如果不行需要经过怎样的步骤?(ctrl +F5)
答案:
第一步:翻译——编译,链接生成一个以后缀为.exe的可执行程序
第二步:执行——在内存中开辟空间运行生成的代码,如何开辟空间运行这里就不过多描述如果有兴趣可以看一下这篇文章——函数栈帧
1.翻译环境
说明:是将源代码转换成可执行程序的环境。
假设:我们要编译一个test.c的源文件
需要两个帮我们把代码转换成二进制指令的可执行程序(后缀.exe)
1.编译器(cl.exe)
2.链接器(link.exe)
编译器
1.预处理
生成test.i文件,并将预处理的结果放在此文件中
1.将头文件的内容包含,同时将包含的指令删除
比如:#include<stdio.h>,这里就是将stdio.h里面的内容拷贝放置在文件test.c中
2.宏指令的替换和define定义的内容的替换
比如:#define MAX 100这条指令,会被删除同时如果有int max = MAX;那么MAX会被替换成100。
注意:包含的头文件的内容也要进行此过程,这就是预处理后代码行数变少的原因
- 注释的删除
注释的作用:让代码更易读懂,同时不会增加程序在运行时的负担。
2.编译
说明:将代码转换成汇编代码(通常是将高级语言转换成低级语言)
生成test.s文件,并将编译的代码放在此文件中。
如何转换呢?
涉及:
1.语义分析
2.词法分析
3.语法分析
4.符号汇总
这几步将代码的语法,符号等,拆分 转换成对应的汇编代码。
3.汇编
说明:将汇编代码转换成二进制指令
生成test.obj文件(Windows下)/test.o(Linux下),并将二进制指令放在test.o中。
这个文件的格式是elf格式的,也就是段表。
段表:每个.o文件都有相同的段表,但是每个段里面的信息可能不相同,但是同一类的信息。
1.汇编还会生成符号表。
链接器
说明:
1.将段表进行合并
2.将符号表进行汇总合并
3.生成可执行程序test.exe
符号表的合并——说白了就是将有用的信息进行筛选,无用的信息进行删除。
2.运行环境
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2.程序的执行便开始。接着便调用main函数。
3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止。
说明:
1.堆栈:是栈区
2.堆:是堆区
3.程序运行还有一种说法:函数栈帧
总图解
二.预处理
1.预定义符号
FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
//printf("%d", __STDC__);vs不遵循ANSI C
return 0;
}
2.define
1.define的定义
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
1.参数列表的左括号必须与name紧邻。
2.如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
例如:
#define SQUARE( x ) ((x) * (x))
int main()
{
int a = SQUARE(3);
printf("%d", a);
return 0;
}
图解:
2.替换规则
#define SQUARE( x ) ((x) * (x))
int main()
{
int a = SQUARE(3);
//在预处理阶段就变成 int a = ((x) * (x));
printf("%d", a);
return 0;
}
3.定义的建议和使用的缺点
1.加括号
#define MAX(a, b) ((a)>(b)?(a):(b))//如果不加最外面的括号结果是什么?
int main()
{
int a = 5;
int b = 6;
int max = 2*MAX(5, 6);
printf("%d",max);
return 0;
}
加上括号确保了define定义的是一个独立且不被外部符号干扰的整体
2.避免使用带有副作用的符号
例如:++,- -等
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
解析:
3.命名约定
宏命名一般使用全大写,函数名部分或者不大写
4.#和##
1.#
功能:将宏里面的参数进行替换转换成字符串
实现一个宏:PRINT(10,i)
效果为:The value of i is 10.字符串里面的i可以换成其它字符
#define PRINTF(value,name) printf("The value of "#name" is %d",value)
int main()
{
int i = 10;
PRINTF(10, i);
return 0;
}
2.##
功能:将符号进行粘连,形成一个新的符号(必须是合法的)
#define STICK(a,b) a##b
int main()
{
int sb = 10;
printf("%d", STICK(s, b));
return 0;
}
结果:10
5.宏和函数的对比
当代码量足够大时,使用函数比较方便,代码较为简便时使用宏比较方便
6.undef
取消define命名的符号
#define STICK(a,b) a##b
int main()
{
int sb = 10;
printf("%d", STICK(s, b));
#undef STICK//取消对STICK的命名
return 0;
}
3.条件编译
1.常量表达式
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
//如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多分支
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
可以将 if else语句与其和起来记忆。