首先我们回味一下之前的老图,这个图由于是我手残加 ppt 即时创作,又因为是C语言入门时讲的,内容非常粗糙磕碜。要仔细研究这张图我们应该将它翻转90度会更加容易理解更贴近原理:
我们所熟知的,栈区数据存储的地址是从高地址到低地址,堆区数据存储的地址则是由低到高,而堆区下面可细分为未初始化和已初始化的全局数据区,字符常量区和代码区。而细心的你可能注意到了我代码区下面留了一撮空间代表下面还有,但这一撮属于灰色地带,目前看作为 “内存” ,但本质上不是内存,涉及到计算机操作系统原理不赘述。
从内存中0x000……0到堆区的地方其实基本上伴随整个程序的运行一直都存在,我们相对熟悉的就是栈区和堆区,栈区的我们函数调用后临时变量在栈帧中形成,随着申请与释放来进行空间管理。
那伴随整个程序的运行一直都存在的这部分数据,像 static 这类函数修饰的变量,为什么又会被改变生命周期呢?其实在编译的时候就被编译进了全局数据区。
指针与内存关系👏
void function(char *a) { return 1; }
我们在写函数时如果内容传的是指针,如果有好的习惯一般会先去对指针做一下合法性判定,这个判定什么意思,比如我们传了一个野指针,它会指向内存中任何一个位置,我是没有办法确认这个随机位置有无访问权限,所以要做合法性判定。
但是指针如果有具体的指向,对应的合法性我们是没办法验证的,包括野指针,是不是很疑惑,野指针不是随机指向,说白了就是乱指,那还没办法验证吗?是的,没办法。很简单,确认指针具体值的合法性,这不是咱作为用户可以做到的,这属于操作系统职责。
这种尴尬的情况我们所谓的合法性判定怎么搞呢?我们所谓的“合法”是落足于应用层面。其实所有的指针在没有被使用时,我们都应该设置成 NULL,这是一个规范问题。
在函数内部要验证指针合法性时,本质上就是在验证指针( !=NULL)。可以直接 if 判断,还有就是很多书中用的一个检查指针的宏——assert ,一般是在调试阶段使用,assert(name)如果内部条件不满足非空,就会直接咔嚓掉,中道崩殂没有后续。但是不好意思,assert只能检验是否 NULL,不能检验是否为野指针。
内存分配与初始化细节👏
之前就想专门提一下几个和内存空间有关联的函数,现在就放在这里一起总结了吧。
我们为指针分配了内存,但是内存大小多少会影响实际结果,不够就会造成越界。
char *p = "hello"; char *q = (char*)malloc(sizeof(char)*strlen(p)+1*sizeof(char)); strcpy(q,p);
p是字符串变量,长度为 5 个字符,但实际内存占用 6 个字符,不要忘了" \0 ",所以我们做 +1 处理,分配完了记得要初始化,初始化为非必须操作,但建议初始化,这是为了能让咱编码盘的明明白白。我们初始化变量时直接 0 或者 NULL,数组可以 = {0},也可以使用 memset 函数:
memset(a,0,sizeof(a));
它的 3 个参数分别代表起始地址,初始化设置的值以及设置的内存大小,单位为字节。
内存泄漏👏
int main() { while(1) { int *p = malloc(1024); } }
这里不做测试了,这会让电脑越来越卡,死循环加申请空间,程序级别的老赖,空间只借不还,以上代码就生动诠释了何为内存泄漏。
给个C语言之外的问题:程序挂了,已经退出了,那内存泄露问题还在吗?我自己的想法是在的,因为内存已经被申请了,退出程序只是终止了空间继续申请,不影响已产生的空间。但是我错了,其实在程序退出时,操作系统会强制拿回这部分空间,内存泄漏也就不在了。
所以诸位警惕windows的操作系统和杀毒软件这类常驻进程,几乎从来不会退出,最怕的就是内存泄漏,蓝屏安排,卡顿安排;但后端的服务器也是如此,无时无刻提供服务,一但内存泄露就会嘿嘿。
Cookie👏
malloc 之后空间要给 free 掉,我们 free(p)目前只知道堆空间的起始地址,并不知道要释放多少空间,如果 p 是 5 个字节,那么 free 一定会释放的比5个字节多,那么辩证思维,其实申请的空间就一定会比 5 个字节多。
编译器是怎么做到正确释放呢?其实实际 malloc 申请空间的时候,系统就会给的更多,多出来的部分,记录的就是申请的详细信息:空间大小,申请时间等等,free 会确认信息然后精准 free掉。
这部分多申请的空间叫 cookie,内存级的 cookie,就是用来保存这些信息的。再延伸就是C语言的边界操作系统了,不赘述。
所以我们在 malloc 时肯定是申请大空间会更好,因为 cookie 的比例会更小,想象一下利息相同时你会借多借少就能体会了