前言:
本章将详细讲解C++内存管理和模板的实现。
第一部分我们讲解C++内存管理,C语言中有malloc/calloc/realloc等开辟空间和free释放空间,那么C++将符合实现呢?
第二部分我们会一起来初步认识模板与泛型编程,并详细探讨函数模板和类模板,为后续的学习做准备。
目录
(一)C++内存管理
(1)引入
(2)C++内存管理方式
2.1、new和delete操作内置类型
2.2、new和delete操作自定义类型
(3)operator new与operator delete函数
(4)总结
(5)定位new(了解)
(二)模板初阶
(1)泛型编程
(2)函数模板
2.1、隐式实例化
2.2、显式实例化
3.2、模板函数匹配原则
(3)类模板
(一)C++内存管理
(1)引入
有了前面C语言的学习,我们对于栈,堆,静态区等存放的数据也会有初步了解。下面我们通过一个题目来回忆一下栈,堆,静态区等存放的是哪些数据:
问题:
分析:
下面是按顺序作答:
- 1、globalvar是全局变量,所以在静态区;
- 2、staticGobalVar是全局的静态变量,所以在静态区;
- 3、staticVar是静态变量,所以在静态区;
- 4、localVar是int类型的变量,所以在栈;
- 5、num1是数组名,也就是数组首元素的地址,也就是int*类型的变量,所以在栈;
- 6、char2是数组名,同上,是局部变量所以在栈;
- 7、char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上;
- 8、 pChar3局部变量在栈区 *pChar3得到的是字符串常量字符在代码段;
- 9、ptr1局部变量在栈区 *ptr1得到的是动态申请空间的数据在堆区
- 图解:
答案:
说明:
- 1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
- 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
- 3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 4. 数据段--存储全局数据和静态数据。
- 5. 代码段--可执行的代码/只读常量。
(2)C++内存管理方式
C语言内存管理方式在有些地方并不适用于C++,所以C++又提出了自己的内存管理方式:
2.1、new和delete操作内置类型
new和delete的用法类似于malloc和free,我们来一起探究如何使用吧:
分析:
这里要注意的是:
2.2、new和delete操作自定义类型
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
//A* p2 = new A(1);
free(p1);
//delete p2;
}
这里我们发现并没有打印任何结果,下面我们把注释的代码解开,则会发现输出以下结果:
这就说明了new和delete分别调用了构造和析构函数。
我们再看下一段代码:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
return 0;
}
这里也没有打印任何结果,但是上面不是才分析出new和delete会调用构造和析构函数吗?
这里是因为new int,其中int是内置类型,所以没有调用A的构造函数,这样一来,自然也不调用其析构函数。当是内置类型时,new和malloc,delete和free的作用几乎等同。
我们最后再看一段代码:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p6 = new A[10];
delete[] p6;
return 0;
}
这里我们发现输出结果如下:
我们可以得出结论:
new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
还会调用构造函数和析构函数。
(3)operator new与operator delete函数
- new=operator new(malloc)+调用构造函数;
- delete=operator delete(free)+调用析构函数;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = new int[4];
top = 0;
capacity = 4;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[]_a;
top = 0;
capacity = 0;
}
private:
int* _a;
int top;
int capacity;
};
int main()
{
//失败了抛异常
int* p1 = (int*)operator new(sizeof(int*));
// 失败返回nullptr
int* p2 = (int*)malloc(sizeof(int*));
if (p2 == nullptr)
{
perror("malloc fail");
}
//申请空间 operator new -> 封装malloc
//调用构造函数
A* p5 = new A;
// 先调用析构函数
// 再operator delete p5指向的空间
// operator delete -> free
delete p5;
// 申请空间 operator new[] ->perator new-> 封装malloc
// 调用10次构造函数
A* p6 = new A[10];
// 先调用10次析构函数
// 再operator delete[] p6指向的空间
delete[] p6;
int* p7 = new int[10];
free(p7); // 正常释放
A* p8 = new A;
//free(p8); // 少调用的析构函数
delete p8;
//对于无空间开辟的少调用析构函数可以正常运行
//Stack st;
Stack* pts = new Stack;
//free(pts);//这样就少调用了析构函数,会造成内存泄漏
delete pts;
return 0;
}
大家可以一一调试试验,验证上面的结论。
(4)总结
对于内置类型:
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来释放空间
(5)定位new(了解)
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
后面我们在做内存池项目时候会使用,这里大家了解即可。
(二)模板初阶
(1)泛型编程
我们以前在一个项目里写交换函数可能会利用函数重载实现不同类型的函数,如下:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
但是我们发现,他们的基本思想和实现是差不多的,甚至除了变量的类型,其他都一样。那么我们可不可以使用一个“模具”来代替呢???
答案是可以的!我们会利用泛型编程来实现。
那么什么是泛型编程呢?
// 泛型编程 -- 模板
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
(2)函数模板
函数模板格式:
样例参照交换函数的模板实现。
2.1、隐式实例化
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
Add(a1,d2);
}
其中Add(a1,d2)无法通过编译。因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅。
我们可以采用两种处理方式:
- 1. 用户自己来强制转化 ,如Add(a, (int)d);
- 2. 使用显式实例化
2.2、显式实例化
我们上面的两种处理方式中提到了显式实例化,那么如何实现呢?
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
ps:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
3.2、模板函数匹配原则
- 1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板数;
- 2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数;
- 3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
(3)类模板
定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
注意:
1、上述Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具;
2、类模板中函数放在类外进行定义时,需要加模板参数列表;
——————————————————————————————————————
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
感谢阅读,祝您学业有成!!