一、内存管理
1.1内存区域的划分
1.1.1内存划分区域图示
1.1.1补:堆和栈都可以进行动态分配和静态分配吗?
不是的,堆无法进行静态分配,只能动态分配;栈可以利用_alloca动态分配,但是分配的空间不能用free或者malloc来释放。
1.1.2几个常见代码在内存区中对应的位置
①
char char2[] = "abcd" ;
此时char2在 栈区
*char2在 栈区
原因:首先char2为一个数组名,他创建出来就在栈区;字符串虽然是常量字符,但是char2对应的是一个数组,所以其实这一常量字符串被复制到了栈中的数组中,因此*char2得到数组首元素地址,在栈区。
②
const char* pchar3 = "abcd";
此时pchar3在 栈区
*pchar3在 代码段(常量区)
const修饰的是指向内容,影响到的是*pchar3
原因:pChar3是一个在栈中创建的指针变量,而“abcd”是位于常量区的一个常量字符串,他们之间的关系是pChar3指向常量区中的一个地址,因此*pChar3在常量区
③
int* ptr1=(int*)malloc(sizeof(int)*4);
此时ptr1在 栈区
*ptr1在 堆区
原因:栈区创建的变量,在堆区申请的空间
④
const int a = 0;
a在 栈区,但是a具有只读属性。
1.1.2补:
32位系统下,最大的内存访问空间大小为4G,但是不可能把所有内存都给堆用,因此32位系统下堆最大申请4G内存空间的说法是错误的。
1.2C++中的内存申请
1.2.1C中原来的内存申请函数是否可以接着用?
是可以的,包括malloc,realloc,calloc都可以正常使用
1.2.2C++中新增的内存申请方式
使用new和delete进行内存申请/释放
①对于内置类型
使用举例:
//申请
int* p1=new int;
int* p2=new int[10];
//释放
delete p1;
delete[] p2;
默认不对内置类型进行初始化,但可以选择显式初始化
int* p1 = new int(10);
int* p2 = new int[10]{1,2,3,4};
②对于自定义类型
其实c++设计new,更多就是为了自定义类型
假如我们定义了一个class A:
A* a1 = (A*)malloc(sizeof(A));//错误,malloc只会进行内存的申请,不会调用构造,无法初始化
A* a2 = new A;//调用默认构造
A* a3 = new A(2);//调用传入参数2的构造函数
1.2.2补:自定义类型数组快速申请
new很强大,如果我们想快速创建自定义类型的数组,只需要
A* p4 = new A[10];
然后用
delete[] A;
来释放
1.2.3支持隐式类型转换快速参与new的构造
A* p4 = new A[10]{1,2,3,4}//包含隐式类型转换->整形转自定义类型
//当然,构造函数多参数也是支持的
A* p4 = new A[10]{{1,2},{3,4},5,6};
1.2.3补:
若是在创建时创建的是数组,但是释放使用的是delete而不是delete[]的时候
内置类型与未显示析构的自定义类型:可以正常释放,不会出现内存泄漏
经过了显示析构的自定义类型:会导致运行时错误,因为delete只会被调用一次,所以会出现内存泄漏
1.2.4C中有realloc进行扩容,那么C++该如何扩容呢?
一则可以直接使用realloc函数,二则可以选择手动扩容,即自己申请一个空间,再把原空间的内容拷贝过去
重⭐1.2.5malloc/free和new/delete的区别
1.2.5.1用法上:
①malloc/free是函数,new/delete是操作符
②malloc申请不会调用构造函数,但是new会,借此完成初始化;同理free也不会调用析构函数,但是delete会调用
③malloc申请空间需要传空间的大小,但是new不需要,new只要后面跟类型就可以,如果需要创建多个对象只需要在后面加上[]来指定个数即可
④malloc的返回值是void*类型,使用的时候需要强制类型转换,但是new不需要,因为new后面跟的就是类型
⑤malloc使用的时候需要进行判空,但是new不用,只是需要进行异常的捕获
1.2.5.2原理上:
申请自定义类型的时候,malloc/free不会自动调用构造函数和析构函数,只会开辟空间;但是new/delete会在空间开启完成以后调用这两个函数完成初始化/销毁
1.3operator new和operator delete函数
1.3.1他们的本质是什么?
①本质上底层为malloc进行实现
new实际上就是 operator new+构造 来实现
②本质上底层为free进行实现
实际为 operator delete+析构 来实现
1.3.2平时C中我们在写malloc的时候常常会有检查的步骤来确认malloc是否成功,C++中呢?为什么是 operator new+构造 而不是 malloc+构造 呢?
这两个问题的答案是一致的,
因为在C++中,申请空间失败会“抛异常”(跳到catch中)
1.3.3对于A* p4=new A[4]这种多个对象空间申请如何处理?
底层调用operator new[]函数,而operator new[]本质上调用的还是operator new函数,也就是仍旧为malloc的空间
1.3补:二者的调用
operator new和operator delete函数可以显示调用,他们的返回值是void*,传入字节数(类似malloc)
1.4对C++中新申请方式的小结
malloc和free,new和delete,new[]和delete[]互相搭配使用,不要混用
原因举例:例如当我们使用new T[]进行空间申请的时候(T为代号,可以为内置类型或者自定义类型)
在函数调用层面:
①内置类型或不显示析构的自定义类型,在向operator new[]函数传参的时候传正常size的总字节数
②进行了显示析构的自定义类型会加上前面4个字节的大小,而这四个字节是为了确定要调用多少次析构,此时传size+4的字节数,汇编层面
在②这种情况下与free/delete混用会导致程序崩溃
1.5定位new表达式(placement——new)(这是一个规定)
1.5.1有何作用?
对已经申请好的空间,帮助显示调用构造函数
①使用举例1
例如想创建自定义类型A的指针并给空间(不用new)
A* p1 = (A*)operator(sizeof(A));
p1->A();//这是错误的,不支持这样显式调用构造
new(p1)A;//这是定位new表达式,作用就是调用构造
new(p1)A(10);//定位new表达式,构造函数传入参数10
②使用举例2-》用以参考
A* p2 = (A*)operator new[](sizeof(A) * 10);
for(int i=0;i<10;i++)
new(p2+i)A(i);
for(int i=0;i<10;i++)
(p2+i)->~A();
operator delete[](p2);
1.5.2何时需要用?
这涉及到内存池:
如果T为自定义类型,一则可以new T[n],默认直接找堆;如果要从内存池申请n个对象那要用到定位new表达式来构造。
二、模板初识
2.1泛型编程
试想,在C语言中我们要多次交换两数的值,而且这些数还有许多不同的类型,我们要怎么办?
是的,只能一个一个去写。
可是他们的逻辑十分相似,因此我们希望可以快捷一些:
写一个框架,针对广泛的类型,而这一思想就是泛型编程,这个框架就是函数模板。
2.2函数模板
2.2.1函数模板格式
template <typename T1,typename T2>
//或者template <class T1,class T2>
此处的T1和T2就是类型的泛型名字
使用举例:
template <typename T>
void Swap(typename T& left,typename T& right)
{
T temp=left;
left=right;
right=temp;
}
之后,主函数中
double a2=1.1,b2=1.3;
int a1=1,b1=3;
Swap(a2,b2);
Swap(a1,b1);
2.2.2同一模板下不同类型调用的是同一个函数吗?
我们若是逐步运行,会发现函数调用都会回到模板中,所以这说明调用的都是同一函数吗?
不,眼见在这里并不真实,他们类型的大小都相同,怎么会是同一个函数呢?
我们把目光放到汇编层次,可以发现找的是函数Swap<int>和Swap<double>这两个不同地址处的函数
总的来说,编译器会根据传参的类型推演出所需要的函数,自动实现他们,方便链接时配对。
2.2.2补:
举的例子里涉及到交换函数,平时我们很常用,所以在std库里其实已经有了一个对应的函数模板,可以直接
swap(a1,b1);
swap(a2,b2);
2.2.3模板的推演
假设我们有这样一个函数模板
template <class T>
T Add(T& a,T& b)
{
return a+b;
}
此时我传参
int a1=10;
double b1=10.2;
会发现程序报错了。
为什么?我们该如何解决这一问题呢?
原因是根据函数模板推演的时候存在歧义。
有三种解决方案
①可以强转赋值
Add((double)a1,b1);
Add(a1,(int)b1);
②显式模板实例化,不进行推演
Add<int>(a1,b1);
Add<double>(a1,b1);
③可以设置两个模板参数,利用auto来自动获取返回值类型
template <class T1,class T2>
auto Add(const T1& a,const T2& b)
{
return a+b;
}
使用时
Add(a1,b1);
即可
2.2.4必须使用显示模板实例化的情况
之前举的例子可以通过参数对模板T的类型进行推演,但如果无法推演呢?
例如
templat <class T>
T* func(int a)
{
//...
}
此时则必须要有显示模板实例化
int* ret = func<int>(1);
2.2.5模板与普通函数可以同时存在
例如模板
template <class T1,class T2>
auto Add(const T1& a,const T2& b)
{
return a+b;
}
与函数
int Add(int a,int b)
{
return a+b;
}
在选择进行调用的时候遵循“有现成的吃现成的”,即
先调用普通函数在调用模板,若没有普通函数,掉对应模板,如果没有对应模板,进行隐式类型转换也会完成调用
2.3类模板
2.3.1作用
假设我在C语言中实现了一个栈(Stack),此时我希望创建两个栈类型的变量st1和st2,一个存int,一个存double,我该怎么办?
我需要写两个仅DataType类型不同的结构体,那可否简化?
可用类模板
templat <class T>
class Stack
{
public:
//...完成栈的定义
}
此时T可以表示类型
2.3.1补
类模板和函数模板都是不提高效率,只简化代码,变量st1与st2的类型并不相同
2.3.2类模板的实例化
函数模板尚有推演的可能,但是类模板一点也没有,所以必须进行实例化
Stack<int> st1;
Stack<double> st2;
2.3.3类模板中的声明与定义分离
类模板的声明和定义分离相较于普通类要修改格式
template <class T>//每一个声明定义分离的函数之前都要加上这一行
void Stack<T>::Push(const T& data)//域访问限定符之前要写完整的类类型
{
//...;
}
且不可以分离文件进行定义(.cpp与.h也不可以),会报链接错误
2.4模板注意事项
①模板运行时不检查数据类型,也不保证类型安全,仅仅相当于类型的宏替换
②模板可以用来创建动态增长的数据结构(模板类型的大小可变)
③模板的实参:模板实例化的内容
④模板参数可以为类型,也可以为非类型,例如
template <class T,size_t N>
其中非类型其实相当于typedef