内存管理:
在C语言中,动态开辟空间可以用malloc,calloc,realloc这三个函数,下面先来复习一下这三者的区别
malloc和calloc都是用来开辟新空间,calloc在malloc的基础上还会初始化该空间为0,用法也不同
malloc(sizeof(int)*n) calloc(sizeof(int),n)
realloc时用来给已经动态开辟好的空间扩容的具体用法示例如下
int* newp = (int*)realloc(p,sizeof(int)*n)
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
void Test()
{// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[3];delete ptr4;delete ptr5;delete[] ptr6;
}
可以发现new和我们之前用的函数都有点不一样,这是因为它是操作符 ,所以导致了它独特的使用方法。
要特别注意new int(10)不是申请10个int空间,而是申请一个int空间并初始化为10,new int[10]才是申请10个int空间,但申请数组就不能初始化了
那new和malloc有什么区别呢?
在回答这个问题之前,我们先来看另外一组操作符
operator new和operator delete
尽管他们有operator,但这和类的那些重载函数不一样,,operator new 和operator delete是系统提供的全局函数,用法和malloc相似
void Test()
{// 动态申请一个int类型的空间int* ptr4 = (int*)operator new(sizeof(int));// 动态申请10个int类型的空间int* ptr6 = (int*)operator new(sizeof(int)*3);operator delete(ptr4);operator delete[](ptr6);
}
下面是operator new的底层实现
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid *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 delete 和free没区别,因为释放空间失败直接终止进程,operator delete只是为了和operator new成对出现
除此之外,operator new和operator delete也是可以重载的,如果你想在开辟空间的基础上做别的事情,就可以重载operator new/delete
new,operator new,malloc
上面将了operator new和malloc的区别,是在调用malloc的基础上可以抛异常。
而new和operator new的区别则是在operator new的基础上会调用自定义类型的构造函数,这是因为new 的原理就是
1.调用operator new函数申请空间
2.在申请的空间上执行构造函数
delete也相似
1.在空间上执行析构函数
2. 调用operator delete函数释放空间
那既然new是在operator new的基础上调用构造函数,有没有什么办法可以直接在operator new 的基础上自己去调用构造函数呢?
有的兄弟,有的,这个时候就需要请出定位new了
当我们已经通过malloc或operator new申请了空间后,如果想要调用该类的构造函数
Date *p = (Date*)operator new(sizeof(Date));
new(p) Date;
new后面的括号里面是指针名称,后面跟类的名称
正常来说是不能显式调用构造函数的,构造函数都是在实例化类时隐式调用的,而定位new就算是一种显式调用
总结:
new和malloc的区别
- new会调用构造函数,失败抛异常,malloc失败了返回0
- malloc是一个函数,new是一个操作符
- malloc用法:参数传字节数,返回值是void*;new后面跟申请对象的类型,返回值是类型的指针
malloc,operator new,new的关系
- malloc
- operator new -> malloc + 失败抛异常
- new -> operator new + 构造函数
读者有没有想过一个问题,为什么会有free,delete这样的函数,有什么意义呢?
大家都知道free和delete都是用来释放之前在堆中动态开辟的空间的,那如果不释放有什么危害?
答案是会造成内存泄漏,虽说对小程序没有什么危害,但对长期运行的程序,出现内存泄露危害很大,或者设备内存本身很小,也有危害,要防止可以用工具检测或智能指针
模板:
大家都知道,重载可以让同一个函数名存在多个,根据参数的不同来选择对应的函数,有些时候函数内部除了类型不同没有任何的区别,这时用重载就会非常麻烦,就会用到模板(泛型编程)
模板分为函数模板和类模板
先来看下面一个Swap的函数模板
template<class T>//<typename T>也可以
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
这是一个可以用于任何类型的Swap函数模板,其实就是在函数的上面一行写上了template<class T>
之后不管调用Swap时是什么类型,都可以执行,但它执行的是这个函数模板吗?
当然不是,函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
也就是说模板本身并不会去执行,是在预处理阶段生成对应的函数/类,真正去执行的是生成的函数/类
可以看到,三次调用的Swap的地址是不一样的,所以是生成了三个不同参数的Swap,再分别调用
看上面这个代码,可以看到模板的最后一行没有加分号,但这并不影响编译通过,这也证明了模板并不会参与编译,只要没有用到这个函数,预处理阶段就不会生成相应的函数。
显式实例化和隐式实例化
模板的实例化分为显式和隐式,如Swap(a,b)就是隐式, 由编译器自己去判断应该调用什么类型的函数,也可以Swap<int>(a,b),这样就是显式实例化,指定编译器必须要用int类型的函数。
如果a是int型,b是double型,Swap(a,b)也会报错,因为不知道该生成那种函数,这时有两种解决方法
- Swap(a,(int)b),这样让b也变成int类型,编译器就知道该实例化生成一个int类型的Swap函数
- Swap<int>(a,b),直接显式实例化也可以让编译器知道该实例化生成一个int类型的Swap函数
而类模板在实例化时必须显式实例化,隐式实例化会报错
并且函数模板和同名的函数可以共存
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
template <class T>
T Add(T left, T right)
{return left + right;
}
void Test()
{Add(1, 2); // 与非模板函数匹配,编译器不需要特化Add<int>(1, 2); // 调用编译器特化的Add版本
}
有人可能会问,第一个Add会调用函数模板生成的函数还是另一个同名函数呢?
答案是同名函数。对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板