目录
一:内存管理
1.C/C++中的内存分布
2.C++中的内存管理(new与delete)
- 在学习C++中的内存管理方式前,我们先回忆一下,C语言中我们是怎么进行内存管理的?
- 申请内存有malloc,calloc,realloc,释放内存有free
- calloc相当于malloc+memset, realloc则是重新分配已有的空间(有原地扩容和异地扩容的区别)
void* malloc (size_t size);
void* realloc (void* ptr, size_t size);
void* calloc (size_t num, size_t size);
- 但是它们三者都无法避免返回时要进行强制类型转换和有可能分配内存失败,要进行判空的问题
- new是一个C++中的操作符,用于在C++中进行内存的分配
- delete也是C++中的操作符,用于在C++中进行内存的释放
- 通过下面的例子看看它们和malloc/free的区别
int main()
{
//动态申请一个int类型的空间
int* ptr1 = (int*)malloc(sizeof(int));
int* ptr2 = new int;
free(ptr1);
delete ptr2;
//动态申请十个int类型的空间
int* ptr3 = (int*)malloc(sizeof(int)*10);
int* ptr4 = new int[10];
free(ptr3);
delete[] ptr4;
//new独有:动态申请一个int类型的空间,并初始化成3
int* ptr5 = new int(3);
delete ptr5;
//C++11的new:动态申请十个int类型空间,并初始化成1-10
int* ptr6 = new int[10]{1,2,3,4,5,6,7,8,9,10};
delete[] ptr6;
return 0;
}
- 通过上面的例子,我们可以看出使用new时不需要计算空间的字节大小,直接在new后面跟上类型名称即可,即
new 类型名
- 如果想要申请复数空间,在类型名后加[]即可,即
new 类型名[size]
- new在申请空间的同时可以对其进行初始化,加上()即可,
new 类型名(初值)
,同时这一条也是new独有的,具体原因是new在申请空间时会调用对应类型的构造函数
- 同样的,delete释放空间时也会对应相应类型的析构函数
3.operator new与operator delete
operator new
和operator delete
是系统提供的全局函数- new在底层实现时通过调用operator new来申请空间,delete在底层实现时通过调用operator delete来释放空间
- 而operator new实现时是通过malloc申请空间,但失败了不是像malloc一样返回null,而是抛异常
- operator delete实现是通过free释放空间
- 总结一下:
- new = operator new + 构造函数 (operator new= malloc+失败抛异常)
- delete = operator delete + 析构函数
4.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.泛型编程
- 我们在使用C语言时,库函数中没有swap,每次我们需要使用时就要自己编写,十分的麻烦,而且不同类型还要编写不同的函数名,防止冲突
- 在C++中我们虽然可以通过函数重载避免同一个功能函数只有一个名字,即函数重载,像这样:
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
void swap(char& a, char& b)
{
char tmp = a;
a = b;
b = tmp;
}
- 但是还是需要自己编写多个函数,复用率低,有一个新类型出现,我们就要多写一个
- 程序猿都是很懒的,能不自己写就不自己写,能不能我们就写一个,其它的编译器自己帮我们写了呢?
- 答案是可以,这就是泛型编程,我们只需要给编译器一个模子,然后传入不同的参数,编译器会根据参数类型的不同,自己编写出对应的函数。
- 这个模子,我们称为模板,它分为函数模板和类模板
2.函数模板
(1)格式
- 有了函数模板,swap函数我们就只用写一个了,它的格式如下:
template<typename T1,typename T2…>
返回值类型 函数名(参数列表)
- 举个例子:
template<typename T>
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
- 其实就是将原本写类型的位置,换成模板参数T,关键字
typename
是用来定义模板参数的,也可以用class
代替,但是不能使用struct
(2)原理
- 它的原理也很简单,在编译阶段,编译器根据传入参数的类型自动推演生成对应类型的函数
(3)函数模板的实例化
- 函数模板的实例化有隐式和显示之分,上述所编写的swap就是隐式实例化,即让编译器自动推断
- 这种情况传入的参数类型必须一致,否则编译器不知道推断成哪一种
- 如图,a类型为int,b类型为double,编译器推断不出来了
- 除了上述的隐式实例化,我们还可以指定模板参数的类型让编译器实例化,也叫显式实例化
int main()
{
double a = 1;
double b = 2.0;
swap<double>(a, b);
return 0;
}
(4) 模板参数的匹配原则
- 一个非模板函数可以和一个同名的模板参数同时存在,而且该函数模板可以被实例化成此非模板参数
- 对于非模板函数和模板函数,如果其它条件均相同,调用时会优先调用非模板函数(换句话说编译器也想偷懒),如果模板能产生一个更匹配的函数,那么就将使用模板函数
3.类模板
(1)格式
typename<class T1,class T2…>
class 类模板名称
{
类成员的定义
}
template <class T>
class vector
{
typedef T* iterator;
public:
vector()
{
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
(2)实例化
- 与函数模板不同,类模板实例化时必须指定模板参数
- 注意,类模板不是类,是一个类家族的集合,模板类是类,是通过类模板实例化的具体类
- 比如vector是类模板,v1,v2才是(模板)类