- 我们在编写代码的时候,不知道大家是否和一开始的我一样,在运行代码的时候就直接CTRL+F5运行了呢??一开始,我只知道会生成一个.exe的可执行文件,中间的原理我一点也不知道。
- 今天就由我带领大家对生成可执行的文件有更深的一层理解。
- 程序的翻译环境和执行环境
- 在ANSI C的任何一种实现中,存在两个不同的环境。
- 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第二种是执行环境它用于 实际执行代码。
- 组成一个程序的每个源文件通过编译过程分别转换目标代码。
- 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C库函数中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数链接到程序中。
翻译也分为几种过程:
- 在预处理中,我们可以通过在Linux环境下,用gcc text.c -E -o text.i 来看一看在预处理中编译器做了什么
- 我们发现短短10行的代码变成了700多行,这是因为在预处理中,包含的头文件被展开了,而且宏已经被替换了。
- 在这里,大家有没有想过一个问题呢??在预处理阶段中,到底是宏替换先执行还是去注释先执行呢???
经过证明,我们发现去注释是先执行的,如果是宏替换先执行的话,那么就不会有Hello World的输出了。
预定义的符号:
- _FILE_ //进行编译的源文件
- _LINE_ //文件当前的行号
- _DATE_ //文件被编译的日期
- _TIME_ //文件被编译的时间
- _STDC_ //如果编译器遵循ANSI C ,其值为1,否则为定义
关于宏的定义,其实本质上就是在预处理的阶段被替换。
我们来看这个代码就知道了,我们用宏定义了SQUARE ,如果没有我刚刚说宏的本质是替换是不是很多人会以为答案是36,经过我上面说的,宏的本质是替换,所以就替换成了printf("%d\n",5+1&1+5);这个,答案就是11
#define SQUARE(x) x*x int main() { int a = 5; printf("%d\n", SQUARE(5 + 1)); return 0; }
宏的好处:
- 在用于函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间多,所以宏比函数在程序上的规模和速度上更胜一筹。
- 第二个就是宏是无关类型的
宏的坏处:
- 宏不能递归,也不好调试
- 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能产生不可预料的问题
- 综上所述,其实我更推荐用函数,函数可以调试,而且在C++中用inline也综合了宏和函数的优缺点。
命名约定
宏名全部大写 函数名不要全部大写
命令行定义
在许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要 编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果 机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
- 比如,下面代码我就用到了命令行定义
常见的预处理指令
- #ifndef
- #define
- #if
- #endif
- #elif
- ...
- 当然还有很多预处理指令,这里就不再叙述了。
- 这里预处理指令中,我们常见的就是
- #ifndef include <stdio.h>
- #define include <stdio.h>
- #endif
- 这里是为了不让头文件重复包含,在我们的第一个图中就讲到了,在预处理中头文件会被展开,所以开text.i文件中会有700多行的代码
- 当然#pragma once也可以防止头文件被重复包含
- 最后:讲一讲大家在做大型项目中,头文件和定义总是分开的,都会用到#include "text.h" 或#include <filename.h>,我来讲一讲它们之间的差别。
- 其实它们只是查找策略的不同,#include "filename.h"查找的策略是现在源文件所在的目录下查找,如果找不到编译器就会查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误
- 而#include <filename.h> 是直接查找库函数头文件中的标准位置中查找头文件