文章目录
一、内存管理
1. C和C++的内存管理
我们以前学C语言的时候是怎样在堆上开辟内存的呢?
我们来回顾一下:malloc 、 calloc 、 realloc,并且和C++中的动态内存管理进行对比。
对于内置类型来说,我们可以有以下使用方式:
int main()
{
//malloc/free 和 new/delete 对应内置类型本质没有区别,只是用法上的区别
//C语言开辟空间的用法
int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)calloc(10, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 2);
//C++开辟空间的用法
int* p4 = new int;
int* p5 = new int(3);//初始化一个int类型的变量
int* p6 = new int[6];//开辟6个int类型的数组
int* p7 = new int[6]{
3,5 }; //初始化数组
//C++98不支持初始化new的数组,C++11才支持用{}列表初始化
free(p1);
free(p3);
delete p4;
delete p5;
delete[] p6;
delete[] p7;
return 0;
}
但是如果有什么好处呢?C和C++开辟内存对于内置类型本质没有区别,只是用法上的区别;但是我们来看一下自定义类型的区别。
对于自定义类型来说,可以有以下使用方式:
class AA
{
public:
//构造函数
AA()
{
cout << "调用一次构造函数" << endl;
}
~AA()
{
cout << "调用一次析构函数" << endl;
}
};
int main()
{
//C语言开辟内存空间
AA* p1 = (AA*)malloc(sizeof(AA) * 5);
//C++开辟内存空间
AA* p2 = new AA[5];
free(p1);
delete[] p2;
return 0;
}
我们来看一看程序的运行结果:
这才真正体现了C++中开辟内存的功能:
new 在堆上申请对象空间 + 调用构造函数初始化对象
delete 先调用自定义类型析构函数 + 释放空间
但是这只是其中的一个功能,还有以下的特性:
int main()
{
// 面向过程的语言,处理错误的方式是:返回值+错误码解决
char* p1 = (char*)malloc(1024u*1024u*1024u*2u);
if (p1 == nullptr)
{
printf("%d\n", errno);
perror("malloc fail");
exit(-1);
}
return 0;
}
int main()
{
// 面向对象的语言,处理错误的方式一般是抛异常,
//C++中也要求出错抛异常 -- try catch
try
{
char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
}
C++提出new和delete,主要解决两个问题:
1、自定义类型对象自动申请的时候,初始化和清理的问题,new/delete会调用构造函数和析构函数。
2、new失败了以后要求抛异常,这样才符合面向对象语言的出错机制。
小细节:delete和free一般不会失败,如果失败了,都是释放空间上存在越界或者释放指针位置不对。
2. operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new 实际上也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异
常。operator delete 最终要是通过free来释放空间。
3. new和delete的实现原理
对于内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,
不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,而malloc会返回NULL。
对于自定义类型:
① new的原理:
1.调用operator new函数申请空间。
2. 在申请的空间上执行构造函数,完成对象的构造。
② new T[N]的原理:
1.调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
2. 在申请的空间上执行N次构造函数。
③ delete的原理:
1.在空间上执行析构函数,完成对象中资源的清理工作。
2. 调用operator delete函数释放对象的空间。
④ delete[]的原理
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
4. 定位new表达式
我们在知道在C++中构造函数一般不用我们自己去调用的,编译器会帮我们去调用;但是我就想自己调用怎么搞呢?我们来学定位new表达式就知道了,定义如下:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type 或者 new (place_address) type(initializer-list);
place_address必须是一个指针,initializer-list是类型的初始化列表;
举例如下:
class A
{
public:
A(int data = 0)
{
cout << "调用构造函数" << endl;
}
~A()
{
cout << "调用析构函数" << endl;
}
private:
int _data;
};
int main()
{
//不用手动调用构造函数
A* p1 = new A(5);
delete p1;
//定位new的表达式如下:要手动调用构造函数
A* p2 = (A*)operator new(sizeof(A)); //开辟空间,但不初始化
new(p2)A(3); // 调用构造函数
p2->~A(); //调用析构函数
operator delete (p2); //释放空间
return 0;
}
5. 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在释放空间前会调用析构函数完成空间中资源的清理。
二、模板的简单认识
1. 函数模板
在以前学习的时候我们肯定写过交换函数swap(),但是不同类型的交换就要写一个函数,比如要写一个int 类型、char类型、double类型…函数重载虽然可以实现,但是有一下几个不好的地方:
1、重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。
2.、代码的可维护性比较低,一个出错可能所有的重载均出错
那这样是不是很麻烦呢?
于是我们来看一看函数模板是怎么解决这个麻烦的吧。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式如下:
template<class T1,class T2,…,class Tn>
返回值类型 函数名(参数列表){}
举例如下:
template<class T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
函数模板的原理:
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器去做。
函数模板的实例化:
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1、隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 3, b = 5;
double c = 3.86, d = 6.32;
Add(a, b); //隐式实例化
Add(c, d); //隐式实例化
return 0;
}
2、显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 3, b = 5;
double c = 3.86, d = 6.32;
Add(a, b);
Add(c, d);
//下面语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
//通过实参a将T推演为int,通过实参d将T推演为double类型,
//但模板参数列表中只有一个T,
//编译器无法确定此处到底该将T确定为int 或者 double类型而报错
//Add(a, d);
//显示实例化就可以编译通过
Add<int>(a, d);
Add<int>(b, c);
return 0;
}
模板参数的匹配原则:
① 非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。如下面所示:
template <class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int Add(const int& x, const int& y)
{
return x + y;
}
int main()
{
int a = 5, b = 8;
Add(a, b); //不会调用模板函数
Add<int>(a, b); //调用编译器特化的Add版本模板函数
return 0;
}
② 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
③ 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
2. 类模板
在我们以前学数据结构的时候,比如实现一个栈,里面的存放的类型是确定的了,如果还要再存放别的类型就要重新写一份栈,这就导致代码的冗余;而在C++中有了类模板可以很好的解决这个问题。
类模板的定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
举例如下:
template<class T>
//Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具
class Stack //实现一个栈类
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new T[capacity];
}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
void Push(const T& x); //声明在类里面
private:
T* _a;
int _top;
int _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template<class T>
void Stack<T>::Push(const T& x)
{
// ...
}
类模板的实例化:
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
举例如下:
// Stack是类名,Stack<int>才是类型
Stack<int> s1;
Stack<double> s2;