执行环境:所在操作系统的平台 win10 win11 linux
翻译环境:MSVC gcc g++
你的vs 2019 和2022 是集成开发环境把编辑器编译器全部给你集成到一块了,也就是所有的功能给你自动生成的,查看中间过程就有些困难,然而 Linux下就可以通过操作选项来一个个看到这些步骤,或者是用vs code这种编辑器配好win下的gcc就可以一步步的看到过程了。
我们想要把一个.c文件变成一个.exe文件要经过一下过程
预处理主要干的活就是头文件展开,宏替换,去注释,条件编译。
在Linux的gcc下通过gcc -E hello.c -o hello.i 也就是开始翻译完成预处理就停下来再生成hello.i文件
预处理进行了宏替换 NUM被替换成了100,而且进行了头文件展开将你代码中调用的方法从你所在的头文件给你复制粘贴进来,然后你的注释也被去掉了,而且预处理之后还是语言
通过gcc -S hello.i -o hello.s开始翻译完成编译就停下来
这时候这是啥呢,他已经不是c语言了他是汇编语言,那我们的编译的过程也就是把刚刚那一份干净的纯C语言变成汇编语言。
虽然叫汇编可不是把c语言变成汇编语言,上一步已经做过了 这一步的命令行是 gcc -c hello.s -o hello.o
这里用vim看就是乱码了,那我们用这个格式化处理工具od 来查看一下
很明显发现这不是二进制吗,是的汇编的工作就是把汇编代码转换成二进制程序,但是这个是不可执行的这个以.o结尾文件叫做目标文件,那.o\文件运行不了说明还是少了一步
直接gcc hello.o链接形成a.out文件./a.out就可以看见程序执行的结果了,这个printf();函数是你写的吗?不是的他是在c语言的库中。链接的作用就是把程序跟库文件相关联的过程。
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这里就能清晰的看到,被替换了。
define 定义的标识符
//语法
#define NAME stuff
举个例子
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
那我们一般对于数字进行宏替换的时候要加;吗?
比如
#define MAX 1000;
#define MAX 1000
让我们gcc一下
就很明显的看到了错误
定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏。
下面是宏的申明方式:
#define name( parament-list ) stuff其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
比如这个例子
我们原本想的是应该是11*11 结果是121 为啥是21呢,那就看看.i 文件看看到底宏替换哪块出问题了
原来是替换之后根据运算符号的先后顺序所以结果是21那么怎样去避免这个问题呢我们在宏定义的时候这样去做
#define sq(x) (x)*(x)
这次是避免了那要是出现这种情况呢
不是按理来说应该是110吗咋变成1210了那我们再看看.i文件
跟刚刚一样的老问题还是顺序问题带来的那这次怎么办呢
#define sq(x) ((x)*(x))
带参宏就会有很多很多的问题在替换中提现到那只能尽可能的加()来避免操作顺序带来的问题
尽量不要使用这种带参数的
替换规则
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
我们知道临街字符串,具有自动连接的特性
使用#把一个宏参数变成字符串
使用##可以把位于它两边的符号合成一个符号。
比如这个例子
我的标识符x跟1就合体成x1了,我调用NUM就是x1
x+1;//不带副作用 我x还是没有变
x++;//带有副作用 我x自增了结果变化了
有这个例子
#include
#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);
}
输出结果就是 6 10 9 因为我在max里面做了一次++ 我宏替换换的是整个 比完大小之后我又加了一次这就是副作用带来的影响。
举个例子吧,比如在做leetcode中老想追求双百,时间哪块想更快咋办呀,可以将小型运算换成宏,调用函数和从函数返回的代码肯比较慢,而且更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型,宏是类型无关的!
当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
简而言之就是取消宏定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
这条gcc a.c -DNUM=100 D也就是我们的 define对NUM进行宏定义
我们刚刚在预处理的时候说了那个时候会进行条件编译那这个是啥呢,其实也就是对代码进行截断比如我现在没有小月卡或者大月卡,我现在充钱了,给了这个程序一个动作,那大月卡用户可以有的资源也就可以拥有了,换个说法也就是调用那一段代码,那段代码是实现大月卡能干什么的方法。这一段被截断的代码现在就在我的总的代码里面了,一家游戏公司不可能专门为普通用户写一套代码,为大月卡写一套,在为小月卡用户写一套,这样的成本也太高了吧。
我们已经知道#include 指令可以使另外一个文件被编译,就像他实际出现#include指令单地方一样。预处理先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次!
” “这样包含自己定义的头文件 首先是在当前目录下寻找找不到再在库函数目录下寻找
<>这样包含的头文件直接就在库函数目录下寻找