文章目录
启动代码
c程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个c程序都已运行。
总之,高级语言程序=启动代码+自己代码。
所以,c的启动代码其实才是整个c程序的开始代码,不过由于启动代码并不是我们自己写的,一般是由编译器提供的 。
启动代码做了什么?
-
对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构。
-
为库的调用预留接口。如果程序使用的是动态库的话,编译时,动态库代码并不会被直接编译到程序中
只会留下相应的接口,程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。
启动代码一般是由汇编语言编写
在程序的内存空间结构还没有布局起来之前,高级语言程序还无法运行,此时只能使用汇编,当利用汇编编写的启动代码将高级语言的内存空间结构建立起来后,自然就可以运行c/c++等高级语言的程序了。
程序的终止
正常终止
进程主动调用终止函数/返回关键字所实现的结束,就是正常终止。
-
main调用return关键字结束
-
程序任何位置调用
exit
函数结束#include <stdlib.h> void exit(int status);//status就是返回的状态值
-
程序任何位置调用
_exit
函数结束
_exit
是一个系统函数(系统API),而exit
是c库函数,exit
就是调用_exit
来实现的。#include <unistd.h> void _exit(int status);
-
裸机时:
只能调用return
返回,因为没有os时,不支持exit
和_exit
函数。
裸机时,main的return
到启动代码后,返回动作到启动代码就截止了。 -
有os时:
return
、exit
、_exit
,使用哪种来返回都行。
推荐使用return
、exit
,因为_exit
是Linux os的系统调用,在windows就不支持了
atexit
#include <stdlib.h>
int atexit(void (*function)(void)) ;
这是一个c库函数,无论在任何位置正常终止程序,都会调用进程终止处理函数。
- 功能:注册(登记)进程终止处理函数,参数就是被登记"进程终止函数”的地址。当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的一些扫尾处理。
- 返回值
函数调用成功返回0,失败返回非零值,不会进行错误号设置。 - 参数function:需要被注册的进程终止处理函数的地址。
-
进程终止处理函数的注册顺序和调用顺序刚好相反。
调用atexit
注册时,会将"进程终止处理函数"的函数地址压入进程栈中,当进程正常终止时,又会自动从栈中取出函数地址,并执行这个函数,实现进程的扫尾操作。
栈的特点是先进后出,先压栈的后调用,所以调用顺序刚好和注册顺序相反。 -
在Linux下,调用
atexit
最多可以允许登记32个终止处理函数。 -
同一个函数如果被登记多次,自然也会被调用多次。
-
在两种情况下,登记的进程终止处理函数不会被调用
- 异常终止,不会调用
- 直接调用
_exit
来正常终止时,不会调用
换句话说,只有使用return和exit来正常终止时,才会调用。
有关标准Io的库缓存的缓冲有三种,无缓冲、行缓冲、全缓冲。
标准输出(printf
)的库缓存就是行缓冲的,在缓存中积压的数据,直到出现以下情况时,才会刷新输出,否则就一直挤压着。
-
遇到
\n
时就刷新输出,\n
表示这是一行,就好比句号表示一句话一样。 -
库缓存中数据满了,也会自动刷新输出,这就好比盆里的水满了溢出一样。
不过一般来说,数据不可能多到能够把缓存装满的。 -
调用标准
fflush
函数,主动刷新数据#include <stdio.h> int fflush(FILE *stream);
将标准IO
stream
的库缓存内的数据输出,如果stream
参数为NULL
,fflush()
flushes all open output streams. -
调用
fclose
关闭标准输出时,会自动调用fflush
刷洗数据为什么调用exit正常终止时,会刷新标准io的缓存呢?
因为
exit
会调用fclose
关闭所有的标准IO,关闭时会自动调用fflush
来刷新数据。
异常终止
进程不是因为return
、exit
和_exit
函数而终止的,而是被强行发送了一个信号,这个信号将进程给无条件终止了,这就是异常终止。
-
自杀:自己调用
abort
函数,自己给自己发一个SIGABRT
信号将自己杀死,杀人凶器是信号;#include <stdlib.h> void abort(void);
-
他杀:由别人发一个信号,将其杀死,杀人凶器也是信号;
为什么按下ctrl+c,就可以将程序终止。
其实向终端输入ctrl+c时,就是在向我的进程发送了某个信号,然后这个信号将我的程序给异常终止了。
main函数参数
int main(int argc,char **argv);
- argc存放参数个数
- argv指针数组,存放参数,第一个参数永远都是程序名。判断参数结尾:
argv[i]==NULL
将命令行参数传递给main函数形参的过程
. / a . o u t ∗ ∗ ∗ ∗ ⟶ 终端窗口进程构建指针数组 ⟶ o s 内核 ⟶ e x e c 启动代码 ⟶ m a i n 函数形参 ./a.out \ **\ **\longrightarrow 终端窗口进程构建指针数组\longrightarrow os内核\overset{exec}{\longrightarrow}启动代码\longrightarrow main函数形参 ./a.out ∗∗ ∗∗⟶终端窗口进程构建指针数组⟶os内核⟶exec启动代码⟶main函数形参
环境变量
-
eviron
全局变量
environ与main函数的argv一样,指向的都是一个字符串指针数组。只不过,argv与命令行参数有关,environ与环境变量表有关extern char **environ;
-
第三个main函数参数
int main(int argc,char **argv,char **environ);
-
调用API添加或修改环境变量
#include <stdlib.h> int putenv(char *string); int setenv(const char *name, const char *value, int overwrite); int unsetenv(const char *name); char *getenv(const char *name);
函数功能
- putenv:设置新的环境变量到环境表中。
如果这环境变量之前就存在,那么这一次的数据会无条件覆盖之前的数据。如果不存在,就添加这个新的环境变量。- string:新的环境变量+值,比如"name=value"。
- setenv函数:功能同上。
- name:环境变量的名字。
- value:环境变量值。
- overwrite:如果发现name环境变量以前就已经存在,会根据overwrite的值来决定是否覆盖,0:不覆盖,非0:覆盖
- getenv:获取环境变量
- name:环境变量的名字。
函数返回值
- putenv函数:调用成功返回0,失败返回非0,errno被设置。
- setenv函数:调用成功返回0,失败返回-1,errno被设置。
- getenv函数:调用成功返回字符串指针,失败返回NULL指针
- putenv:设置新的环境变量到环境表中。
当有os支持时,基本所有的进程都是由父进程"生"出来的:原始进程—>进程—>进程一>终端进程—>a.out进程
所有进程的"环境变量表"都是从父进程复制得到的,最原始进程的"环境变量表"则是从"环境变量文件"中读到的。
环境变量文件—>原始进程进程环境表—>子进程进程环境表—>子进程进程环境表
C内存空间布局和库
C程序的内存空间结构
-
c程序的内存空间必须布局为c程序运行所需的空间结构,c程序才能运行。
比如程序在调用函数时需要用到"栈"这个东西,那么就必须在内存空间中构建出"栈",否者c序程序没办法实现函数调用。
如果空间没有布局好,进程将无法运行,因此程序的内存空间布局是非常重要的进程环境。 -
c的内存结构是由启动代码来搭建的,比如启动代码会把c内存空间的某一部分空间构建为"栈",或者说以"栈"的方式来管理这片空间。
-
c程序的内存空间为什么也叫”进程空间”?
因为c程序在内存中运行起来后就是进程了,所以c程序的内存空间也叫"进程空间”。
不光是c程序,所有高级语言的程序在运行时,都涉及内存空间的结构布局,不过它们的结构都是相似的。