C++
内存管理
(
整理笔记
)
C++
内存管理详解
l
内存分配方式
在
C++
中
,
内存分成
5
个区
,
分别是
堆
、栈、自由存储区、全局
/
静态区
和常量存储区
.
栈
:
存放函数参数以及局部变量
,
在出作用域时
,
将自动被释放
.
栈内存分配运算内置于处理器的指令集中
,
效率
很高
,
但分配的内存容量有限
.
堆
:new
分配的内存块
(
包括数组
,
类实例等
),
需
delete
手动释放
.
如果未释放
,
在整个程序结束后
,OS
会帮你回收掉
.
自由存储区
:
malloc
分配的内存块
,
需
free
手动释放
.
它和堆有些相似
.
全局
/
静态区
:
全局变量
(global)
和静态变量
(static)
存于此处
.(
在以前的
C
语言中
,
全局变量又分为初始化的和未初始化的
,C++
不分
)
常量存储区
:
常量
(const)
存于此处
,
此存储区不可修改
.
l
堆与栈的区别
void f()
{
int *p = new int[5];
}
上面一段代码就包含了堆与栈
.
指针
P
被分配在了栈中
,
而
new
出来的东西则被分配在了堆中
,
此句可以解释为
”
在栈中存放了一个指向堆内存的指针
p
”.(
可否理解为
:
指针
p
的值是堆内存块的首地址
??????
)
主要区别
:
管理方式不同
:
栈是编译器自动管理的
,
堆需手动释放
空间大小不同
:
在
32
位
OS
下
,
堆内存可达到
4GB
的的空间
,
而栈就小得可怜
.(VC6
中
,
栈默认大小是
1M
,
当然
,
你可以修改它
)
能否产生碎片不同
:
对于栈来说
,
进栈
/
出栈都有着严格的顺序
(
先进后出
),
不会产生碎片
;
而堆频繁的
new/delete,
会造成内存空间的不连续
,
容易产生碎片
.
生长方向不同
:
栈向下生长
,
以降序分配内存地址
;
堆向上生长
,
以升序分配内在地址
.
分配方式不同
:
堆动态分配
,
无
静态分配
;
栈分为静态分配和动态分配
,
比如局部变量的分配
,
就是动态分配
(alloca
函数
),
函数参数的分配就是动态分配
(
我想的
…
).
分配效率不同
:
栈是系统提供的数据结构
,
计算机会在底层对栈提供支持
,
进栈
/
出栈都有专门的指令
,
这就决定了栈的效率比较高
.
堆则不然
,
它由
C/C++
函数库提供
,
机制复杂
,
堆的效率要比栈低得多
.
可以看出
,
栈的效率要比堆高很多
,
所以
,
推荐大家尽量用栈
.
不过
,
虽然栈有如此多的好处
,
但远没有堆使用灵活
.
l
控制
C++
的内存分配
其实
C++
的内存管理容易而且安全
,
因为当一个对象消除时
,
它的析构函数能够安全的释放所有分配的内存
.
在嵌入式系统中
,
内存的分配是一个常见问题
,
保守的使用内存分配是嵌入式环境中的第一原则
.
当你需使用
new/delete
时
,
一个防止堆破碎的通用方法是从不同固定大小的内存池中分配不同类型的对象
(??????).
对每个类重载
new
和
delete
就提供了这样的控制
.
class TestClass
{
void *operator new(size_t size);
void operator delete(void *p);
};
void *TestClass::operator new(size_t size)
{
void *p = malloc(size);
return p;
}
void TestClass::operator delete(void *p)
{
free(p);
}
而对象数组的分配又不同于单个对象的分配
,
所以你仍需再重载
new[]
和
delete[]
操作符
.
但值得注意的是
,
对于
C++
而言
,
分配对象数组的大小等于数组参数的大小再加上额外的对象数目的一些字节
,
所以要尽量避免使用对象数组
.
class TestClass
{
void *operator new[](size_t size);
void operator delete[](void *p);
};
void *TestClass::operator new[](size_t size)
{
void *p = malloc(size);
return p;
}
void TestClass::operator delete[](void *p)
{
free(p);
}
void main()
{
TestClass *p = new TestClass[10];
delete[] p;
}
l
常见的内存错误及对策
²
内存分配未成功
,
却使用了它
解决办法
:
在使用之前检查指针是否为
NULL
,
如果指针
p
是函数参数
,
那么在函数入口处
assert(p!=NULL).
如果是用
malloc
或
new
申请的话
,
应该用
if(p==NULL)
进行防错处理
.
²
内存分配成功
,
但未初始化就使用它
解决办法 : 不要嫌麻烦 , 记得初始化就行了 .
解决办法 : 不要嫌麻烦 , 记得初始化就行了 .
²
内存分配成功且已初始化
,
但操作越过了边界
解决办法 : 此问题通常出现于循环之中 , 注意不要多 1 或少 1 就行 .
解决办法 : 此问题通常出现于循环之中 , 注意不要多 1 或少 1 就行 .
²
忘记释放内存
解决办法 : 含有这个错误的函数每调用一次就丢失一块内存 , 造成内存耗尽 . 记得 free 或 delete 就行 .
解决办法 : 含有这个错误的函数每调用一次就丢失一块内存 , 造成内存耗尽 . 记得 free 或 delete 就行 .
²
释放了内存却继续使用它
有三种情况
:
※ 程序中对象的关系过于复杂 , 难以搞清哪个对象是否已经释放了内存 .
※ 函数中 return 写错 , 返回了指向栈中的指针或引用 .
※ 程序中对象的关系过于复杂 , 难以搞清哪个对象是否已经释放了内存 .
※ 函数中 return 写错 , 返回了指向栈中的指针或引用 .
※
free
或
delete
后
,
没有将指针设为
NULL,
产生
”
野指针
”.
l
指针与数组
C++
中
,
指针和数组有着不少相似的地方
,
容易让我产生错觉
,
以为它们是等价的
,
其实不然
.
数组
在静态存储区或是栈上被创建
,
数组名对应着
(
而不是指向
)
一块内存
,
其地址与容量在生命周期内保持
不变
.
而指针可以随时指向任意类型的内存块
,
远比数组灵活
,
但也危险
.
char a[] = "hello";
a[0] = 'x';
char *p = "world";
p[0] = 'y'; //
试图修改常量字符串
,
编译器不能发现
,
执行会报错
杜绝
”
野指针
”
“
野指针
”
不是
NULL
指针
,
是指向
”
垃圾内存
”
的指针
.
它的缺省值是随机的
,
所以它会乱指一气
.
产生
”
野指针
”
的原因有
3
种
:
1
、指针变量没有被初始化
;
2
、指针被
free/delete
后被没有设置为
NULL;
3
、指针操作超越了变量的作用域范围
.
如下例
,p->fun()
时
,a
已经消失
.
class A
{
public:
void fun()
{}
};
void Test()
{
A *p;
{
A a;
p = &a; //a
的生命周期会在出作用域时结束
}
p->fun(); //p
此时是
"
野指针
"
}
l
malloc/free
和
new/delete
有了
malloc/free
为何还需要
new/delete
呢
? malloc/free
是标准库函数
,
而
new/delete
是运算符
,
它们都可用于申请
/
释放动态内存
.
但对于非基本数据类型
(
比如类对象
)
而言
, malloc/free
无法
自动执行对象的构造
/
析构
函数
.
而
new/delete
却可以
.
malloc
函数
malloc
的原型
:
void *malloc(size_t size);
函数
malloc
的使用
:
int *p = (int*)malloc(sizeof(int)*length);//length
前是乘号
可见
,
在使用
malloc
时需要进行类型转换
.
而使用
sizeof
运算符也是良好的代码风格
.
new
new
内置
了
sizeof,
所以用起来写法更简洁
.
注意
,
使用
new
创建对象数组时
,
只能
使用对象的无参数构造函数
.
如
Obj *o = new Obj[100];
l
内存耗尽怎么办
?
解决办法
:
1
、判断指针是否为
NULL,
如果是立即返回
void fun()
{
A *a = new A();
if(a==NULL)
return;
}
2
、判断指针是否为
NULL,
如果是立即终止
void fun()
{
A *a = new A();
if(a==NULL)
exit(1);
}
提示
:
不要不忍心使用
exit(1),
否则会害死
OS.
所以推荐使用方法
2
.
不过搞笑的是
,
在
32
位
OS
上
,
永远也不会内存耗尽
.