· CSDN的uu们,大家好。这里是C++入门的第十三讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
1. 加深对内存四区的理解
2. new-delete 与 malloc-free
2.1 能否用 free 释放 new 出来的空间
2.3 new 与 delete的底层实现
3. 定位new / placement-new
在学习完C语言之后,站在语言的角度,我们知道内存一般可以划分为四个区域:栈区,堆区,静态区以及常量区( 站在操作系统的角度,静态区叫做数据段,常量区叫做代码段 )。在正式学习C++的动态内存管理之前,我们先来做几道题加深一下对内存四区的理解:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
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);
}
选择题:
选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)1:globalVar在哪里?____
2:staticGlobalVar在哪里?____
3:staticVar在哪里?____4: localVar在哪里?____
5:num1 在哪里?____6:char2在哪里?____
7:*char2在哪里?___
8:pChar3在哪里?____9:* pChar3在哪里?____
10:ptr1在哪里?____11:* ptr1在哪里?____
1: globalVar 是一个全局变量,当然是在静态区的!
2:staticGlobalVar 是一个全局的静态变量是在静态区哦!
3:staticVar 是一个局部的静态变量,但还是在静态区哦!
4:localVar 是一个局部变量,当然是在栈区的!
5:num1 是一个局部的数组,当然也是在栈区的!
6:char2 是一个数组,当然是在栈区的,虽然 "abcd" 是一个字符串常量,但是使用字符串常量初始化数组,起始就是将每一个字符拷贝到栈区,然后给数组初始化的!
7:*char2 代表字符数组的第一个字符,当然是在栈区的
8:pChar3 在栈区啊!字符串常量是一个地址,pChar3 指向了这个地址,但 pChar3 这个变量本身还是在栈区的。
9:*pChar3 代表字符串常量的第一个字符,当然是在常量区的!
10:ptr1 是一个局部变量,指向堆区开辟的空间,是在栈区的!
11:*ptr1 代表整形数组的第一个元素,malloc 开辟的空间是在堆区的,因此 *ptr1 就是在堆区的!
昨晚这些练习题之后来总结一下吧:
1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段--存储全局数据和静态数据。
5. 代码段--可执行的代码/只读常量。
还记得我们使用C语言的时候是怎么向堆区申请空间的吗?我们都是使用malloc或者calloc这两个的函数。可是malloc和calloc有什么缺陷呢?
我们定义了一个类 A 代表自定义类型,然后我们用 malloc 向堆区 申请一个 A 大小的空间和一个整形大小的空间,看看申请之后的结果!
class A
{
public:
A(int x = 0)
:_x(x)
{}
private:
int _x;
};
int main()
{
A* a = (A*)malloc(sizeof(A));
int* _int = (int*)malloc(sizeof(int));
return 0;
}
我们发现对于malloc,无论是内置类型还是自定义类型,都没有进行初始化,这就不符合我们的预期了!我们想的是malloc对于自定义类型能够调用他的构造函数就好了!但是malloc表示:臣妾做不到啊!那calloc能否满足我们的需求呢?显然也是不能的,calloc只能暴力地将申请 出来的空间初始化为0!
C++的祖师爷也是看malloc,calloc非常不爽啊!于是定义了两个关键字:new,delete用于动态内存的分配和管理!我们来看看new分别会对内置类型和自定义类型做什么样的处理呢?
可以看到在我们不手动初始化的时候,对于自定义类型,会去调用他的默认构造函数;对于内置类型没有做处理。
那我们想要根据自己的需求初始化应该怎么做呢?我们只需要加一个括号然后传入我们想要初始化的值哦!
注意:
对于自定义类型加括号传值初始化本质都是调用对应的构造函数,乳沟你没有对应的构造函数时要报错的哦!
对于内置类型加括号就是直接初始化了!
new 出来的空间记得要用delete释放哦!
delete 指针;
那我们要开一个自定义类型的数组,或者内置类型的数组应该怎么做呢?
new 类型[数量];
int main()
{
A* a = new A[10];
int* _int = new int[10];
delete[] a;
delete[] _int;
return 0;
}
我们不做初始化的话,对于自定义类型还是回去调用他的默认构造函数,对于内置类型还是不会作处理!
那么我们该如何手动初始化呢?我们只需要在后面加上一个大括号,然后填上你想要初始化的值就可以了!
int main()
{
A* a = new A[5]{ A(1), A(2), A(3) };
int* _int = new int[5] {1, 2, 3, 4};
delete[] a;
delete[] _int;
return 0;
}
大括号里面如果只进行了部分内存的初始化,对于内置类型和C语言一样未手动初始化的内存会自动初始化为 0 ;对于自定义类型当然是调用他的默认构造函数啦!
注意:如果你使用 new[] 申请数组,那么释放空间的时候就必须 delete [] 。必须对应使用。
我们已经知道了使用new关键字在堆上申请空间时对于自定义类型会调用他的构造函数,那么对于delete会对自定义类型作何处理呢?
我们在构造函数和析构函数的函数体内写上打印语句代表调用了构造或析构函数:
class A
{
public:
A(int x = 0)
:_x(x)
{
cout << "A(int x = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _x;
};
int main()
{
A* a1 = new A[5]{ A(1), A(2) };
delete[] a1;
return 0;
}
可以看到:我们并没有显示的调用析构函数,但是析构函数确实是被调用了的!那这肯定是delete搞的鬼! 我们现在就要弄清楚是先delete释放空间还是先调用析构函数!你可以仔细想想,答案自然就出来了!如果你还不确定就来看看下面的代码来帮助你理解:
我们定义了一个Stack类,在构造函数中为 _a 在堆区上开辟了空间,在析构函数中释放了这块空间。在 main 函数中我们在堆区上开辟了一个对象大小的空间。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack* st = new Stack(10);
delete st;
return 0;
}
我们通过画图来理解这段程序的内存分配:
假设我们delete的时候是先释放通过new申请的空间,那么必然会造成内存泄漏,因为析构函数已经无法做到释放通过malloc申请的空间。(delete之后内存会变成随机值)因此在delete的时候一定是先调用的析构函数,释放成员变量维护的空间,然后再释放new申请的空间。
你可能会突发奇想,要是我用free来释放new出来的空间,或者使用delete释放malloc出来的空间会怎么样呢?话不多说我们直接来试一试!
我们先来用内置类型试试:
int main()
{
// new一个整形 用free释放
int* _int1 = new int;
free(_int1);
//malloc 一个整形用 delete释放
int* _int2 = (int*)malloc(sizeof(int));
delete _int2;
//new 一个整形数组 用 free 释放
int* _int3 = new int[10];
free(_int3);
//malloc 一个整形数组 用delete释放
int* _int4 = (int*)malloc(sizeof(int) * 10);
delete _int4;
return 0;
}
我们可以看到,对于内置类型 这两个可以混合使用,没有啥问题。但是编译器会报警告!
现在来看看自定义类型呢:
int main()
{
// new一个自定义类型 A 用free释放
A* a1 = new A[10];
free(a1);
//malloc 一个自定义类型A 用 delete 释放
A* a2 = (A*)malloc(sizeof(A) * 10);
delete[] a2;
return 0;
}
无论是 malloc 开空间 delete 释放空间,还是 new 开空间 free 释放空间,这是否被允许完全是由编译器决定的!上面我们的实验环境是 VS2022,但是在 VS2019 对于自定义类型就会报错了!
为了适应不同平台的需求,我们应该遵守:malloc - free,new - delete 相对应的原则。
在C++中有这样两个全局函数 operator new 与 operator delete,里面隐藏着 new 与 delete 的底层实现。这里的 operator new 与 operator delete 不是重载的 new 与 delete哈:
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 与 operator delete 的实现。起始 new 与 delete 这两个关键字就是通过调用 operator new 与 operator delete 来实现向堆区申请空间的!仔细观察 operator new 发现它居然是 通过 malloc 来申请空间的!仔细观察 operator delete 发现他是通过 _free_dbg 来释放空间的,free 其实是一个宏函数,他也是调用 _free_dbg 这个函数来释放空间的!因此 operator delete 可以理解为就是通过delete来释放空间的!
观察operator new 发现它开辟空间失败是抛异常,并不像malloc 那样返回空指针。
这些都是可以通过汇编代码和调试信息验证的哦!
我们现在来看 new 空间失败是否是抛异常的呢?
我们通过while循环一直开空间,知道开空间失败,抛出异常,然后我们不服偶这个异常并打印异常信息:
int main()
{
try
{
while (1)
{
int* a = new int[1024 * 1024];
cout << a << endl;
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
我们打印的异常信息:bad allocation 很明显new 空间失败的确是通过抛异常来处理的 。
下面我们来证明new关键字的却是先调用 operator new 在调用构造函数,delete 关键字 是先调用 析构函数再调用operator delete的。
通过调试程序转到汇编代码我们看到new的确是先调用operator new 再调用 构造函数的:
通过调试程序转到汇编代码我们看到delete的确是先调用 析构函数 在调用 operator delete的:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式: new (指针) 类型。
举个例子:我们通过malloc像堆区申请了一块空间,但是我们知道malloc并不会初始化这一块空间于是我们就可以通过定位new 指定调用构造函数来初始化这块空间。
class A
{
public:
A(int x = 0)
:_x(x)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _x;
};
int main()
{
A* a = (A*)malloc(sizeof(A));
new (a)A(10);
delete a;
}
因为要求括号内必须是指针,又因为这是内置类型,所以我们必须显示调用析构函数才能释放成员变量维护的堆区空间。
a->~A(); //显示调用析构函数
使用场景: 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如 果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。