【C++】new和模板

new

new的功能和C语言里malloc的功能是一样的,都是在堆上动态开辟内存空间。那为什么C++里面不继续使用malloc而是又重新造了一个功能一样的new呢?

#include <iostream>

using namespace std;

int main()
{
	int* a = new int[10];
	int* b = (int*)malloc(sizeof(int) * 10);
	for (int i = 0; i < 10; ++i)
	{
		a[i] = i;
		cout << a[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 10; ++i)
	{
		b[i] = i;
		cout << b[i] << " ";
	}
	cout << endl;
	return 0;

}

比如我们用自定义类型在堆上开辟动态的内存空间时,malloc没有办法很好的支持动态申请的自定义对象初始化,因为malloc根本就不会调用构造函数,但是new不同,new会先去调用一个叫operato new的函数,开辟空间,然后再去调用构造函数。
我们现在先认为上面的结论是对的,等把new深入了解了之后再来证明。

如何使用new

我们在使用new动态开辟空间时,直接把存储的类型放在new的后面,new会自动计算大小,所以不需要对其进行强制类型转换,”new + 类型“。这种方法是开辟单个空间时的用法。
如果我们要开辟多个空间,就要在类型的后面加上方括号,方括号里面填入你想开辟的空间个数。”new + 类型 + [要开辟空间的个数]。

单个空间初始化

new在开辟的时候是支持初始化的,如果我们要对单个类型进行初始化,就在类型后面加上括号,括号里面填入你想初始化的内容,这种方法很容易和开辟多个空间是的new狠相似,不要弄混。“new + 类型 + (初始化的内容)。

多个空间初始化

同样new也是支持多空间初始化的,在方括号后面加上花口号,在花括号里面填写初始化的内容即可,如果你填入的初始化数据,小于开辟的空间时,剩余的空间会初始化成零(当然这是对int类型来进行初始化时的结果,不同类型会有所不同)。”new + 类型 + [要开辟空间的个数] + {初始化的内容}。

#include <iostream>

using namespace std;

int main()
{
	int* a = new int[10];
	int* b = new int[10] {0, 1, 2, 3, 4};
	int* c = new int;
	int* d = new int(10);
	for (int i = 0; i < 10; ++i)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 10; ++i)
	{
		cout << b[i] << " ";
	}
	cout << endl;
	cout << *c << endl;
	cout << *d << endl;
	return 0;

}

delete

像malloc要把空间释放需要使用free,new会使用delete,用法是"delete + 你要释放的空间"。这是对于单个空间释放调用的delete,对于多个空间释放需要调用delete[],用法是"delete + [] + 你要释放的空间"。对于这个两种使用方法一定要匹配使用,否者运行的结果是未知的。

#include <iostream>
using namespace std;

int main()
{
	int* a = new int[10];
	int* b = new int[10] {0, 1, 2, 3, 4};
	int* c = new int;
	int* d = new int(10);
	for (int i = 0; i < 10; ++i)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 10; ++i)
	{
		cout << b[i] << " ";
	}
	cout << endl;
	cout << *c << endl;
	cout << *d << endl;
	delete[] a;
	delete[] b;
	delete c;
	delete d;
	return 0;
}

delete在底层上会先去调用对应类型的析构函数,再去调用operator delete的函数将空间释放。

new与malloc的区别

new与malloc最大的区别就是new简化了用法,我们不需要对类型的大小进行计算。
new是支持初始化的但是malloc没有支持。
new和malloc在开辟空间失败时malloc返回的时空指针,需要我们自己来断言一下,比较麻烦,new会抛异常。这个抛异常需要使用两个东西try,catch,来进行捕获。
我们需要把new放入到try里面,catch会对try里面的异常信息进行捕捉。
比如执行到new的位置出现异常,就会直接跳转的catch里面,如果没有出现异常就会跳过catch。

#include <iostream>

using namespace std;

