内存操作API
内存类型
- 堆内存
- 栈内存
栈内存不需要程序员自己管理,一般都是编译器隐式管理,所以栈内存一般也被成为“自动”内存。
而程序员通过诸如malloc这样的函数申请的内存属于堆内存,这块区域需要程序员自己进行管控。
比如下面这段:
void func() {
int *x = (int *) malloc(sizeof(int));
......
}
上面函数内的这行代码中,实际上既有栈内存的创建,也有堆内存的创建。
malloc调用
malloc函数传入的参数类型为size_t
,也就是对应需要申请内存区域的大小。
如果申请成功,返回的是一个指向该区域指针,反之如果申请失败,就会返回NULL。
另外在正常使用中,malloc的参数一般都不会直接填入数字,而是通过sizeof()
函数来进行计算的,就像前面看到的那样sizeof(int)
。
sizeof()
内部也可以传入具体的变量:
int x;
int *s = sizeof(x);
这里还有一种情况需要格外注意:
int *x = malloc(10 * sizeof(int));
printf("%d\n", sizeof(x));
这段代码中,第一句申请了一块10个整型的数组空间,但是后面一句的printf()
中,此时使用sizeof
打印出的结果可能是一个非常小的值,这是因为此时sizeof
认为需要计算的只是单个int类型的大小。
如果需要得到确切的大小,可以考虑这样:
int x[10];
printf("%d\n", sizeof(x));
free调用
free()
函数本身接收一个指针类型的参数,这个指针就是前面对应malloc()
返回的指针,无需再传入具体需要释放的内存大小。
由此可见,free()
的释放实际上依赖于内存分配库本身对内存的标记。
常见的使用错误
忘记分配内存
对于用惯了Java这类编程语言的人来说,这可能初期是一个经常会犯的错误,一定要切记,使用之前一定要申请内存区域,否则会报错
没有分配足够的内存
在程序中,一定要仔细计算所需内存空间的大小,一旦内存不够,就及时申请更大的内存区域,否则程序就会出现一些错误。
忘记初始化
内存分配成功后,一定要记得对内存区域进行初始化,否则可能读到一些稀奇古怪的内容, 导致程序运行异常。
忘记释放内存
内存使用完之后,一定记得及时释放,最好的方式就是在malloc
的时候,对应及时写一个free
,然后再在两个调用之间插入具体的业务逻辑,确保不会出现内存泄漏的问题。
反复释放内存
对于已经释放的内存区域,不要反复进行释放操作,这样做会导致程序崩溃。
用完之前释放内存
这种被称为“悬挂指针(dangling pointer)”,如果在free
之后又进行了malloc
,就会导致程序数据出现问题,需要格外注意。
错误地调用free
因为free
接收的是一个指针类型的参数,所以如果传入的是一些其他指针数据,而非malloc返回的那种指针,可能就会出现各种异常情况。
补充
为什么进程退出的时候没有内存泄漏?
实际上,系统中的内存管理存在两级:
第一级是操作系统进行的内存管理,操作系统会在进行运行时,将一定的内存区域交给进程进行控制,等进程退出后,操作系统会重新收回之前分配的这部分内存空间。
第二级是进程内部进行的内存管理,如前面提到的malloc
、free
这类操作。
对于那些短期运行的程序,即使有出现没能及时释放的内存,也不用担心,因为进程退出后,操作系统不管内部有没有释放,都会统一全部回收掉,所以也就没有所谓的内存泄漏的问题了。
但是对于像web服务这种,它需要长期驻留内存中的,如果出现了这种没有及时释放的内存空间,长此以往,内存泄漏的就会越来越多,最终就是大量内存被浪费掉了,可用内存越来越少,从而导致程序崩溃。
底层操作系统支持
前面提到的malloc
和free
这类的函数,实际上它们并非是系统调用,而只是一个库调用,但是这个库调用本身也是建立在一些系统调用之上的,这里给出了两个系统调用介绍:brk、sbrk。
brk调用的作用就是改变程序分段的位置:堆结束的位置。这个调用需要一个新分段地址作为参数,然后将它和现有分段地址进行比较,如果比现有分段地址大,那就是扩充内存,如果比现有的小,那就是释放一定的内存。
sbrk的效果和brk实际上是类似的,只是它操作的是栈。
程序员本身是不建议直接使用brk、sbrk的,很容易出现各种问题,建议坚持一直使用malloc
和free
。
还有一个方法:mmap()
:它也是申请一块内存区域,但是不同的是,它申请的这块内存区域不和任何具体的文件进行关联,它是一块匿名空间,仅仅只是存在于交换空间(swap space)中,申请后,在使用上和malloc
申请的内存区域一模一样。关于交换空间,后续章节有详细介绍。
其他调用
calloc
: 分配内存,并在返回之前将其置零,如果认为内存已经归零,并且忘记初始化它,使用calloc
可以防止出现一些错误。
realloc
: 当需要为某些东西(比如一个数组)分配空间,然后需要添加一些东西时,可以考虑使用它,该方法会申请一块更大的内存区域,同时将老的内存区域的内容复制一份到新的内存空间中,然后返回新内存空间指针。