目录
第一部分 语法篇
第三章 内存管理
在VC中,栈空间未初始化的字符默认是-52,补码是0xCC。0xCCCC在GBK编码中就是"烫"。
堆空间未初始化的字符默认是-51,两个-51在GBK编码中就是"屯"。
建议27:区分内存分配的方式
程序由4部分组成:
- 代码区,存放程序的执行代码。无法控制。
- 数据区,存放全局数据、常量、静态变量等。自由存储区、全局/静态存储区、常量存储区。
- 堆区,存放动态内存
- 栈区,存放程序中用到的局部数据。
内存的5个区: - 栈区,存储函数内部变量,函数执行完自动释放。栈内存分配运算内置于处理器的指令集,效率很高,缺点是分配的内存容量有限。
- 堆区,由new运算符分配的内存块,由
delete
释放。如果没释放,程序结束后操作系统自动回收。 - 自由存储区,由
malloc
等分配的内存块,用free
释放。 - 全局/静态存储区,
C
的全局常量分为初始化和未初始化,C++
里没区分。 - 常量存储区,存放常量,不允许修改。
堆与栈的区别:
- 管理方式:栈由编译器自动管理;对由开发人员自己管理。
- 空间大小:堆内存可以占据整个内存空间;栈内存一般只有一定大小的空间。
- 碎片问题:频繁地
new/delete
会造成内存空间不连续;栈的数据是连续的。 - 生长方向:堆是向上增长;栈是向下增长。
- 分配方式:堆都是动态分配的。栈可能是静态或者动态分配的。栈的静态分配由编译器完成,动态分配由
alloca
函数完成,由编译器释放。 - 分配效率:堆内存的分配效率很低,还可能引发用户态和和心态的切换。栈内存的分配效率很高。
因此要在合适的地方采用合适的内存分配方式。
建议28:new/delete与new[]/delete[]必须配对使用
内置类型没有构造、析构函数,所以使用delete
或delete[]
一样。
建议29:区分new
的三种形态
-
new operator
,最常见的new
运算符。
语言内建的,不能重载,也不能改变其行为。做如下三件事:
(1)分配内存(2)调用构造函数(3)返回对象的指针 -
operator new
,申请原始内存。也就是new operator
的第一步。与C
库中的malloc
函数很像。 -
placement new
,选择调用哪个构造函数。也就是new operator
的第二步。
三种形态的用法:
- 如果只是想在堆上建立对象,使用new operator。
- 如果只是想分配内存,使用operator new。
- 如果想在已经获得的内存里建立一个对象,使用placement new。
operator new
与placement new
的示例:
class A{
public:
A();
A(int a);
~A();
void* operator new(size_t size);
void* operator new[](size_t size);
void operator delete(void*ptr, size_t sz);
void operator delete[](void*ptr, size_t sz);
public:
int a;
};
A::A():a(0)
{
cout << "construct A" << endl;
}
A::A(int a):a(a)
{
cout << "construct A" << endl;
}
A::~A() {
cout << "destruct A" << endl;
}
void * A::operator new(size_t sz)
{
cout << "custom operator new for size " << sz << endl;
return ::operator new(sz);
}
void * A::operator new[](size_t sz)
{
cout << "custom operator new for size " << sz << endl;
return ::operator new(sz);
}
void A::operator delete(void* ptr, size_t sz)
{
std::cout << "custom operator delete for size " << sz << endl;
::operator delete(ptr);
}
void A::operator delete[](void* ptr, size_t sz)
{
std::cout << "custom operator delete for size " << sz << endl;
::operator delete(ptr);
}
int main(int argv, char* args[]) {
cout << "1. operate new" << endl;
A *ptrA = new A(123); // operator new for size 4
cout << "(*ptrA).a: " << (*ptrA).a << endl;
cout << "sizeof ptrA: " << sizeof(ptrA) << endl; // sizeof ptrA: 8
delete ptrA; // custom operator delete for size 4
cout << endl;
size_t len = 3;
A *ptrB = new A[len]; // custom operator new for size 20
cout << "sizeof ptrB: " << sizeof(ptrB) << endl; // sizeof ptrB: 8
cout << "sizeof A: " << sizeof(A) << endl; // sizeof A: 4
for (size_t ik = 0; ik < len; ++ik) {
cout << "(*(ptrB+" << ik << ")).a: " << (*(ptrB + ik)).a << endl;
}
delete[] ptrB; // custom operator delete for size 20, (20=8+4*3)
cout << "\n2. placement new" << endl;
void* s = operator new (sizeof(A)); // 分配内存
A* ptrC = (A*) s;
::new(ptrC) A(2019); // 调用一个参数的构造函数,ptrC->A::A(2019);
cout << "(*ptrC).a: " << (*ptrC).a << endl;
::new(ptrC) A(); // 调用没有参数的构造函数,ptrC->A::A();
cout << "(*ptrC).a: " << (*ptrC).a << endl;
delete ptrC;
return 0;
}
/* Output:
1. operate new
custom operator new for size 4
construct A
(*ptrA).a: 123
sizeof ptrA: 8
destruct A
custom operator delete for size 4
custom operator new for size 20
construct A
construct A
construct A
sizeof ptrB: 8
sizeof A: 4
(*(ptrB+0)).a: 0
(*(ptrB+1)).a: 0
(*(ptrB+2)).a: 0
destruct A
destruct A
destruct A
custom operator delete for size 20
2. placement new
construct A
(*ptrC).a: 2019
construct A
(*ptrC).a: 0
destruct A
custom operator delete for size 4
*/
建议30:new内存失败后的正确处理方式
malloc
函数在申请内存失败后会返回NULL。
new
在申请内存失败后会抛一个异常,不会执行if(ptr == NULL){}
// new失败抛异常
char *pStr = new string[SIZE];
if(pStr == NULL){
// 如果new失败,不会执行
return false;
}
// new失败不抛异常
char *pStr = new(std::nothrow) string[SIZE];
if(pStr == NULL){
// 如果new失败,不会执行
return false;
}
如果想检查new是否成功,则应该捕捉异常:
try{
char *pStr = new string[SIZE];
}catch(const bad_alloc& e){
return -1;
}
一般来说,new失败可能是内存不足引起的,捕获这个异常没有意义,可以直接core dump。
建议31:new内存失败后会调用new_handler函数
建议32:检测内存泄露工具
原理:截获对内存分配和内存释放的函数的调用。每当分配一块内存时,将其加入一个全局内存链中,每当释放一块内存时,将其删除。程序结束后,内存链中剩余的指针就是未被释放的。
(1)Windows平台:MS C-Runtime Libraay、BoundsChecker、Insure++
(2)Linux平台:Rational Purify、Valgrind
建议33:operator new/operator delete的重载
- 重载的
operator new
必须是类成员函数或全局函数,不可以是某一个命名空间之内的函数或全局静态函数。 - 因为
operator new
是在类的具体对象被构建出来之前调用的,在调用operator new
的时候this
指针尚未诞生,因此重载的operator new
必须是static的。同理,operator delete
也是static的。 -
operator new
与operator delete
成对重载 -
delete
会调用free
函数
建议34:智能指针
RAII(Resource Acquisition In Initialization)
智能指针实际是一个指针类,将指针通过构造函数传递给指针类的对象,当这个对象析构时,将指针delete。
不能使用临时的share_ptr对象。当将share_ptr对象作为函数参数时,可能会由于函数参数求值顺序,引起内存泄露。
建议35:内存池
当程序中需要频繁地申请大小相同的内存,用内存池技术能提高效率。
内存池技术通过批量申请内存,降低内存申请次数,节省时间,且能减少内存碎片的产生。
网友评论