在我们编写的程序通常需要有不同性质或者不同类型的数据,而不同的数据是存储在不同的区域中的(如堆、栈等),将数据分类存储可以更好的管理数据。接下来,我们来看看下面一段代码中的各个类型的数据的内存分布情况:
int globalVar = 1; //全局变量,存储在静态区(数据段)
static int staticGlobalVar = 1; //静态全局变量,存储在静态区(数据段)
void Test(){
static int staticVar = 1; //静态局部变量,存储在静态区(数据段)
int localVar = 1; //局部变量,存储在栈区
int num1[10] = { 1, 2, 3, 4 }; //num1(表示数组首元素地址),存储在栈区,sizeof(num1) = 40
//char2(表示数组首元素地址),char2 及 *char2 都存储在栈区;
//该代码表示在栈区开辟一块字符数组的空间,并将字符串常量拷贝到空间中
char char2[] = "abcd"; //sizeof(char2) = 5 ; strlen(char2) = 4
//指针pChar3作为局部变量存储在栈区,指向常量池中的字符串常量,因此*pChar3存储在常量池(代码段)中
const char* pChar3 = "abcd"; //sizeof(pChar3) = 4/8 取决于运行机器是32位还是64位;strlen(pChar3) = 4
//以下代码均表示在堆区开辟一块空间,并用存储在栈上的指针ptr指向开辟的空间
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
说明:
示例代码:
void Test(){
int* p1 = (int*)malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
free(p3);
}
关于C语言中动态内存开辟方式的区别:
关于C语言中动态内存开辟方式的一些具体描述还可参见之前的博客:C语言动态内存分配
C语言中的内存管理方式在C++中可以继续使用,但对于存储如自定义类型数据的内存的开辟就不是那么方便了,而且其使用起来也比较麻烦,因此C++又提出了自己的内存管理方式:通过
new
和delete
操作符进行动态内存管理。
示例代码:
void Test(){
// 动态申请一个int类型的空间
int* ptr1 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);
// 动态申请5个int类型的空间
int* ptr3 = new int[5];
// 动态申请5个int类型的空间,并初始化为{1, 2, 0, 0, 0}
int* ptr4 = new int[5] {1, 2};
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
}
如下为调试运行代码时监视窗口中查看到了内存开辟情况:
new 和 delete 操作符使用说明示意图如下:
注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用 new[] 和 delete[] 。两对必须匹配使用。
示例代码:
class A{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A(){
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main(){
// new/delete 和 malloc/free 最大区别是:
//new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
程序运行输出结果:
说明:在申请自定义类型的空间时,new 会调用其构造函数,delete 会调用其析构函数,而 malloc 和 free不会。
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
与operator delete
函数是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间,也就是说 new 和 delete 是对 operator new 和 operator delete 的封装。
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;
申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK) //宏函数
通过上述两个全局函数的实现知道,operator new 实际也是通过 malloc 来申请空间,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足的应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。注意:operator new 和 operator delete 不是对操作符 new 和 delete 的运算符重载,而是普通的函数名,是先有的函数底层实现,才有的 new 和 delete 操作符的上层封装使用。
如果申请的是内置类型的空间, new 和 malloc ,delete 和 free 基本类似,不同的地方在于:new 和 delete 申请和释放的是单个元素的空间,new[] 和 delete[]申请的是连续空间,而且 new 在申请空间失败时会抛出异常,malloc 则会返回 NULL。
new 的原理
delete 的原理
new T[N] 的原理
delete[] 的原理
关于 delete 的实现机制:
我们可以看到,在使用 new 操作符开辟空间时,如果要开辟一块连续的内存空间,通常会指明相应的个数,而当我们要释放空间时,却不需要在 delete[] 中指明个数,这是由于编译器在我们使用 new[] 开辟内存空间时会在头上多开辟 4 个字节的空间用来存储空间对象个数以便知道之后该调用多少次析构函数来进行空间对象中资源的清理,同时返回指向待使用空间的指针,而 delete[] 在释放空间时会根据空间指针向前寻找前面空间所存储的空间对象个数,再根据此个数来判断需要调用相应对象类型的几次析构函数;但如果开辟空间时编译器判断对于该类型对象的释放不需要调用析构函数时,则不会再多开辟那 4 个字节的空间。因此,再次强调,new 和 delete ,new[] 和 delete[] ,malloc/calloc/realloc 和 free 必须匹配使用。
定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new(place_address)type
或 new(place_address)type(initializer-list)
place_address
必须是一个指针,initializer-list
是类型的初始化列表
使用场景:
定位new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式显示调用构造函数进行初始化。
示例代码:
class A{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A(){
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main(){
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
malloc/free 和 new/delete 的共同点: 都是从堆上申请空间,并且需要用户手动释放。
malloc/free 和 new/delete 的不同点:
内存泄漏: 内存泄漏是因为疏忽或错误造成程序未能释放已经不再使用的的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害: 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
示例代码:
void MemoryLeaks(){
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap Leak)
堆内存指的是程序执行中需要通过malloc / calloc / realloc / new 等等从堆中分配的一块内存,用完后必须调用相应的 free 或 delete 释放。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak
。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等却没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
在vs下,可以使用Windows操作系统提供的 _CrtDumpMemoryLeaks()
函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其它更准确的位置信息。
示例代码:
int main(){
int* p = new int[10];
// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
输出窗口:
强调: 在写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的还可以采用上述方式定位。如果工程比较大,内存泄漏位置比较多,不太好查找时,一般都是借助第三方内存泄漏检测工具处理的。
总结:内存泄漏非常常见,其解决方案分为两种:事前预防型(智能指针等);事后查错型(内存泄漏检测工具)。
以上是我对C/C++中内存管理相关知识的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!