【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来表明这个是一个类型,不是变量。这样编译器就可以确定这是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。