C语言基础01-运行流程
开发和运行C程序的步骤:
0:编写程序(ASCII码存储)
C语言源文件的扩展名为".c",源文件以ASCII码形式存储,不能直接被计算机执行,因为计算机只能识别二进制指令,也就是0和1
1:预处理(编译器实现,不是C语言内容)
预处理过程进行的操作:主要处理#开头的指令
何时需要预编译:总是使用不经常改动的大型代码体。
1)将所有的“#define”删除,并且展开所有的宏定义
比如讲程序中所有的NULL替换成0,C语言和C++默认将NULL替换为0
宏定义不存在类型问题,它的参数也是无类型的。只进行字符替换,没有类型安全检查。
写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
1)集成化的调试工具不能对宏常量进行调试。
2)只做简单的替换,不自动加括号
3)宏定义不可以嵌套。
4)字符串" "中永远不包含宏。
5)宏定义不分配内存,变量定义分配内存。
宏函数,替换成宏参数的相应语句,不再是简单的字符串替换:
#define SQR(X) X*X
SQR(2+1):返回2+1*2+1 也就是5
SQR(2+1)/SQR(2+1) : 返回 2+1*2+1/2+1*2+1 返回7.5
#是把宏参数转化为字符串的运算符,##是把两个宏参数连接的运算符。
自动区分参数运算与替换(当不为参数名和运算符时,字符替换不能替换成运算符)
#define SQR(X) X*X
SQR(1)替换成1
#define SQR(X) XY
SQR(任何数)替换成XY
#define STR(arg) #arg
宏STR(hello)展开时为”hello”
#define NAME(y) name_##y
宏NAME(1)展开为name_1
assert()宏
assert()是一个定义在<assert.h>的宏(也就是在头文件中define的)。
接受一个布尔值,若为真(非0)则继续执行;为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行
2)处理所有的条件编译指令如#if
条件编译:有时候希望只对其中一部分内容进行编译.此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃。
if格式:
功能:当表达式的值为真时,编译语句序列1,否则编译语句序列2或3。其中,#elif和#else可以省略
#if 表达式
语句序列1
#elif 表达式
语句序列3
#else
语句序列2
#endif
ifdef格式:
功能:当标识符已被定义时(用#define定义),编译语句序列①,否则编译语句序列②。其中#else和语句序列②可有可无。
#ifdef 标识符
语句序列①
[#else
语句序列②]
#endif
ifndef格式:
功能:该格式功能与ifdef相反
#ifndef 标识符
语句序列①
[#else
语句序列②]
#endif
头文件保护:头文件中允许包含其它的头文件,为避免头文件被重复包含,可在其中使用条件编译。
例如,为避免头文件my_head.h被重复包含,可在其中使用条件编译
#undef:取消宏定义
#ifdef:如果宏已经定义
条件编译(编译预处理中的条件命令):
#ifndef _MY_HEAD_H //如果该宏没有被定义
引入头文件
#define _MY_HEAD_H /*空宏*/ //定义空宏,就是定义成了空白符,用作标志
#elif
#else
#endif #终止#if预处理命令
3)处理“#include”预编译指令
将被包含的头文件插入到该编译指令的位置。(这个过程是递归进行的,因为被包含的文件可能还包含了其他文件)
4)删除所有的注释“//”和“/* */”。
5)添加行号和文件名标识.
方便后边编译时编译器产生调试用的行号心意以及编译时产生编译错误或警告时能够显示行号。
保留所有的#pragma编译指令,因为编译器需要使用它们。
2.编译(编译源程序,生成可识别目标文件obj)
要看什么变量,有些(静态变量)是编译的时候,有些(动态变量)是运行时候赋值
C语言代码由固定的词汇按照固定的格式组织起来(ASCII码存储源程序),CPU无法识别。
CPU只认识几百个二进制形式的指令。
这就需要一个工具(特殊的软件:编译器),将C语言代码转换成CPU能够识别的二进制指令,、
编译器能够对预处理完后的源程序 进行识别代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。目标文件中并不包含程序运行所需要的库函数等。
1)词法、语法、语义分析检查:并转换成二进制形式
词法分析:
使用一种叫做lex的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。
语法分析:
语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。用于语法分析也有一个现成的工具,叫做:yacc。
语法检查:
若出现语法错误,则编译失败。
如果编译成功则生成目标文件,目标文件名跟源程序文件名一样,扩展名为".obj"。比如,mj.c编译后生成目标文件mj.obj;
你的代码语法正确与否,编译器说了才算,我们学习C语言,从某种意义上说就是学习如何使用编译器。
编译器可以 100% 保证你的代码从语法上讲是正确的,因为哪怕有一点小小的错误,编译也不能通过,编译器会告诉你哪里错了,便于你的更改。
2)全局变量、静态局部变量初始化
确定全局变量、静态变量地址。全局变量存放在全局数据段中(静态区、全局区)。
C语言
原则:一定在编译期初始化,不能用变量进行初始化(会待定)。
对于C语言的全局和静态局部变量,不管是否被初始化,其内存空间都是全局的;
如果初始化,那么初始化发生在任何代码执行之前,属于编译期初始化。由于内置变量无须资源释放操作,仅需要回收内存空间,因此程序结束后全局内存空间被一起回收,不存在变量依赖问题,没有任何代码会再被执行!
以下称之为静态初始化:用常量来对变量进行初始化
常量初始化:有初值的全局、静态局部变量 在编译时就赋初值,放在内存的数据段(区),链接时加入执行文件的 data 段, 执行时载入 RAM 内。
零初始化:没有赋初值的全局、静态局部变量初值是 0. 。变量会被保存在 bss 段。
未初始化的全局变量在编译时因为都是 0, 所以不用在编译链接后的二进制文件中占用 ROM 的空间, ROM 内只有 BSS 段起始地址和大小, 但是在运行时要加载到 RAM 中占用 RAM 空间, 当然要先对 BSS 段清零.。
C++
原则:先分配内存,编译期可确定的则编译期初始化,其他在main之前初始化。能用变量进行初始化(会待定)
以下变量包含内置数据类型和自定义类型的对象。
编译时初始化(静态初始化):
全局变量初始化赋值常量或仅声明时,同C语言。
全局静态变量也在此初始化,线程安全。
main之前初始化(动态初始化):
全局变量 用其他变量(包括函数)来初始化时,或者是复杂类型(类)的初始化(需要调用构造函数)。
第一次调用时初始化:
(1)局部静态变量(函数内的静态变量)在第一次使用时分配内存并初始化。
在初始化语句之前设置一个局部静态变量的标识static来判断是否已经初始化,运行的时候每次进行判断,如果需要初始化则执行初始化操作,否则不执行。这个过程本身不是线程安全的。
(2)全局或静态对象当且仅当对象首次用到时才进行构造,并通过atexit()来管理对象的生命期,在程序结束之后(如调用exit,main),按FILO顺序调用相应的析构操作!
因为C++引入了对象,对象必须有构造、析构操作。由于构造和析构并非分配内存那么简单,需要执行相关代码,无法在编译期完成。
需要明确的是:静态初始化执行先于动态初始化! 能静态初始化的变量,它的初始值都是在编译时就能确定,因此可以直接 hard code 到生成的代码里,而动态初始化需要在运行时执行相应的动作才能进行。
3)局部变量等预留内存(逻辑空间)
当源码被编译成二进制文件后,其中的变量,函数的虚拟地址,也就是内存空间中的地址就已确定。
在运行时,操作系统为其分配物理内存并添加虚拟地址到物理地址的映射。
实现了定义任何一个变量时,在编译时就为其分配相应的存储单元。
但是还没有实际分配内存单元。
4)可执行代码(包括函数)放入代码区
编译器为每个函数的代码分配一个地址空间并编译函数代码到这个空间中,函数名就对应这个地址空间。
也即每个函数名都有自己唯一的代码空间。
同理,类的成员函数也是如此。
函数名的本质就是函数实体的名字,是函数实体的抽象。
每个函数线程拥有自己的代码空间和栈空间
3.链接(obj链接C语言函数库,生成可执行文件)
将所有二进制形式的目标文件和系统组件(标准库、动态链接库)组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
链接生成的可执行文件的文件名跟源程序文件同名,扩展名为".exe",计算机可以直接执行。
程序被操作系统加载到内存的时候(链接成可执行文件后),所有的可执行代码(程序代码指令、常量字符串、函数等)都加载到代码区。
这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。
可执行程序(.exe)
我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)。
在 Windows 下,可执行程序的后缀有.exe和.com(其中.exe比较常见)
可执行程序的内部是一系列计算机指令和数据的集合,它们都是二进制形式的,CPU 可以直接识别,毫无障碍;
例如,在屏幕上输出“VIP会员”,C语言的写法为:
puts("VIP会员");
二进制的写法为:
4.运行(运行可执行文件)
- 如果是在Windows环境下,直接双击".exe"文件即可运行C语言程序。
动态分配存储空间(堆、栈)
函数中的局部变量,如不专门声明为static类型存储类别,都是动态分配存储空间的,数据存储在动态存储区中。
函数中的形参和函数中定义的变量,都属此类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间。
局部变量所占用的内存空间的分配和销毁,取决于编译器的实现,编译器在为了优化程序性能,可能有不同的策略来分配、释放内存。比如:VC编译器可能在函数入口处即分配这里的全部变量,GCC编译器也可能真的在定义处才分配。
一般来说,函数进入时,将所有局部变量包括形参,入栈(该函数线程的栈)。
该探究的应该是这些局部变量的生命期。生命期都是开始于定义变量的地方,终止于语句块结束的地方(对应的反花括弧处结束)
#include<iostream>
using namespace std;
int main(){
int i=2;int j;
//cout<<j;
cout<<&i<<endl<<&j<<endl;
if(true)
{
int last;
cout<<&last<<endl; //最后入的栈: GNU GCC编译器是将所有的局部变量一起全部入栈,
//而代码体的局部变量 代码题被调用时一起全部入栈
}
int k;
cout<<&k;
}
5.总结
1)预处理,将#头文件拷贝到源程序
2)编译:检查语法、
C:静态初始化所有全局变量与局部静态变量,且放在静态存储区
C++:静态初始化部分全局变量,且放在静态存储区
3)链接:链接库,将可执行代码(包括函数,如主函数)放入到代码区。
4)运行
C++:动态初始化剩下的全局变量。
执行main
C++:其他如静态局部,对象变量等第一次使用后分配。
调用函数时,局部变量都放在栈中,函数结束,局部变量释放内存空间。