七、内存管理
7.1 new与malloc的区别,delet和free的区别?内部实现?
new 与 malloc的区别:
- new 是运算符,malloc是库函数
- new会调用构造函数,malloc只申请内存
- new返回指定类型的指针,malloc返回void指针
- new自动计算所需的内存大小,malloc需要手动设置空间
- new可以被重载
new的内部实现:
7.2 malloc, calloc, realloc, 和 alloca 申请内存的区别?
- calloc 是申请N个大小为S的空间,且会初始化空间值为0;malloc不会初始化,是随机的垃圾数据(在VS Debug模式下,会是0xcccccc这种特殊值,为了调试方便)
- malloc 是在堆上申请大小为S的一个空间,但不会初始化
- realloc 是将原本分配的内存扩充到新的大小,要求新的大小必须大于原大小
- alloca 是在栈上申请空间,不需要(不能)使用free,运行到作用域以外的时候释放申请的空间
7.3 内存泄漏(内存溢出)有哪些因素?
- 在类的构造函数和析构函数中没有匹配的调用new和delete函数 两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内 存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存
- 没有正确地清除嵌套的对象指针
- 在释放对象数组时在delete中没有使用方括号
- 指向对象的指针数组不等同于对象数组 对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间 指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了
- 缺少拷贝构造函数
- 两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。 按值传递会调用(拷贝)构造函数,引用传递不会调用。 在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。 所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
- 没有将基类的析构函数定义为虚函数
- 指针的值被篡改,导致丧失了对内存的访问方式,无法释放申请的内存
7.4 C++内存模型(堆、栈、静态区)
C++内存分为5个区域:
-
堆 heap :
由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露” -
栈 stack :
是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。函数栈内的变量地址总是连续的,从高地址向低地址生长。 -
全局/静态存储区 (.bss段和.data段) :
全局和静态变量被分配到同一块内存中。在C语言中,未初始化的静态变量放在.bss段中,初始化的放在.data段中;在C++里则不区分了。 -
常量存储区 (.rodata段) :
存放常量,不允许修改(通过非正当手段也可以修改) -
代码区 (.text段) :
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即
- 自由存储区(栈区):局部非静态变量的存储区域,即平常所说的栈
- 动态存储区(堆区): 用operator new ,malloc分配的内存,即平常所说的堆
- 静态存储区:全局变量 静态变量 字符串常量存在位置
注意:
- 栈区变量要注意析构函数的调用次序,由于是先进后出,则先创建的对象,最后被析构。
7.5 存储说明符(存储方案)有哪些?
7个存储说明符:
- auto (C++11去掉),存放在栈区的自动变量
- register 存放在寄存器的自动变量
- static 存放在静态区的静态变量
- extern 声明在外部定义的全局变量
- mutable 即使对象声明为了const, mutable成员也可以被修改
- volatile 声明不将变量放入寄存器,而是每次访问都从内存中取值,保证每次的值都是最新的
- thread_local 在整个线程周期存在的静态变量
7.6 堆与栈的区别?
- 堆是先进先出,栈是先进后出。
- 栈的大小固定,受限于系统中有效的虚拟内存,可能会发生栈溢出;堆可以动态生长
- 栈的空间有系统释放,堆内存由程序员释放
- 堆容易产生碎片
- 申请方式上,栈是系统自动分配,堆是由程序员申请
7.7 内存对齐
为什么需要内存对齐?
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2)硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。
图一:
我们普通程序员心中的内存印象,由一个个字节组成,但是CPU却不是这么看待的
图二:
cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为(memory granularity)内存读取粒度。
我们再来看看为什么内存不对齐会影响读取速度?
假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:
1.数据从0字节开始
2.数据从1字节开始
解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。
当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。
但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。
内存对齐的三个原则:
- 对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 这个数据成员的自身长度(或者可以自己设置)的倍数。
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
- 结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐
typedef struct A{ int a;//0~4 double b;//根据规则一,偏移量应该为sizeof(double)的倍数;8~15 char c;本来应该16~17但是根据规则三,最后补位16~23 }A;//所以A的大小应该为24 struct B{ int id;0~4 A a;//规矩规则二,应该为8~31; }; //所以最后的大小应该为32
7.8 memcpy 和 memmove的区别
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
memcpy和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容。他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。在内存覆盖情况下, memcpy会报错。
7.9 动态内存管理
http://blog.csdn.net/chenxin_516/article/details/41014025
动态内存管理是指管理动态内存,即堆内存。动态内存管理中常见的问题有(发生段错误的可能原因):
- 1. 野指针:一些内存单元已经释放,但之前指向它的指针还在使用。
- 2. 重复释放:程序试图释放已经被释放过的内存单元。
- 3. 内存泄漏:没有释放不再使用的内存单元。
- 4. 缓冲区溢出:数组越界。
- 5. 不配对的new[]/delete
针对1~3的问题,C++11提供了只能指针解决。此三种智能指针(unique_ptr、shared_ptr及weak_ptr)使用时,需要包含头文件:<memory>。
7.10 析构函数会在什么时候被调用?
1) 变量在离开其作用域时被销毁
2) 当一个对象被销毁时,其成员被销毁
3) 容器被销毁时,其元素被销毁
4) 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
5) 对于临时对象,当创建它的完整表达式结束时被销毁
7.11 什么是栈溢出?
栈溢出就是缓冲区溢出的一种。栈溢出就是不顾堆栈中数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的栈数据。栈是从高地址向低地址方向增涨,堆的方向相反。在一次函数调用中,栈中将被依次压入:形参,返回地址,EBP(调用地址)。如果函数有局部变量,接下来,就在栈中开辟相应的空间以构造变量。如果这些值的大小超过了函数栈的最大容量(默认的栈大小为1MB)就会造成栈溢出。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们也至多说这个程序有bug。但如果向这些单元写入的是精心准备好的数据,就可能使得程序流程被劫持,致使不希望的代码被执行,落入攻击者的掌控之中,这就不仅仅是bug,而是漏洞(exploit)了。
解决方案?
(1)用栈将递归改写为非递归
(2)使用静态变量或者动态变量替代自动变量
(3)增大函数栈的大小
#include <stdio.h>
#include <stdlib.h>
void foo()
{
printf("foo()\n");
exit(0);
}
void call()
{
int buffer[2];
buffer[3] = (int)foo; // 缓冲区溢出
}
int main(void)
{
call();
}