从我们指尖敲下的一行行代码到可以运行的程序之间到底经历过什么,我们可以从这一篇文章来了解了解
(1)程序的翻译环境和执行环境
在ANSIC的任何一种实现中,都存在两种不同的环境:翻译环境和执行环境。
翻译环境的作用是把源代码转换成可执行的机器指令
执行环境则用于实际执行代码
从test.c(源代码)——>翻译环境——>text.exe——>执行环境
翻译环境(以vs环境为例)存在两个装置:cl.exe(编译器)和link.exe(链接器)
生成的test.exe中存放的是二进制指令(机器指令)
在我们以后参加的公司项目中,大多需要团队协作,在一个工程中也会存在很多的源文件和头文件
翻译过程:·
注:每一个源文件都需要单独经过编译器生成对应的目标文件(.obj)
目标文件在与链接器来连接生成可执行程序(如上图)
不同的平台的标准会有所差异,vs平台上已经是集成平台所以编译过程可能不会显现出来
建议使用gcc平台,以下以gcc环境为例
从源文件到目标文件的过程中,编译器需要进行三个步骤
1.预编译(预处理)test.c —E -O——>test.i
2.编译 test.i —S——>test.o
3.汇编 test.o —C——>test.exe
预处理的作用:
头文件的包含(include)
注释的删除
#define定义的符号的替换
编译:
作用:把C语言代码翻译成汇编代码
过程分为语法分析,词法分析,语义分析,符号汇总四个部分(由于是初解暂时不做了解)
汇编:
作用:把汇编代码转换成二进制指令,形成符号表
经过编译器形成的目标文件再通过链接器生成可执行程序
链接的过程有:
合并段表
符号表的汇总和重定位
在之前,我们似乎提到过很多次符号这两个字,编译里的符号汇总,汇编过程中形成符号表,链接中符号表的合并和重定位
那么,这些过程有什么区别呢?
编译(符号汇总):对代码进行分析,判断哪些是全局符号,像函数,全局变量等
汇编(形成符号表):根据地址和符号对应形成符号表,
由于编译时是单独编译,可能会出现同一符号但地址不同的情况
链接(符号表的合并和重定位):为跨源文件进行服务
前提是目标文件已经是二进制文件(有固定格式)
以gcc环境中编译产生的目标文件为例,其格式为elf(可执行程序的格式同样也为elf)
两种相同格式的函数经过链接生成可执行程序
把目标文件分成段,两个目标文件对应段存放的数据是相同的
不同源文件中有全局符号,通过编译,汇编,链接的过程来将全局符号与其地址一一对应
便于记录和查看
运行环境:
我们先来了解程序执行的过程:
1.程序必须载入内存(这一步由操作系统来完成,要是独立环境中需要手动完成)
2.程序开始执行,调用main函数
3.开始执行代码,程序将使用运行时堆栈(也就是函数栈帧),来储存函数的局部变量和返回地址
当然,程序也可以使用静态内存,储存于静态内存的变量在整个程序过程中一直保留它们的值
4.终止程序,正常终止main函数,也可能会意外终止
预处理详解
预处理符号:
__FILE__: 进行编译的源文件
__LINE__文件当前的行号
__DATE__文件被编译的日期
__TIME__文件被编译的时间
__STDC__如果遵循ANSIC,其值为1,否则未定义
用法举例:
效果:
#define
1.定义标识符
格式 #define name stuff
2.定义宏
#define机制包括了一个规定,允许把参数替换到文本当中,这种实现通常为宏或定义宏
方式:#define name(parament list(有逗号隔开的符号表)) stuff
注:函数列表左括号必须与name紧邻
且stuff会原封不动的传过去
并且由于是原封不动的传过去,可能会导致计算的错误,最好通过使用括号来控制运算的优先级,来保证运算结果的正确性
#define在替换时也有相应的规则
1、在调用宏时,首先对参数进行检查,看看是否包含由#define定义的符号如果是,则首先被替换
2、替换文本随后被插入到程序原来的位置,对于宏和参数名被他们的值所替换
3,最后,再次对结果文件进行扫描,如果有#define定义的符号,重复上述
注:宏参数和井define定义中可以出现其他开define定义的符号.
但是对于宏,不能出现递归
当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索
(3)“#”一把一个宏参数变成相应的字符中
把a传入printf_format函数,用#修饰的num输出时是a,而不是num
“##” 可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符
(4)带副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,副作用就是表达成录值的时候出现的永久性效果
宏和函数的对比
宏常被由于简单的运算
有两个原因:
首先:用于调用函数和从函数返回的代码可能比实际这个小型计算机所需要的时间更多
因为函数的时间花费大致有三个方面
一是调用前的准备,包括传参,函数栈帧空间的维护
二是主要的运算过程
三是函数返回,返回值的处理,函数栈帧的销毁
而宏只有运算时的时间消耗,所以宏在程序的规模和速度方面更甚一筹
其次,函数的参数必须声明为特定的类型,而宏是类型无关的
但是,宏也有缺点
每次使用宏时,可能会大幅增加程序长度
宏也没法调试
由于类型无关,也就不够严谨
宏可能带来运算符优先级的问题,导致程序容易出错
不能递归
在敲代码的时候,有一个命名约定:宏在命名的时候全部大写
函数在命名的时候不全部大写
#undef——用于移除一个宏定义
#undef name
命令行定义:有些环境中,允许在命令行中定义符号,由于启动编译过程
条件编译:
在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或放弃就可以使用条件
文件包含:
头文件包含的方式有两种:
本地文件包含使用双引号#include” "(现在本地工程下寻找,找不到再在标准路径下寻找)
库文件包含使用<>:#include<>(只在标准路径下寻找)
嵌套文件包含:可能会导致文件内容的重复
可以用条件编译指令来解决
#ifndef __TEST_H__
#define __YEST_H__
//头文件的内容
#endif //__TEST_H__
或
#pragma once