目录
🌊1.程序的翻译环境和执行环境
在ANSI c的任何一种现实中,存在两个不同的环境。
⚡️第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
⚡️第二种是执行环境,它适用于实际执行代码。
🌊2.详解编译+链接
🌀2.1 翻译环境
- ⚡️组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
- ⚡️每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- ⚡️连接器同时也会引入标准c函数库中任何被该程序所用到的函数,而且她可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
vs2019–集成开发环境IDE(Integrated Development Environment )
编辑+编译+链接+调试
集成了编辑器、编译器(cl.exe)、链接器(link.exe)、调试器
🌀2.2 编译本身也分为几个阶段:
❄️预处理
⚡️生成test.i
- 头文件的包含
- 删除注释的内容
- 替换define的内容
在Linux环境下验证一下
❄️编译
⚡️生成test.s
把c语言代码转换成汇编代码
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
在Linux环境下验证一下
❄️汇编
- 生成test.o(目标文件)
- 把汇编代码 转换为二进制代码
- 形成符号表
那么,生成的myfile.o 是可执行程序吗?
答案是:不是的
可以看到在尝试运行myfile.o之后,请求被拒绝,加上了可执行权限之后仍然无法执行。
🌀2.3 链接
❄️链接
- 合并段表
- 符号表的合并和重定位
在这里涉及到一个重要的概念:函数库
- 我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
- 最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到
系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用
函数库一般分为静态库和动态库两种。
1、静态库和静态链接:链接的时候,如果是静态链接,找到静态库,拷贝静态库中的我所需要的代码到我自己的可执行程序当中,其后缀名一般为“.a”
2、动态库和动态链接:链接的时候,如果是动态链接,找到动态库中的我需要的代码的地址到我自己的可执行程序中的相关位置。动态库一般后缀名为“.so”,如前面所述libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,如下所示。 gcc hello.o –o hello
3、静态链接成功:我们的程序,不依赖任何库,自己就可以独立运行
4、动态链接成功:我们的程序,还是依赖动态库,一旦动态库缺失,我们的程序便无法运行
5、静态库,因为自身拷贝的问题,比较浪费空间
6、动态库因为可以做到被大家共享方法,所以真正的实现永远都是在库中,程序内部只有地址,比较节省空间
7、Linux默认使用的是动态库和动态链接,这点可以通过 file 命令验证
🌀2.3运行环境
⚡️程序执行的过程:
- 程序必须载入内存中,在有操作系统的环境中,一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始,接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行堆栈(Stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(Stack)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
🌊3.预处理详解
🌀3.1预定义符号
__FILE__//进行编译的源文件
__LINE__//文件当前的行号
__DATE__//文件被编译的日期
__TIME__//文件被编译的时间
__STDC__//如果编译遵循ANSI C,其值为1,否则未定义
❄️3.2.1#define
-
1.定义标识符
-
2.定义宏
#define机制包括了一个规定,允许吧参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
下面是宏的声明方式:
#define name(paraent-list)stuff
其中的parament-list是一个逗号隔开的符号表,他们可能出现在stuff中
⚡️注意
参数列表的左括号必须与name紧邻
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#include<stdio.h>
#define MAX(x,y) (x>y?x:y)
int main()
{
int a = 10;
int b = 20;
printf("%d",MAX(a,b));
return 0;
}
❄️3.2.2 #define替换规则
⚡️在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
-
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,他们首先被替换。
-
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对接过文件进行扫描,看看他是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意 -
宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
-
当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。
❄️3.2.3 #和##
☁️#的作用
⚡️如何把参数插入到字符串中
int main()
{
printf("hello world!");
printf("hello""world!");
return 0;
}
最后的运行结果一样,说明字符串是有自动连接的特点的
使用# ,把一个宏参数变成对应的字符串
#include<stdio.h>
#define PRINT(val,format) printf("the value of”#val “is ”foramt\n",val)
int main()
{
int a = 10;
PRINT(a,"%d");
int b = 20;
PRINT(b,"%d");
int f = 3.5f;
PRINT(f,"%f");
return 0;
}
☁️##的作用
##可以把位于它两边的符号合成一个符号
它允许宏定义从分离的文本片段创建标识符
#define CAT(A,B) A##B
int main()
{
int Class107 = 100;
printf("%d\n",CAT(Class,107);
return 0;
}
运行结果:100
❄️3.2.4带副作用的宏参数
当宏参数在红的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致一些不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
❄️3.2.5 宏和函数对比
⚡️宏通常被应用于执行简单的运算
比如在两个数中找出较大的一个
#define MAX(a,b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
⚡️原因有二:
- 用于函数调用和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多
⚡️所以宏比函数在程序的规模和速度方面更胜一筹
- 更为重要的是函数的参数必须声明为特定的类型
所以函数只能在类型合适的表达式上使用。反之宏则可以适用于任何可以用>来比较大小的类型。
⚡️宏是无关类型的
⚡️宏的缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
- 宏是没法调试的
- 宏由于无关类型,也就不够严谨
- 宏可能会带来运算符优先级的问题,导致程序容易出现错误。
宏有时候可以做到函数做不到的事。比如:宏的参数可以出现类型,但是函数做不到
#include<stdlib.h>
#include<stdio.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
//int* p1 = (int*)malloc(sizeof(int) * 10);
int* p2 = MALLOC(10, int);
for (int i = 0; i < 10; i++)
{
*(p2 + i) = i;
printf("%d ", p2[i]);
}
return 0;
}
运行结果:(可以看到p2成功开辟)
宏和函数的一个对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅的增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一点 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在穿餐的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的参数,即使他们执行的任务是相同的。 |
调试 | 宏不方便调试 | 函数时可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
❄️3.2.6 命名的约定
⚡️一般来讲函数的宏的使用语法很相似,所以语言本身没发帮我们区分二者
那我们平时的一个使用习惯是:
把宏名全部大写
函数名不要全部大写
但是有例外
offset
getchar
🌀3.3 #undef
⚡️这条指令用于移除一个定义
🌀3.4 命令行定义
许多c的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个极其内存大些,我们需要一个数组能够大些。)
🌀3.5 条件编译
⚡️在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令、
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ //如果符号定义了,就参与编译
printf("%d ", arr[i]);
#endif __DEBUG__
}
return 0;
}
运行结果
常见的条件编译指令:
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
现在我们在Linux环境下编写了这样一段代码,放到microtest.c这个文件下
现在对文件进行预处理生成test.i文件
打开test.i文件
我们发现,满足条件,条件编译里的语句被保留下来参与编译
同理。下面这段代码的条件编译也很好理解
值得一提的是,这里的条件语句与python里面的条件语句if ,elif ,else和c语言中 if ,else if ,else 相类似,都是只会执行一个分支下的语句。
test.i里面的内容
🌀3.6 文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方
一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
编写下面代码,多次包含microtest.h头文件
在生成的test.i文件中我们发现这个头文件被编译了多次
❄️3.6.1 头文件被包含的方式:
- 本地文件包含
#include “filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
如果找不到就提示编译错误。
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
注意按照自己的安装路径去找。
- 库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
❄️3.6.2 嵌套文件包含
我们可以看到,如果多个文件都包含了comm.h这样的话,在进行预编译的时候会对该头文件多次展开多次编译。很明显是会影响程序运行的效率的。那么我们该怎样避免这种情况的发生呢?
- ⚡️方法一:
每个文件的开头写
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H
在microtest.h文件中编写下面代码
在microtest.c中编写下面代码
将头文件多次包含,来测试是否与编译的时候是否会将头文件多次展开。
test.i文件里的内容
可以看到头文件中的内容(Add函数)只留下了一个。
- ⚡️方法二:
#pragma once
test.i里面的内容
可以看到头文件中的内容(Add函数)也只留下了一个。
🌊4.总结
这一篇博客介绍了程序的翻译环境和执行环境,大致了解了一个程序先写好之后如何从一个.c文件变成.exe可执行程序。同时也了解了一些预处理的指令语法,在今后写项目的时候会有很大的帮助!最后我是Maria,一个来自重庆的女孩,现在在读大二,希望和大家一起学习,一起进步!!!