int main()
{
	try
	{
		char* a = new char[0x7fffffff];
		cout << a << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;

}

malloc只会开辟空间不支持初始化,但是new可以。
对于自定义类型的初始化,new是支持的,它会先开辟空间,调用一个operator new的函数,然后再去调用对应自定义类型个默认构造,而malloc根本就知道构造函数是什么,所以没有办法对自定义类型进行初始化。

new/delete 和 malloc/free的区别

在申请自定义类型对象时,malloc和free只会开辟空间和释放空间,不会去调用构造函数和析构函数,而new会先开辟空间,然后再去调用默认构造,完成对对象的初始化,delete在释放空间前,会去调用该自定义类型的构造函数,然后再释放空间。

定位new

我们要想证明上面我们所说的结论是否正确就需要把这个定位new个搞懂。
定位new的作用就是对已有空间进行初始化,具体使用方法如下:

#include <iostream>
using namespace std;

class A
{
	friend ostream& operator<<(ostream& out, const A& a);
public:
	A(const int& val = 0)
	{
		_a = new int (val);
	}


	~A()
	{
		delete[] _a;
		_a = nullptr;
	}
private:
	int* _a;
};

ostream& operator<<(ostream& out,const A& a)
{
	out << *(a._a);
	return out;
}

int main()
{
	A* aa1 = new A[10];
	for (size_t i = 0; i < 10; ++i)
	{
		cout << aa1[i] << " ";
	}
	cout << endl;
	new(aa1)A[10]{ 1,2,3,4,5,6,7,8,9,10 };
	for (size_t i = 0; i < 10; ++i)
	{
		cout << aa1[i] << " ";
	}
	cout << endl;
	delete[] aa1;
	return 0;
}

第一次打印的结果我们可以发现都为0,这是因为new去调用A这个类的默认构造。
第二次打印就是我们使用定位new,重新初始化的结果。
定位new就是new + (重新初始化的对象) + 构造函数。可能上面这个例子后面的这个构造函数看不是那么明显。
请看下面这个例子。

#include <iostream>
using namespace std;

class A
{
	friend ostream& operator<<(ostream& out, const A& a);
public:
	A(const int& val = 0)
	{
		_a = new int(val);
	}


	~A()
	{
		delete[] _a;
		_a = nullptr;
	}
private:
	int* _a;
};

ostream& operator<<(ostream& out, const A& a)
{
	out << *(a._a);
	return out;
}

int main()
{
	A* aa1 = new A;
	cout << *aa1 << endl;

	new(aa1)A(10);
	cout << *aa1 << endl;
	delete aa1;
	return 0;
}

之前我们想验证上面的结论就存在一个问题,那就是我们没有办法显示的调用构造函数,但是现在,有了这个定位new我们就可以显示的调用构造函数了,析构函数是可以显示调用的。我们现在就可以来证明上的结论。

new 和 delete的底层

我们可以对new进行转到定义,我们会看到一个operator new的函数,先说明一下operator new不是new的运算符重载。
在这里插入图片描述
我们在对delete进行转到定义我们会看到一个operator delete的函数。
在这里插入图片描述
那这个operator new 和 operator delete,这个两个函数是干什么的呢?我们来看一下这来个函数的代码。
operator new的代码

/*
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 new就是对malloc进行的封装,operator new跟malloc的区别就是malloc申请空间失败会返回NULL,operator new则会抛异常。
new会先去调用这个函数,开辟空间,然后它会去调用构造函数,我们去调试一下就可以证明,它会去调用构造函数。
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_dbg(pUserData, pHead->nBlockUse);这是什么呢?
这就是free。

#define free(p) _free_dbg(p, _NORMAL_BLOCK)

operator delete函数就是对free的封装。
所以在执行到delete的时候,会去先调用析构函数,然后才会调用operator delete对空间进行释放。

模板

有了模板的出现,我们就可以少写很多冗余的代码,编译器可以通过模板来帮我们完成相应的工作。
比如我们要写一个交换的函数:

#include <iostream>
using namespace std;

void swap(int& x1, int& x2)
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}

void swap(double& x1, double& x2)
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1, d = 11.11;
	swap(a, b);
	swap(c, d);
	cout << "a = " << a << "," << "b = " << b << endl;
	cout << "c = " << c << "," << "d = " << d << endl;
	return 0;
}

利用之前的函数重载,我们可以写出多个同名函数,但是非常麻烦,每有一种类型的交换,我们就要去写swap函数,如果要是又一百种类型,我们就要写一百种,但是有了模板,我们就可以写一个函数模板,然后让编译器去干这个活。
使用函数模板的swap函数:

#include<iostream>
using namespace std;
template <class T>
void swap(T& x1, T& x2)
{
	T tmp = x1;
	x1 = x2;
	x2 = tmp;
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1, d = 11.11;
	swap(a, b);
	swap(c, d);
	cout << "a = " << a << "," << "b = " << b << endl;
	cout << "c = " << c << "," << "d = " << d << endl;
	return 0;
}

如果像上面这种写法,编译器报错
在这里插入图片描述
这是因为在C++的标准库里面已经有了swap函数的模板,那你可以会问,那为什么上面的那两个swap没有出现这种问题,因为编译器会调用最匹配的函数,所以没有报错。但是我们写的这个函数模板,并不是真正存在的函数,这个函数模板就像是模具,如果又有一模一样的模具,使用哪个都可以,我们可以随便用一个就可以了,但是编译器无法决定这个件事。
所以我可以将swap函数模板用命名空间封起来,指定调用就不会出问题了。

#include<iostream>
using namespace std;
namespace lzq
{
	template <class T>
	void swap(T& x1, T& x2)
	{
		T tmp = x1;
		x1 = x2;
		x2 = tmp;
	}
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1, d = 11.11;
	lzq::swap(a, b);
	lzq::swap(c, d);
	cout << "a = " << a << "," << "b = " << b << endl;
	cout << "c = " << c << "," << "d = " << d << endl;
	return 0;
}

模板的使用

关键字:template
template后面跟参数类型,后面跟的类型可以是单类型或者是多类型,用尖括号括起来。
里面的参数前面要加上class和typename,来说明这个参数是类型名,而非变量名。
除了函数模板以外还有类模板。
单类型的函数模板和模板类:

template<class 模板参数名>
函数返回类型 函数名(函数参数)
{
}
template<typename 模板参数名>
class 类名
{
}

多类型的函数模板和模板类

template<typename 模板参数名, typename 模板参数名, typename 模板参数名, .....>
函数返回类型 函数名(函数参数)
{
}
template<class 模板参数名, class 模板参数名, class 模板参数名, .....>
class 类名
{
}

下面的代码是实现的一个简单的栈,只能插入数据。

#include <iostream>
using namespace std;
#include <string>

template <class T>
class Stack
{
public:
	Stack(const T& val = T())
		:_a(nullptr)
		,_size(0)
		,_capacity(0)
	{}


	void push(const T& val)
	{
		if (_size == _capacity)
		{
			size_t sz = _size;
			size_t cp = _capacity == 0 ? 4 : 2 * _capacity;
			T* tmp = new T[cp];
			for (int i = 0; i < _size; ++i)
			{
				tmp[i] = _a[i];
			}
			_a = tmp;
			_capacity = cp;
		}
		_a[_size] = val;
		++_size;
	}

	~Stack()
	{
		delete[] _a;
	}
private:
	T* _a;
	size_t _size;
	size_t _capacity;
};

int main()
{
	Stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	st1.push(4);
	st1.push(5);
	Stack<double> st2;
	st2.push(1.1);
	st2.push(2.1);
	st2.push(3.1);
	st2.push(4.1);

	Stack<string> st3;
	st3.push("hello world");
	st3.push("hello C++");
	st3.push("hello C");

	return 0;
}

在这里插入图片描述
有了模板我们就不需要对int类型单独写一个类,对double类型单独写一个类,对string类型单独写一个类,编译器会按照我们提供的类模板来实例化对应的模板类。

#include<iostream>
using namespace std;
namespace lzq
{
	template <class T>
	void swap(T& x1, T& x2)
	{
		T tmp = x1;
		x1 = x2;
		x2 = tmp;
	}
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1, d = 11.11;
lzq::swap(a, b);
lzq::swap(c, d);
	cout << "a = " << a << "," << "b = " << b << endl;
	cout << "c = " << c << "," << "d = " << d << endl;
	return 0;
}

像上面这个代码在使用函数模板的时候,有三种调用方法,第一种方法就是上面代码的调用方法。

	lzq::swap(a, b);
	lzq::swap(c, d);

第二种方法是,如果我们要把a和c进行交换,类型不同,编译器是无法生成对应得模板函数得,这样得话有两种解决方法,把函数模板的参数改成两个。

namespace lzq
{
	template <class T1, class T2>
	void swap(T1& x1, T2& x2)
	{
		T1 tmp = x1;
		x1 = x2;
		x2 = tmp;
	}
}

要不就进行强制类型转换。

	lzq::swap(a, (int)c);
	lzq::swap((double)b, d)

但是这么写是错的,因为强转会生成临时对象,而临时对象具有常性所以这里设计到权限的放大问题。我们这里在T1前面加上const就可以了,但是无法交换了。我们可以看一下个例子,来确定这个方案是可行的。

#include<iostream>
using namespace std;
template <class T>
T ADD(const T& x1,const T& x2)
{
	return x1 + x2;
}

int main()
{
	cout << ADD((int)1.1, 22) << endl; 
	cout << ADD(1.1, (double)22) << endl;
	return 0;
}

第三种方法显示实例化,就是在函数名后面加上尖括号,里面添加你想用的类型。

	cout << ADD<int>(1.1, 22) << endl; 
	cout << ADD<double>(1.1, 22) << endl;

上面写的swap函数依然没有办法使用这种方法,因为涉及权力的放大。
我们在实例化类对象的时候就要用到第三种方法,就拿那个栈为例。

Stack<int> st1;
Stack<double> st2;
Stack<string> st3;

最后再来看一下typename和class的区别:
这个两个关键字的效果基本上是一样的。只有在我们使用模板类类型的时候,但是模板类并没有实例化出来,这个是时候我们就要typename来表明这个是一个类型,不是变量。这样编译器就可以确定这是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗!伯!特!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值