C++
中的内存管理机制和C语言
是一样的,但在具体内存管理函数上,C语言
的malloc
已经无法满足C++
面向对象销毁的需求,于是祖师爷在C++
中新增了一系列内存管理函数,即 new
和 delete
著名段子:如果你还没没有对象,那就尝试 new
一个吧。
一、内存分布
在内存中存在五大分区,各个分区都各司其职,比如我们耳熟能详的栈区,堆区,静态区。
二、C语言的三种动态内存管理的函数
malloc:申请指定大小的空间
int* pi = (int*)malloc(sizeof(int) * 1); //申请一个整型 double* pd = (double*)malloc(sizeof(double) * 2); //申请两个浮点型 char* pc = (char*)malloc(sizeof(char) * 3); //申请三个字符型
注意:malloc申请的空间都是未初始化的,即被空间置为随机值。
calloc:将初始化的空间初始化为0
int* pi = (int*)calloc(1, sizeof(int)); //申请一个整型 double* pd = (double*)calloc(2, sizeof(double)); //申请两个个浮点型 char* pc = (char*)calloc(3, sizeof(char)); //申请三个字符型
注意:
calloc
参数列表与malloc
不同,同时calloc
申请的空间会被初始化为0
realloc
:对已申请的空间进行扩容
int* tmp = (int*)realloc(pi, sizeof(int) * 10); //将 pi 扩容为十个整型 pi = tmp; //常规使用方法
注意: 我们要对所有的申请函数进行空指针检查,预防野指针问题
堆区
的空间由我们管理,编译器很信任我们,因此我们要做到有借有还,再借不难
凡是动态开辟的空间,用完后都需要释放
free(tmp); //此时tmp指向pi扩容后的空间,释放tmp就行了 tmp = pi = NULL; //两者都需要置空 free(pd); pd = NULL; free(pc); //只要是动态开辟的,都需要通过 free 释放 pc = NULL;
三、C++的动态管理
1、new开辟空间
void Test_CPP() { // 动态申请一个int类型的空间 int* p1 = new int; // 动态申请一个int类型的空间并初始化为10 int* p2 = new int(10); // 动态申请10个int类型的空间 int* p3 = new int[10]; }
C++允许大括号进行初始化
int* p1 = new int[5]{1,2} // 1 2 0 0 0 int* p2 = new int[5]{1,2,3,4,5}; // 1 2 3 4 5
2、delete释放空间
void Test_CPP() { int* p1 = new int; int* p2 = new int(10); int* p3 = new int[10]; // 多个对象 // 单个对象,delete即可。 delete p1; delete p2; // 多个对象,delete[] 。 delete[] p3; p1 = nullptr; p2 = nullptr; p3 = nullptr; }
3、new与malloc的区别与free与delete的区别
在申请自定义类型的空间时,new 会调用构造函数,
delete 会调用析构函数,而 malloc 与 free 不会。
new:在堆上申请空间 + 调用构造函数输出。
delete:先调用指针类型的析构函数 + 释放空间给堆上。
四、函数模版
函数模板代表了一个函数家族,该函数模板与类型无关,
在使用时被参数化,根据实参类型产生函数的特定类型版本。
1、函数模版格式
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){}
① template 是定义模板的关键字,后面跟的是尖括号 < >
② typename 是用来定义模板参数的关键字
③ T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
2、函数模版使用方法
来看一下这一段代码,假设要实现Add函数处理各个类型的加法。
//处理整型的加法函数 int Add(const int& a, const int& b) { return a + b; } //处理浮点型的加法函数 double Add(const double& a, const double& b) { return a + b; }
这时候用一个模版既可以把他们全部搞定
template <class T> //还可以写成template <typename T> T Add(const T& a, const T& b) { return a + b; }
还可以实现多参数模版:
//定义一个多参数模版,求数的平均值 template <class T1 ,class T2> T2 findaver( T1 arr[], int n) { T1 sum = 0; T2 aver; for (int i = 0; i < n;i++) { sum += arr[i]; } aver = sum / (double)n; return aver; }
总之,在函数模板
的存在下,我们不再需要再编写不同类型参数的相似函数了
3、实现原理
只需要两样东西:编译器与函数重载
当我们编写好函数模板
后,编译器
会记住这个模板
的内容,当我们使用模板
时,编译器
又会根据参数类型,创建相应的、具体的函数供参数使用,而这就是函数重载
的道理
编译器在识别参数类型生成函数时,有两种途径:
-
自动识别 (
隐式
) -
我们手动指定(
显式
)
3.1隐式实例化
隐式实例化
就是编译器自动识别参数后生成函数的过程
//Add 模板 template <class T> T Add(const T& a, const T& b) { return a + b; } int main() { Add(2, 1.5); //此时编译失败! return 0; }
解决方法一:强制类型转化
int main(){ Add((double)2, 1.5); return 0; }
3.2显式实例化
显式实例化
就是给编译器打招呼,让它在建房子时按照我们的意愿来
Add<int> (2, 3.14); //此时编译器会调用 _3Addii 函数,至于传参时的类型转换,由编译器完成 Add<char> (2, 5); //调用 _3Addcc 函数
这种行为是完全合法的,< >
符号也正式和我们见面了,在后面的 STL
学习中,< >
会经常使用到,比如生成一个类型为 int
的顺序表,直接 vector<int>
,生成 char
类型的顺序表 vector<char>
,一键生成,非常方便,当然还有很多容器都会用到显式实例化
五、类模版
模板
除了可以用在函数
上面外,还可以用在类
上,此时称为 类模板
STL
库中的容器,都是 类模板
的形式,我们使用时,需要什么类型的 类
,直接显式实例化为对应 模板类
即可
//简单演示下 STL 中的容器,这些都是类模板的实际运用 vector<int> v1; //实例化为整型顺序表类 list<double> l1; //实例化为浮点型链表类
类模版与函数模版不同,类模版只能显式实例化。
简单写一个类模版:
//类模版,简单写一个栈模版 //简单写一个栈模板 template<class T> class Stack { public: //构造函数 Stack(int capacity = 4); //析构函数 ~Stack(); //…… private: T* _pData; int _top; int _capacity; }; //注意类模板中方法的实现方式! //定义构造函数 template<class T> Stack<T>::Stack(int capacity) { _pData = new T[capacity]; //内存管理,一次申请4块空间 _capacity = capacity; _top = 0; } //定义析构函数 template<class T> Stack<T>::~Stack() { delete[] _pData; //注意:匹配使用 _capacity = _top = 0; } int main(){ Stack<int> s1; Stack<char> c1; return 0; }
类模板
使用时需要注意一些问题:
-
模板类
中的函数在定义时,如果没有在类域中,就需要通过类模板
+类域访问
的方式定义 -
类模板
不支持声明与定义分开在两个文件中实现,因为会出现链接错误