原文链接:blog,转载注明来源即可。
本文代码:Github
本文结构
执行程序:main 函数
定义
main()
是 C 程序的主函数,是程序执行的入口,Golang 与之类似,C99 标准对 main 的 2 种正确的定义:
- // 无参数
- int main(void) {
- return 0;
- }
- // 有参数
- int main(int argc,char *argv[]) {
- return 0;
- }
- // main 还可有第三个参数 char *ecvp[],用于存储运行环境的环境变量
为提高程序的可移植性,避免使用下边对 main 定义的形式:
- // 没有纳入 C 的标准,大部分编译器是不允许这么写的
- void main() {
- // ...
- }
参数值
在程序中就能检查并获取运行参数了(a.out 相当于 Windows 上的 a.exe):
@H_502_29@
返回值
return 0;
返回给操作系统程序的执行状态为 0,表示正常退出,返回其他值则认为程序发生了错误。如:
- /* demo.c */
- int main(void) {
- return 233.7; // 返回值会被强制转换,截断为整型 233
- }
在 Unix 上使用 $?
来验证程序的退出状态:
@H_502_29@
说明:在 shell 中执行 cc demo.c && ./a.out
后 a.out
作为 shell 的子进程运行,在退出时将 0 返回给了 shell,故能使用 echo $?
查看其退出状态。
终止进程
终止的 8 种方式
5 种正常终止
- main() 中 return
- exit()
- _exit() 或 _Exit()
- 多线程程序中,最后一个线程从其 main() 中 return
- 多线程程序中,从最后一个线程调用 pthread_exit()
3 种异常终止
- abort()
- 接到一个信号
- 多线程的程序中,最后一个线程对取消请求作出响应 // 多线程部分在第 12 章
退出状态
状态范围
- // 退出状态码为 0 ~ 255,超出后则返回 n % 256
- int main(void) {
- exit(257); // 返回 1
- }
状态不确定的三种情况
atexit()
函数
atexit()
注册的函数称为 exit handler,在进程退出时会被 exit()
自动调用后再清理缓冲区。有 2 个特点:
特性:
- int main(void) {
- if (atexit(myExit2) != 0) {
- err_sys("can't register myExit2"); // 终止函数先注册后调用
- }
- if (atexit(myExit1) != 0) {
- err_sys("can't register myExit1");
- }
- if (atexit(myExit1) != 0) { // 终止程序登记一次就会被调用一次
- err_sys("can't register myExit1");
- }
- printf("main is done\n");
- return 0;
- }
- static void myExit1(void) {
- printf("first exit handler\n");
- }
- static void myExit2(void) {
- printf("second exit handler\n");
- }
运行:
@H_502_29@
限制:
书上说一个进程使用 atexit()
最多注册 32 个清理函数,但后来的操作系统有所差异,使用 sysconf
查看这个限制值:
差距太大了,于是我在 macOS 上使用 for 循环验证了一下:
- int main(void) {
- for (int i = 0; i < 214748367; i++) {
- if (atexit(myExit) != 0) { // 终止程序登记一次就会被调用一次
- err_sys("can't register myExit1");
- }
- }
- return 0;
- }
- static void myExit(void) {
- printf("exit handler\n");
- }
程序能正确调用,只是内存占用会飙升233:
@H_502_29@
C 程序的启动与终止流程:
命令行参数
在 main() 函数的参数值已讨论,不过遍历命令行参数的结束条件还有另一种方式:
- // for (i = 0; i < argc; i++) { // 循环输出全部的命令行参数
- for (i = 0; argv[i] != NULL; i++) { // POSIX 和 ISO C 标准都要求 argv[argc] 是空指针
- printf("argv[%d]: %d\n",argv[i]);
- }
进程的环境表
ecvp
参数
环境参数是name=value 格式的字符串,能通过第三个参数 char *ecvp[]
接收到环境表:
- int main(int argc,char *argv[],char *ecvp[]) {
- for (int i = 0; ecvp[i] != NULL; i++) { // POSIX 要求 argv[argc] 是空指针
- printf("ecvp[%d]: %s\n",argv[i]);
- }
- }
输出的环境参数:
- ecvp[8]: LANG=zh_CN.UTF-8
- ecvp[9]: PWD=/Users/wuyin/C/apue
- ecvp[10]: SHELL=/bin/zsh
- ...
- ecvp[16]: HOME=/Users/wuyin
- ecvp[18]: USER=wuyin
environ
全局变量
环境表与命令行参数表一样,也是字符指针数组,指针指向各环境参数。其地址存储在全局变量 environ
中:
- #include <stdio.h>
- #include <unistd.h>
- extern char **environ; // 使用来自 unistd.h 的外部变量 environ,类型是指向指针的指针
- int main(int argc,char *ecvp[]) {
- char **env = environ;
- while (*env != '\0') { // 环境参数字符串以 NULL 结尾
- printf("%s\n",*env);
- env++;
- }
- return 0;
- }
进程的内存分布
@H_502_29@
常驻内存:从进程开始到退出一直存在,使用常量地址访问。
静态区域
正文段
只读数据段
已初始化数据段
未初始化数据段
- 图中的 .bss
- 内容:程序中没有初始化的数据:比如仅声明,使用默认零值的变量
动态区域
堆
- 用于动态内存分配
- 一般由程序员手动分配 malloc 和 释放 free
栈
进程间的共享库
@H_502_29@
参考 阮一峰:编译器的工作过程
在 link 链接阶段,C 程序引用的库分为 2 种:
静态库 *.a、*.lib:外部函数库添加到可执行文件,体积大,但适用性更高
动态库(共享库)*.so、*.dll:外部函数库只在运行时动态引用,体积更小,但适用性更低
内存分配
动态内存分配函数
- #include<stdlib.h> // 定义
- // 分配 size 字节大小的内存区域
- // 成功则返回内存地址,失败返回 NULL
- void *malloc(size_t size);
- // 分配 nobj 个长度为 size 字节的内存区域,并将每一个字节都初始化为 0
- // 成功则返回内存地址,失败返回 NULL
- void *calloc(size_t nobj,size_t size);
- // 为 ptr 指向的空间分配新的 newsize 字节的内存
- // ptr 必须指向动态分配的内存,即三个 *alloc() 函数的返回值
- // ptr 为 NULL:realloc() 与 malloc() 相同
- // newsize 为 0: ptr 指向的空间会被释放,返回 NULL,类似 free()
- // 更大: 存储区域有足够的空间可扩充,则扩充后直接返回 ptr
- // 存储区域没有足够的空间可扩充,复制 ptr 指向区域的数据到更大的内存空间
- // 类似 Golang 中 slice 在 len 接近 cap 之后进行内存重新分配的操作
- void *realloc(void *ptr,size_t newsize);
注意
返回值类型
返回值都是 void *
,不是没有返回值或返回空指针,而是返回通用指针,指向的类型未知,类似于 Go 中的 interface{} 类型。在使用时,需要将返回的 void *
进行强制类型转换,方便存储数据
内存释放
必须使用 void free (void* ptr);
来手动释放分配的内存,如果忘记释放的内存累计过多,进程将可能出现内存泄漏
对一块内存只能释放一次,调用多次 free()
将出错
使用示例
- #include <stdio.h>
- #include <stdlib.h>
- int main() {
- int n = 2;
- int *buf1 = (int *) malloc(n); // 强制将 void* 转换为 int*
- if (buf1 == NULL) { // 检查是否分配成功
- exit(1);
- }
- for (int i = 0; i < n; i++)
- printf("buf1[%d]: %d\n",buf1[i]); // malloc 分配的空间值的值是未知的
- int *buf2 = (int *) calloc(n,sizeof(int)); // calloc 分配的空间有默认值 0
- for (int i = 0; i < n; i++)
- printf("buf2[%d]: %d\n",buf2[i]);
- free(buf1);
- // free(buf);
- // malloc: *** error for object 0x7fd9b4d000e0: pointer being freed was not allocated
- // 没有 free(buf2) 则可能发生内存泄漏
- return 0;
- }
运行:
@H_502_29@
环境变量
查询环境变量的值
- #include<stdlib.h>
- char *getenv(const char* name); // 环境变量 name 存在则返回值的指针,不存在则返回 NULL
设置环境变量的值
- #include<stdlib.h>
- // str 是 name=value 格式的参数,用于新增或覆盖 name
- // 执行成功返回 0,失败返回 -1
- int putenv(char *str);
- // 设置 name 环境变量的值为 value
- // 若 rewrite == 0 则新增或不覆盖
- // != 0 则新增或覆盖
- int setenv(const char *name,const char *value,int rewrite);
- // 删除 name 环境变量,不存在也不会报错
- int unsetenv(const char *name);
参考前边环境表的 environ 全局变量,操作环境参数时更推荐使用上边的函数。
setjmp 与 longjmp 函数
函数内跳转
在 C 中使用 goto 在函数内部(栈中)跳转,可往前也可往后跳。不过为了提高代码的可维护性,应尽量少使用。除非你明确要使用它来跳出深层次的循环,那也不错。
函数间跳转
在 C 中使用 setjmp()
与 longjmp()
在函数之间(栈之间)跳转:
示例
- #include <stdio.h>
- #include <setjmp.h>
- #include <stdlib.h>
- jmp_buf saved_state;
- void myLongjmp();
- int main(void) {
- printf("设置 setjmp 的锚点\n");
- int ret_code = setjmp(saved_state);
- if (ret_code == 1) {
- printf("结束序号为 1 的跳转\n");
- return 0;
- }
- int *flag = (int *) calloc(1,sizeof(int));
- myLongjmp(); // 直接跳转到第 11 行
- }
- void myLongjmp() {
- printf("准备开始序号为 1 的跳转\n");
- longjmp(saved_state,1);
- }
效果:
@H_502_29@
内存泄漏
使用 Valgrind 做内存泄漏检测,结果显示有 4 字节的内存 lost,即是 16 行分配的内存没有释放:@H_502_29@
在 setjmp()
和 longjmp()
之间分配的内存,在跳转后直接就废弃了,可能会因此发生内存溢出。
和 goto 一样,除非你知道自己在做什么,否则应尽量避免使用。
getrlimit 与 setrlimit 函数
系统对进程能调用的资源有限制,可使用 getrlimit()
来查看、setrlimit()
来修改
函数原型
- #include<sys/resource.h> // 定义
- // resource 是标识资源类型的常量
- // rlimit 的定义
- struct rlimit {
- rlim_t rlim_cur; // current (soft) limit // 当前软链接的限制值
- rlim_t rlim_max; // maximum value for rlim_cur // 硬链接的限制值
- };
- // 修改成功返回 0,失败返回非 0
- int getrlimit(int resource,struct rlimit *rlptr);
- int setrlimit(int resource,struct rlimit *rlptr);
可修改的资源值
参数值 | 参数说明 |
---|---|
RLIMIT_AS | 进程可使用的最大存储 |
RLIMIT_NOFILE | 进程可打开的最大文件数 |
RLIMIT_STACK | 进程的栈的最大长度 |
… | … |
更多请参考:手册
总结
开始学习 APUE 就卡在了第三章,于是从第七章熟悉的 main()
开始学起,发现 C 和 Golang 真的有千丝万缕的联系,比如 atexit()
与 defer func(){}
,另外还有进程相关的内存分布、内存分配都值得深入学习,后边依旧在本篇笔记中补充。
计划下周 4.27 前更新第八章笔记 :)