【C++】动态对象创建

在C语言中提供了内存分配函数malloc()(或calloc()/realloc())和free(),这些函数在运行时从堆内存中分配存储单元。

然而,在C++中这些函数将不能很好地运行。因为构造函数不允许我们向它传递内存地址来进行初始化。

C++是如何保证正确的初始化和清理,又允许我们在堆上动态创建对象呢?

malloc()和free()都是库函数,因此不在编译器控制范围为内。然而,如果有一个完成动态内存分配及初始化组合动作的运算符和另一个完成清理及释放内存组合动作的运算符,编译器就可以保证所有对象的构造函数和析构函数都会被调用。

一、对象创建和销毁(new/delete)

对象创建(new)

当创建一个C++对象时,将会发生两件事:

1)为对象分配内存;

2)调用构造函数来初始化那个内存。

C++解决方案是吧创建一个对象的所有动作都结合在new运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并为这块内存调用构造函数。

示例演示:

Type *ptr=new Type(2);

在运行时等价调用了malloc(sizeof(Type)),并使用2作为参数来为Type调用拷贝构造函数,this指针指向返回值的地址。在指针赋给ptr之前,它是不确定的、初始化的对象。它自动的被赋予正确的Type类型。

默认的new还进行检查以确保传递地址给构造函数之前内存分配完成。

对象销毁(delete)

new表达式的反面就是delete表达式进行对象销毁。delete表达式首先会调用析构函数,然后释放内存。

示例演示:

delete ptr

delete只用于删除由new创建的对象。如果用delete删除由malloc()创建的对象,这个动作行为是未定义的。

如果正在删除的对象的指针是0(NULL/nullptr),将不会发生任何事情。所以经常在删除指针后立即将指针赋值为0以免对它进行多次删除。

内存管理的开销

当在堆里创建对象需要一定的时间和空间开销。

首先会从堆上搜索一块足够大的内存来满足要求。这可以通过检查按某种方式排列的映射或目录来实现,映射和目录用来显示内存的使用情况。这个过程很快但可能要试探几次,所以它可能是不确定的,即每次运行malloc()并不是花费完全相同的时间。

在指向这块内存的指针返回之前,这块内存的大小和地址必须记录下来,这样以后调用malloc就不会使用它,而当调用free时,系统就知道释放多大的内存。

delete void*可能会内存泄漏

对一个void* 类型指针进行delete 操作,可能出错,因为它将不执行析构函数。

class Test
{
private:
	int* data;
	int size;
public:
	Test(int len) :size(len) {
		data = new int[size];
		cout << "constructor" << endl;
	}
	~Test(){
		delete []data;
		cout << "destructor" << endl;
	}
};

int main()
{
	Test* t = new Test(10);
	delete t;
	void* t2 = new Test(10);
	delete t2;
}

由上图输出结果可知,指针t指向一个Test对象,所以析构函数会被调用,从而释放分配给data的内存。但是t2是通过void*类型的指针指向Test对象,只会释放Test对象的内存,而不会调用析构函数,也就不会释放data所指向的内存,这就造成内存泄漏

new operator/operator new/placement new辨析

(1)new 和operator new

operator new可以作为一个常规函数显式调用,但在c++中,new是一个具有非常特定行为的操作符: 带有new操作符的表达式,首先调用函数operator new(即这个函数),将其类型说明符的大小作为第一个参数,如果成功,然后自动初始化或构造对象(如果需要)。 最后,表达式作为指向适当类型的指针计算。

A* ptr= new A ;//这个new就是new operator

底层实现:

① 调用operator new(sizeof(A))  //申请A大小的内存空间

② 调用A的构造函数A() //初始化对象

③ 返回对象指针

同理delete ptr也有operator delete

delete ptr;    //删除对象并回收内存

底层实现:

① 调用析构函数~A()//删除对象

② 调用operator delete(a)//回收内存

(2)operator new

三种形式:

//分配空间,分配失败抛出bad_alloc异常
void* operator new(std::size_t size) throw(std::bad_alloc);

//分配空间,返回null,不抛出异常
void* operator new(std::size_t size,const std::nothrow_t & nothrow_value) throw();

//分配空间,并返回一个ptr指针
void* operator new(std::size_t size,void* ptr) throw();

 

#include <iostream>
using namespace std;

class A 
{
public:
	A() {
		std::cout << "A constructor" << std::endl;
	}
	~A() {
		std::cout << "A destructor" << std::endl;
	}
	void* operator new(size_t size) {
		std::cout << "A::operator new" << std::endl;
		return malloc(size);
	}
	void* operator new(size_t size,const std::nothrow_t&nothrow_value) {
		std::cout << "A::operator new nothrow" << std::endl;
		return malloc(size);
	}
};

int main()
{
	A* ptr1 = new A;
	A* ptr2 = new(std::nothrow) A;
	delete ptr1;
	delete ptr2;
}

 输出结果:

 由上结果可知:(1)中分析是正确的,先通过调用operator new来申请内存空间,再通过构造函数构建对象初始化。

(3)placement new

实际上述operator new的第三种形式就是placement new。

它实现在ptr所指地址上调用对象构造函数进行创建并初始化一个对象。

 二、用于数组的new和delete

在堆上创建对象数组使用new[]。语法格式如下:

Test* t = new Test[10];

这样就可以在堆上为10个Test对象分配内存,并为每一个对象调用构造函数。指针t实际上是一个数组的起始地址。对于普通的指针ptr,delete ptr 释放内存是正确的,但是delete t将会出错,它只会调用一个析构函数,还有9个没有调用。

解决方案:给编译器一个信息,说明它是一个数组的起始地址。

语法格式如下:

delete []t;

[]告诉编译器产生代码,该代码的任务是从数组创建时存放在某处的对象按对应数量取回,并为数组的所有对象调用析构函数。

内存耗尽

当运算符new()找不到足够大的连续内存块安排对象时,一个new-handler的特殊函数将会被调用。new-handler的默认动作是产生一个异常。new-handler的行为和new()绑在一起,如果已经重载了new(),new-handler将不会按默认调用,也需要被重载。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值