C++中的匿名对象和new与delete
一丶内存分布
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);
}
来说明一下变量的内存分布:
globalVar属于全局变量,位于静态区;
staticGlobalVar属于静态变量,位于静态区;
staticVar属于静态变量,位于静态区;
localVar属于局部变量,位于栈;
num1属于局部变量,位于栈;
char2是数组(属于局部变量),位于栈;
char2是通过拷贝常量区到栈上的数据,故位于栈;
pChar3是指针(属于局部变量),位于栈;
由于pChar3本身位于栈上,其指针的内容是指向常量区的某个字符串,因此pChar3位于常量区;
ptr1丶ptr2丶ptr3是指针(属于局部变量),位于栈;
ptr1丶ptr2丶*ptr3是在堆上开辟的空间内容,位于堆;
对于char2丶char2以及pChar3丶pChar3的图示说明:
各变量的分布图示:
这里简单做一下各个内存的说明:
1.栈又叫堆栈,存放非静态局部变量丶函数参数丶返回值等等.,栈是向下增长的。
2.堆用于程序运行时动态内存分配,堆是可以向上增长的。
3.数据段存储全局数据和静态数据,同时数据段又称静态区。
4.代码段又称常量区,存放可执行的代码和只读常量。
二丶匿名对象
匿名对象指的是无名的对象。
匿名对象的作用域的范围只在创建当行,在代码到达下一行前就会被销毁。
代码示例:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//有名对象
A aa1;
A aa2(1);
cout << "匿名对象:" << endl;
//匿名对象
A(2); //声明周期仅在当前一行 在这一行构造完后 代码到达下一行之前立即析构
cout << endl;
//有名对象
A aa3(1);
return 0;
}
运行结果:
可以看到匿名对象在创建的那一行被构造出,在代码到达下一行前匿名对象被析构销毁。
三丶new与delete
在C++中通常用new和delete操作符进行动态内存管理。
I.new和delete操作内置类型
如果申请的是内置类型的空间,new和malloc, delete和free基本类似。不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
格式:
- new开辟单个元素空间:
内置类型 标识符= new 内置类型*- 使用 delele 标识符 进行空间释放
- new开辟单个空间并对其进行初始化
内置类型* 标识符 = new 内置类型(…)- 使用 delele 标识符 进行空间释放
- new开辟大小为n数组空间:
内置类型* 标识符 = new 内置类型[n]- 使用 delele[ ] 标识符 进行空间释放
- new开辟大小为n的数组空间并进行初始化:
内置类型* 标识符 = new 内置类型[n]{…, …, …, …,}- 使用 delele[ ] 标识符 进行空间释放
代码展示:
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
// 动态申请10个int类型的空间并初始化前四个元素为1 2 3 4
int* ptr7 = new int[10]{1, 2, 3, 4};
//空间释放
delete ptr4;
delete ptr5;
delete[] ptr6;
delete[] ptr7;
}
申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用new[ ]和delete[ ]。
要遵守一个配套使用的原则:new和delete丶new[ ]和delete[ ]。
II.new和delete操作自定义类型
new和delete操作自定义类型时和操作内置类型时的使用方法是一样的,但在操作自定义类型时会调用对象的构造和析构。
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
cout << "A(int a)" << endl;
}
A(int a1, int a2)
{
cout << "(int a1, int a2)" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Test1()
{
//自定义类型
//new不仅开辟空间 还将调用构造
//new一个对象 通过默认构造函数
A* p2 = new A;
// new一个对象 通过有参构造函数
A* p3 = new A(2);//此处不是匿名对象
//delete不仅释放空间 还将调用析构
delete p2;
delete p3;
cout << endl;
}
void Test2()
{
//new一个对象数组 所有的对象通过默认构造生成
A* p1 = new A[10];
//释放资源
delete[] p1;
cout << endl;
//不仅new一个对象数组 而且在new的时候进行一些初始化操作
A aa1(1);
A aa2(2);
A aa3(3);
A aa4(4);
cout << endl;
//注意:编译器可能优化拷贝构造过程
A* p2 = new A[4]{ aa1,aa2,aa3,aa4 };
delete[] p2;
}
void Test3()
{
//利用单参构造的隐式转换进行初始化
A* p5 = new A[4]{ 1,2,3,4 };
delete[] p5;
cout << endl;
//利用构造函数的隐式类型转换进行混合的初始化
A* p6 = new A[6]{ 1,2,3,{4,5},{5,6},{7,8} };
delete[] p6;
}
int main()
{
Test1();
Test2();
Test3();
return 0;
}
C++兼容C,允许使用malloc/calloc/realloc和free进行资源管理,但C++一般使用new和delete进行资源管理,这是因为new和delete比malloc等的功能更齐全,使用new和delete不仅能完成自身的资源管理,还能通过调用自定义类型对象的构造和析构对对象内的数据进行资源管理,这是十分便利的。此外,new和delete相比于malloc等,还包含了异常,当开辟或释放空间出错时会抛出异常,而malloc等不具备这样的能力。
对于内置类型来说,new和delete与malloc等差异不大;但对于自定义类型来说,new和delete会调用构造和析构,这是根本的差异。
III.深入new和delete
new和delete是用户在进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
- new的原理:
1.调用operator new函数申请空间;
2.在申请的空间上执行构造函数,完成对象的构造。
- delete的原理:
1.在空间上执行析构函数,完成对象中资源的清理工作;
2.调用operator delete函数释放对象的空间。
- new T[N]的原理:
1.调用operator new[ ]函数,在operator new[ ]实际调用operator new函数完成N个对象空间的申请;
2.在申请的空间上执行N次构造函数。
- delete[ ]的原理:
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理;
2.调用operator delete[ ]释放空间,实际在operator delete[ ]中调用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来释放空间。
于是new和delete对于自定义类型,可以这么说:
new = malloc + 构造函数 +异常。
delete = 析构函数 + free + 异常。
IV.定位new表达式
定位new表达式是在已分配的原始空间中调用构造函数初始化一个对象。
***使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
- 定位new表达式在实际中一般配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显式调用构造函数进行初始化。
代码示例:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a):" <<(void*)this<< endl;
cout << _a << endl;
}
A(int a1, int a2)
{
cout << "(int a1, int a2)" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Test1()
{
//相当于new
//开辟空间
A* p1 = (A*)operator new(sizeof(A));
//利用定位new来初始化已有的空间
//new(p1)A; //对已有空间调用默认构造初始化
new(p1)A(10); //对已有空间调用有参构造初始化
//相当于delete
p1->~A();
operator delete(p1);
}
void Test2()
{
//开辟多个对象
A* p2 = (A*)operator new(sizeof(A) * 10);
//利用定位new直接初始化
//new(p2)A[10]{ 1,2,3,4 };
//相当于new[ ]
for (int i = 0; i < 10; i++)
{
new(p2 + i)A(i);
}
//相当于delete[ ]
for (int i = 0; i < 10; i++)
{
(p2 + i)->~A();
}
operator delete[](p2);
}
int main()
{
Test1();
Test2();
return 0;
}
V.malloc/free和new/delete的相同点和区别
malloc/free和new/delete的相同点:
- 都是从堆上申请空间,并且需要用户手动释放。
它们的不同点可以从两个角度——用法和原理上讲。
在用法上:
- malloc和free本身都是函数,而new和delete本身是操作符;
- malloc申请的空间不会初始化,而new可以进行初始化;
- malloc申请空间时,需要手动计算空间大小并传递,new只需要在其后跟上空间的类型即可。如果是多个对象,[ ]中指定对象的个数即可;
- malloc的返回值为void*,在使用时必须强转。而new不需要,因为new后跟的是空间的类型;
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空。而new不需要,但是new需要捕获异常;
在原理上:
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用析构函数和构造函数。而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中的资源清理。
VI.malloc/free和new/delete交错使用时的出现的问题
对于内置类型来说,new配套free丶malloc配套delete使用并无影响。但对于自定义类型来说,这样交错地使用可能会出现一些意想不到的问题。
先来看一段代码:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
cout << "A(int a):" <<(void*)this<< endl;
}
A(int a1, int a2)
{
cout << "(int a1, int a2)" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* p2 = new A[10]; //x86下大小为44个字节 大小为40多开了4个字节
delete[] p2;
return 0;
}
我们通过反汇编查看p2指向的空间所占多少字节:
在x86下占44个字节(16进制下的2C等于10进制44)。
理论上计算类对象的大小,A类对象的大小为4,那么10个A类对象总共的大小应当为40才对。那么多出来的4字节是干什么的呢?
这多出来的4个字节也是开辟出来的空间,它用于记录对象的个数,也就是需要析构的次数。对象在创建后,一旦不需要了就需要析构,这是必要的资源管理。
现在多出的4个字节是在我们自行实现了析构的基础上。若我们没有自行实现析构,那么它的大小就会变为40个字节:
就是说此时没有再多开辟空间来记录对象析构次数的信息。
好,那么现在我们知道了在编写了析构的情况下,采用new来创建连续的空间去存放对象时,会多开辟一块大小为4字节的空间用于记录对象总共的析构次数。
在自行编写了析构的情况下,new和free混合使用时,会发生崩溃:
出现了内存泄露的崩溃报告。
当注释掉实现了的析构后,程序又可以正常运行:
出现这样的原因是因为free释放内存的位置不正确导致的。我们来看有无析构时new为对象开辟连续空间的内存图:
当使用delete[ ] p2时,若未自行编写析构,那么释放的位置是从p2开始的;若自行实现了析构,那么释放的位置会从记录析构个数的空间开始释放,就是说此时将开辟的空间全部析构了,不会出现内存泄露的问题。
本博客仅供个人参考,如有错误请多多包含。
Aruinsches-C++日志-4/22/2024