文章目录
1.内存管理
程序内存划分
在C++中,内存地址空间(在进程层面,体现为进程的虚拟地址空间中的用户空间)被划分为四部分:
- 栈 Stack
- 堆 Heap
- 数据段 Data Segment
- 代码段 Code Segment
-
栈:从高地址向低地址增长。在程序运行过程中开辟函数栈帧,保存创建的临时变量(函数的非静态局部变量、函数参数、返回值等),当函数执行结束时,这些存储单元自动被释放。详见
-
堆:从低地址向高地址增长。用于程序运行时的动态内存分配,在C/C++中,由malloc/new创建的数据对象就保存在堆上,与栈不同的是,堆上开辟的空间必须由程序员自己管理,即用完要调用free/delete释放,否则会造成程序运行过程中的内存泄露(若程序退出,OS回收自动空间了)。
-
数据段:数据段保存的是全局数据(global data)和静态数据(static data),在C++中包括已初始化数据和未初始化数据,C语言则有所区分。
-
代码段:代码段也可以称为常量数据区,用于保存可执行代码和只读常量。
静态分配和动态分配
内存分配的方式有两种:静态分配和动态分配。
对于堆,堆都是动态分配的,堆上的空间都是在运行时被分配的,需要程序员手动分配和释放。
对于栈,栈可以是静态分配也可以是动态分配,静态分配由编译器完成,如局部变量的创建、函数调用信息、函数栈帧大小;动态分配由alloca函数进行分配,与堆不同的是,栈上动态的分配内存由编译器释放。
如何理解静态分配和动态分配?
静态分配:静态分配是在程序运行前,即编译时的内存分配。程序编译时,编译器为程序员声明的局部变量分配内存空间,具体大小根据变量类型而定。内存单元一旦被静态分配,大小无法再修改。(通俗理解,静态分配只是在编译时确定变量未来存储在内存空间中的地址和大小,形成相应的机器代码,并且在生成的机器代码中使用已知的内存地址来引用这些变量。等到程序加载时,CPU只需根据编译时已确定的地址为变量开空间和赋值,不需要进行额外的内存管理操作)
在C/C++程序的运行过程中,函数栈帧的创建、局部变量的分配一系列对栈空间的分配行为虽然看起来是动态的,但是内存“分配多大”、“分配到哪”都是编译时已经决定了,运行时无法改变,所以栈的分配方式看作是静态分配。
动态分配:动态分配是在程序运行时的内存分配。C库函数例如 calloc()
和 malloc()
或者操作符 new
均支持分配动态内存。动态分配的内存空间,通过这些函数或操作符的返回值赋值给指针变量。
2. C语言的内存控制
C语言的内存动态分配:
malloc
,calloc
,realloc
等函数分配内存,用free
函数释放内存。这是老生常谈了,但是这几天产生一些对于free()
函数的疑惑。下面是malloc
和free()
的函数原型。
void *malloc(size_t size);
void free(void *ptr);
malloc
的参数是一个整型size,用于指定开辟空间的大小,这很好理解。但是,free
的参数只有一个指针ptr,指向即将释放空间的起始位置,它是如何知道要释放多大的空间呢?经了解,不同的malloc算法实现方式,大概有两种主流的做法:
-
malloc
时,在分配的空间前另外开辟一个整型空间(或者是一个存放内存块信息的结构体),用于保存这块空间的大小,这样一来,free
只需回退指针参数ptr
,找到要释放的空间大小即可。 -
起始地址
ptr
可以直接通过某种算法转换为空间大小。
参考文章:Understanding glibc malloc
3. C++的内存控制
new和delete
C++中通过new和delete两个关键字进行动态内存管理。
Why
相比于C语言,C++引入了类对象的概念,对象需要在创建时调用构造函数,销毁时调用析构函数,那么C语言中的动态内存管理接口如
malloc
和free
就无法满足这种需求,因为它们只是简单的从堆上开辟空间和释放空间!
new和delete的使用
void test2()
{
//开辟一个int对象,值为默认(0)
int* pa1 = new int;
//开辟一个int对象,指定值为1
int* pa2 = new int(1);
//开辟int数组
int* parr1 = new int[10];
int* parr2 = new int[5]{1,2,3,4,5};//since c++11
int* parr3 = new int[5]{1,2,3};since c++11, 后两个数是默认值0
int* parr4 = new int[0];//可以, 但解引用行为是未定义的
/*If this argument is zero, the function still returns a distinct non-null pointer
on success (although dereferencing this pointer leads to undefined behavior).*/
//释放空间
delete pa1;
delete pa2;
delete[] parr1;
delete[] parr2;
delete[] parr3;
delete[] parr4;
}
//对于内置类型,new和delete的使用亦是如此,new无非就是开一块空间,然后拿一个值去初始化,delete时就释放空间就行。而对于自定义类型,new除了开辟空间,初始化的操作需要调用类的构造函数,delete释放空间之前也要先调用析构函数。
void test3()
{
People* p1 = new People;//调用默认构造函数
People* p2 = new People(18, "Jcole");//传参调用构造函数
People* p3 = new People[3];
People* p4 = new People[3]{{10,"Bill"}, {20, "Tyler"}, {30, "Lamar"}};
delete p1;
delete p2;
delete[] p3;
delete[] p4;
}
test3的执行结果
default constructor #p1
call the constructor of People #p2
default constructor #p3
default constructor
default constructor
call the constructor of People #p4
call the constructor of People
call the constructor of People
call the destructor of People #delete
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
operator new和operator delete
new
和delete
是C++中用于动态管理内存的两个操作符(关键字),而其底层分别调用了operator new
和operator delete
,这是两个全局函数,operator new
用于开辟新的空间,operator delete
用于释放空间。下面是两个函数的源代码。
operator new
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 new 实际是调用
malloc
开辟空间,当申请空间成功时,直接返回指针;如果申请失败,尝试执行预设的应对措施,若用户并无预设应对措施,则抛异常。
operator delete
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 delete实际是调用
free
释放空间
new和delete的实现原理
至此,对于一个动态内存管理的自定义对象(内置类型没有构造和析构),它在程序中的”历程“是这样的
-
对于
new T[N]
,底层是调用operator new[]
。先在operator new[]
中实际调用operator new
函数完成N个对象空间的申请,然后在申请的空间上调用N次构造函数。 -
对于
delete[]
,底层是调用opertor delete[]
。先在即将释放的空间上调用N次析构函数清理资源,然后调用operator delete[]
释放空间,实际在operator delete[]
中调用operator delete
来释放空间。
值得注意的一点是,
operator new[]
的参数是申请空间的字节大小size
,但是使用new[]
分配数组时是指定数组元素个数。不必担心,当你使用new[]
来分配一个数组时,编译器会根据数组类型的大小和元素数量计算所需的总字节大小,并将其传递给operator new[]
。
⭕特别注意,必须严格遵守new with delete和new[] with delete[]的匹配使用,否则会导致不可预期的后果!!
4.Some Problems
Problem #1: delete[]怎么知道调用多少次析构函数
当你使用 new[]
分配一个数组array,array使用结束后,就必须使用delete[]
释放,如下:
class Cls
{
public:
Cls(int i = 0):i_(i){}
~Cls()
{
std::cout << counter++ << " call ~Cls()" << std::endl;
}
private:
int i_;
static int counter;
};
int Cls::counter = 0;//计数器,记录delete[]时调用析构函数的次数
const long long len = 10;
void test1()
{
Cls* array = new Cls[len];
delete[] array;
}
int main()
{
test1();
return 0;
}
运行结果
1 call ~Cls()
2 call ~Cls()
3 call ~Cls()
4 call ~Cls()
5 call ~Cls()
6 call ~Cls()
7 call ~Cls()
8 call ~Cls()
9 call ~Cls()
10 call ~Cls()
那么问题来了,当我们创建Cls类数组时,显式传入了数组长度10,这非常正确,
operator new
能够根据传入的数组长度,申请合适的内存空间。然而,当array使用结束后释放时,并没有传入数组长度或空间大小,那么delete[]
怎么知道要调用多少次析构函数呢?根据运行结果,delete[]
确实正确地调用了10次析构函数。
实际上,在C++中,new T[]
申请数组空间时,会在头部额外开辟一个8字节(x64)的空间,用以存储long long
类型的数组长度(元素个数),但返回指针时跳过了这一段空间,返回的是有效空间的首地址。
因此,当调用delete[]
时,函数拿到了有效空间的首地址ptr,只需让ptr回退8个字节(一个long long
类型的长度),找到数组长度,即可知道需要调用多少次析构函数!
operator new[]
接收到的参数size_t size
是实际数组长度,包括额外的头部;operator delete[]
接收到的参数void* o
指向的是包含额外头部的整个数组空间。因此可以通过重载operator new[]
和operator delete[]
,观察动态数组的实际长度和实际释放地址。
验证demo:
class Cls
{
public:
Cls(int i = 0):i_(i){}
~Cls(){}
private:
int i_;
};
void* operator new[](size_t sz)
{
printf("数组实际长度: %ld\n", sz);
void* p = malloc(sz);
return p;
}
void operator delete[](void* fp)
{
printf("实际释放的地址: %p\n", fp);
free(fp);
}
const long long len = 10;
void test1()
{
Cls* array = new Cls[len];
char* ptr = (char*)array;
printf("数组预期长度: %d\n", len*sizeof(Cls));
printf("数组首地址: %p\n", ptr);
delete[] array;
}
验证结果
数组实际长度: 48
数组预期长度: 40
数组首地址: 0x18b3c28
实际释放的地址: 0x18b3c20
Problem #2: new/delete和new[]/delete[]的匹配使用
new
和delete
是用于动态分配单个对象的内存,而new[]
和delete[]
则用于动态分配数组的内存。如果你在分配内存时使用了new
,却使用了delete[]
来释放内存,或者使用了new[]
,却使用了delete
来释放内存,会导致未定义的行为(Undefined Behavior)。这种不匹配的使用会导致内存泄漏或者程序崩溃等严重后果
例如,使用new[]
分配内存,却使用了delete
来释放内存。
void test4()
{
// 对于内置类型
int* p1 = new int[10];
delete p1;
// 可能不会出错,因为delete内置类型对象时,只需回收空间,不用调用析构函数
// 对于自定义类型
Cls* p2 = new Cls[10];
delete p2;
// 出错,delete p2只会调用一次析构函数,其它对象都没有析构,释放空间后可能会导致内存泄漏
}
又如,使用new
分配内存,却使用了delete[]
来释放内存。
void test4()
{
//未定义
int* p3 = new int;
delete[] p3;
//可能在非法空间调用了析构函数
Cls* p4 = new Cls;
delete[] p4;
}
在不同平台上,上述两种情况产生的后果可能会不同,可能会出错,也可能“侥幸”成功运行,因为这是未定义行为(Undefined Behavior)。因此,必须严格遵守new with delete和new[] with delete[]的匹配使用。
Problem #3: new/delete和malloc/free的区别
malloc()
和free()
是函数,new
和delete
是操作符。new
和malloc()
都是在堆上申请动态内存,并返回指向这块空间的指针。new
在申请空间成功后,会进行初始化操作:对于内置类型,赋值初始化;对于自定义类型,调用构造函数对其进行初始化。malloc()
却不会初始化。delete
和free()
都是对动态内存进行回收。而delete
会在回收空间前,调用析构函数,进行对象的资源清理,free()
却不会。new
在分配失败时会抛出std::bad_alloc
异常,而malloc()
在分配失败时返回空指针。delete
在删除空指针时是安全的,而free()
不接受空指针。new
和delete
的操作对象是类的实例对象,而malloc()
和free()
的操作对象是一块内存空间。因此:- 使用
malloc()
开辟空间时需要用户指定空间大小(以字节为单位),而new
不用,只需在其后说明空间对象的类型,如果是多个对象,在[]
中指定个数即可` malloc()
返回的是void*
类型的指针,使用时需要手动类型强转;而new
返回的就是空间中对象的类型指针。
成功后,会进行初始化操作:对于内置类型,赋值初始化;对于自定义类型,调用构造函数对其进行初始化。malloc()
却不会初始化。
- 使用
delete
和free()
都是对动态内存进行回收。而delete
会在回收空间前,调用析构函数,进行对象的资源清理,free()
却不会。new
在分配失败时会抛出std::bad_alloc
异常,而malloc()
在分配失败时返回空指针。delete
在删除空指针时是安全的,而free()
不接受空指针。new
和delete
的操作对象是类的实例对象,而malloc()
和free()
的操作对象是一块内存空间。因此:- 使用
malloc()
开辟空间时需要用户指定空间大小(以字节为单位),而new
不用,只需在其后说明空间对象的类型,如果是多个对象,在[]
中指定个数即可` malloc()
返回的是void*
类型的指针,使用时需要手动类型强转;而new
返回的就是空间中对象的类型指针。
- 使用