【ONE·C++||内存管理和模板初阶】-创新互联
学习笔记,慢慢补充。
创新互联主要从事网站设计、网站制作、网页设计、企业做网站、公司建网站等业务。立足成都服务盂县,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:18980820575文章目录- 总言
- 1、C/C++内存分布
- 1.1、内存区域划分简介
- 2、动态内存分布管理
- 2.1、C语言中动态内存管理:malloc、calloc、rellaooc、free
- 2.2、C++中动态内存管理 :new、delete
- 2.2.1、针对内置类型
- 2.2.2、针对自定义类型
- 2.2.3、new申请动态内存失败后如何处理
- 2.2、operator new与operator delete全局函数
- 2.2.1、关于new、delete功能的底层原理
- 2.2.2、重载operator new与operator delete
- 2.2.3、new、delete实现原理总结
- 2.3、定位new
- 2.3.1、是什么和为什么
- 3、模板初阶
- 3.1、泛型编程
- 3.2、函数模板
- 3.2.1、函数模板的概念和基本使用方式
- 3.2.2、函数模板底层原理
- 3.2.3、函数模板的实例化:隐式、显式等
- 3.2.4、模板参数的匹配原则
- 3.3、类模板
- 3.3.1、为什么和是什么
- 3.3.2、模板实例化说明
- 3.3.3、以Stack的实现演示模板使用
1、C/C++内存分布 1.1、内存区域划分简介
1)、问题引入:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{static int staticVar = 1;
int localVar = 1;
int num1[10] = {1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
选择题:
选项: A、栈 B、堆 C、数据段(静态区) D、代码段(常量区)
1、globalVar
在哪里?____ staticGlobalVar
在哪里?____
2、staticVar
在哪里?____ localVar
在哪里?____
3、num1
在哪里?____
4、char2
在哪里?____ *char2
在哪里?___
5、pChar3
在哪里?____ *pChar3
在哪里?____
6、ptr1
在哪里?____ *ptr1
在哪里?____
填空题:
1、填空题:
sizeof(num1)
= ____
sizeof(char2)
= ____ strlen(char2)
= ____
sizeof(pChar3)
= ____ strlen(pChar3)
= ____
sizeof(ptr1)
= ____
3、 sizeof 和 strlen 区别?
2)、划分区域示意图:
此处详细讲解请看C动态内存章。
1)、C++可以兼容C,为什么还需要提出自己的内存管理方式?
1、首先要明确的是,C++兼容C,故malloc、calloc、rellaooc、free这些在C++中照样可以用。
2、但有些地方,C中的动态内存管理方式就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
2.2.1、针对内置类型
1)、new、delete操作符针对内置类型进行动态内存管理的基本用法
1、首先要明确的是,new、delete是关键字。而C语言中那套动态内存管理依赖的是函数。
如何开辟空间?
void Test()
{// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
}
int main(void)
{Test();
return 0;
}
如何使用new开辟数组类型的空间?(即一次性同时生成多个类型相同的元素)
该问题C++98不支持,C++11才支持,书写方法如下:
void Test()
{//C++11:
int* ptr1 = new int[10]{1,2,3 };
}
int main(void)
{Test();
return 0;
}
如何释放空间?
void Test()
{// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
delete ptr4;
delete ptr5;
delete[] ptr6;//单个对象直接delete,若是多个对象则使用[ ]
//C++11:
int* ptr1 = new int[10]{1,2,3 };
delete[] ptr1;//单个对象直接delete,若是多个对象则使用[ ]
}
int main(void)
{Test();
return 0;
}
总结:针对内置类型,new、delete
跟malloc、free
没有本质的区别,只有用法的区别。new、delete
用法相对简化了。
1)、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()
{// 1、堆上申请空间
A* p1 = (A*)malloc(sizeof(A));
if (p1 == NULL)
{perror("malloc fail");
return 0;
}
// 1、释放空间
free(p1);
// 1、堆上申请空间 2、调用构造函数初始化
A* p2 = new A;
A* p2 = new A(10);
// 1、调用析构函数清理对象中资源 2、释放空间
delete p2;
cout<< endl<< endl;
A* p3 = new A[2];
delete[] p3;
//C++11支持
A* p3 = new A[2]{1,2 };
A* p3 = new A[2]{A(1), A(2) };
// 结论:new/delete 是为自定义类型准备的。
// 不仅在对申请出来,还会调用构造和析构初始化和清理
return 0;
}
malloc、free对自定义类型
1、对自定义类型使用malloc、free申请和释放空间:并不会对自定义类型初始化。
// 1、堆上申请空间
A* p1 = (A*)malloc(sizeof(A));
if (p1 == NULL)
{perror("malloc fail");
return 0;
}
// 1、释放空间
free(p1);
new、delete对自定义类型
1、使用new动态申请自定义类型,会调用默认构造函数初始化。
2、若类不提供默认构造,若要编译通过,则需要自己传参。
// 1、堆上申请空间 2、调用构造函数初始化
A* p2 = new A;//这种写法需要有默认构造
A* p2 = new A(10);//如果没有默认构造则需要我们手动传参
delete p2;
3、相比于free,delete会调用析构函数清理对象中的资源(但是否需要这是取决于析构函数本身)
A* p3 = new A[2];
delete[] p3;
4、若是没有默认构造,多个自定义类型需要传参的写法(一种是构造(隐式类型)、一种是拷贝构造:
//C++11支持
A* p3 = new A[2]{1,2 };
A* p3 = new A[2]{A(1), A(2) };
delete[] p3;
结论:new/delete 是为自定义类型准备的。不仅在对申请出来,还会调用构造和析构初始化和清理.
2)、delete、new构造、析构顺序探索
对于多个自定义类型,此处需要注意构造和析构的顺序,以及在堆上的地址排序。
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 = new A[10];
cout<< endl;
delete[] p1;
return 0;
}
int main()
{//C: 失败返回NULL
char* p1 = (char*)malloc(1024u * 1024u * 1024u * 2 - 1);//u表示无符号
//cout<< p1<< endl;//cout自动识别类型,会先去解引用字符指针,而非取地址。
printf("%p\n", p1);
//C++: new失败,不需要检查返回值,他失败是抛异常
try
{//申请空间失败的一种写法:
char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];//此处失败会直接跳转后续catch部分,不会指向下一行。
printf("%p\n", p2);
///申请空间失败的另一种方法:
size_t n = 0;
while (1)
{ char* p2 = new char[1024];
++n;
printf("%p->[%d]\n", p2, n);
}
}
catch (const exception& e)//捕获异常
{cout<< e.what()<< endl;
}
return 0;
}
1、new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是 系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。
2、而operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
1、 一般情况下不需要对 operator new 和 operator delete进行重载,除非在申请和释放空间时候有某些特殊的需求。比如:在使用new和delete申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏。
2、关于专属的operator new的实现:可用于频繁调用new需要提高效率的情况
// new ->operator new + 构造函数
// 默认情况下operator new使用全局库里面
// 每个类可以去实现自己专属operator new new这个类对象,就会调自己实现这个operator new
// 实现一个类专属的operator new -- 了解一下
struct ListNode
{int _val;
ListNode* _next;
// 内存池
static allocatoralloc;//静态成员变量
void* operator new(size_t n)//重载专属的operator new
{cout<< "operator new ->STL内存池allocator申请"<< endl;
void* obj = alloc.allocate(1);
return obj;
}
void operator delete(void* ptr)//重载专属的operator delete
{cout<< "operator delete ->STL内存池allocator申请"<< endl;
alloc.deallocate((ListNode*)ptr, 1);
}
struct ListNode(int val)
:_val(val)
, _next(nullptr)
{}
};
// allocator以后会讲,现在先会用即可
allocatorListNode::alloc;//类外定义
int main()
{// 频繁申请ListNode. 想提高效率 -- 申请ListNode时,不去malloc,而是自己定制内存池
ListNode* node1 = new ListNode(1);//new相对于malloc简化
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
delete node1;
delete node2;
delete node3;
return 0;
}
1)、场景引入:
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 = new A;
A* p2 = (A*)malloc(sizeof(A));
if (p2 == nullptr)
{perror("malloc fail");
}
return 0;
}
如上述代码:有时我们需要对已经定义的类进行初始化。而我们知道,类初始化需要调用构造函数,目前已知的两种构造函数调用方式是,①在定义类时会调用构造函数初始化成员;②通过new开辟自定义类型也会进过构造函数。
那么,针对上述p2这样已经申请好的类,成员变量私有情况下,如何为它初始化?
2)、是什么和怎么用:
概念:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type
或者new (place_address) type(initializer-list)
①place_address
必须是一个指针
②initializer-list
是类型的初始化列表
演示:
new(p2)A;
new(p2)A(10);
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 = new A;
A* p2 = (A*)malloc(sizeof(A));
if (p2 == nullptr)
{perror("malloc fail");
}
new(p2)A;//方法一
new(p2)A(10);//方法二
return 0;
}
3)、为什么用:
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
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;
}
int main()
{int i = 1, j = 2;
double x = 1.1, y = 2.2;
Swap(i, j);
Swap(x, y);
char m = 'A', n = 'B';
Swap(m, n);
return 0;
}
类似于上述Swap交换函数,虽然我们也可以通过typedef重命名来更换类型,但如果出现多个类型同时需要使用Swap函数时,typedef仍旧不能很好的解决问题。
因此,我们提出了泛型编程。
泛型编程: 编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
template
返回值类型 函数名(参数列表){}
实例演示:
templatevoid Swap(T& left, T& right)
{T tmp = left;
left = right;
right = tmp;
}
1、typename后面类型名字T是随便取,Ty、K、V,一般是大写字母或者单词首字母大写
2、T 代表是一个模板类型(虚拟类型)
3、注意:typename
是用来定义模板参数关键字,也可以使用class
(切记:不能使用struct代替class)
template//此处typename与class只有一些细微区别,后面再讲述
void Swap(T& left, T& right)
{T tmp = left;
left = right;
right = tmp;
}
演示如下:此处有一个问题,我们用不同类型的变量去调用这个函数模板时,所生成的Swap函数是不是同一个?
1)、问题引入:
接上文:
2)、总述:
1)、函数模板实例化·隐式实例化中一个容易出错的问题
templateT 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);//right
Add(d1, d2);//right
Add(a1, d1);//error
return 0;
}
隐式实例化中编译报错的场景之一:
报错原因并非不能隐式类型转换(事实上C++沿袭C,不用模板我们自己写Add时是成功的),此处报错的真正原因在于推演实例化报错。即无法确定需要转换到哪个类型。
2)、如何解决隐式实例化中这类问题?
解决方案一:强制类型转换
演示如下:
templateT Add(const T& left, const T& right)
{return left + right;
}
int main()
{//编译器自动推演,隐式实例化
cout<< Add(1, 2)<< endl;
cout<< Add((int)1.1, 2)<< endl;
cout<< Add(1.1, (double)2)<< endl;
return 0;
}
解决方案二:显示实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型
templateT Add(const T& left, const T& right)
{return left + right;
}
int main()
{//显式实例化:在函数名后的<>中指定模板参数的实际类型
cout<< Add(1, 2)<< endl;
cout<< Add(1.1, 2)<< endl;
cout<< Add(1.1, 2)<< endl;
return 0;
}
1、此处模板类型有几个,显示实例化时,实际类型就要写几个。
2、如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
解决方案三:使用多个模板类型
templateT1 Add(const T1& left, const T2& right)
{return left + right;
}
int main()
{cout<< Add(1, 2)<< endl;
cout<< Add(1.1, 2)<< endl;
cout<< Add(2, 1.1)<< endl;//要注意理解此处细节:参与Add时仍旧会发生隐式类型转换为double,只是返回类型为int
return 0;
}
3)、一些必须显示实例化的场景说明
templateT* Func(int n)
{T* a = new T[n];
return a;
}
class A{};
int main()
{// 必须显示实例化才能调用
Func(10);//此处是因为传入的参数为int型,而实际T需要返回类型为类A,因此在此处指定实际类型为A。
return 0;
}
1)、关于函数模板、非模板函数同时存在时,如何调用的问题?
回答:如何有匹配的非模板函数,率先调用该函数,若没有,则调用模板函数。
演示实例一:
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
templateT Add(T left, T right)
{return left + right;
}
void Test()
{Add(1, 2); // 有匹配的非模板函数,不会调用模板
Add(1.1, 2.2);// 没有匹配的非模板函数,会去调用模板
Add(1, 2); // 调用编译器特化的模板
}
int main()
{Test();
return 0;
}
演示实例二:
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
templateT1 Add(T1 left, T2 right)
{return left + right;
}
void Test()
{Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
int main()
{Test();
return 0;
}
1)、为什么需要类模板的引入?
class Stacki
{private:
int* _a;
int top;
int capacity;
};
class Stackc
{private:
char* _a;
int top;
int capacity;
};
int main()
{Stackc st1; // char
Stacki st2; // int
return 0;
}
typedef char STDataType;//此处修改类型
class Stack
{private:
STDataType* _a;
int top;
int capacity;
};
int main()
{Stack st1; // char
Stack st2; // char
return 0;
}
2)、类模板如何使用?
类模板格式说明:
templateclass 类模板名
{// 类内成员定义
};
3)、以类模板定义Stack举例说明使用细节:各类知识杂糅与理解
总体演示:
template//用模板定义一个类
class Stack
{public:
//构造函数
Stack(size_t capacity = 4)//缺省值
: _a(nulltpr)//初始化列表:默认先处理为这样子
, _top(0)
, _capacity(0)
{if (capacity >0)//虽然给了缺省值,但也有可能实参传递进来的是0
{ //为_a开辟空间,此处使用了new,注意new的用法以及new多个空间时[]中传入的数据
_a = new T[capacity];//此处不需要检查new是否失败,因为可以使用抛异常try\catch
_capacity = capacity;
_top = 0;
}
}
private:
T* _a;
size_t _top;
size_t _capacity;
};
int main()
{//用同一个类模板显示实例化
//不过Stack、Stack是不同的两个类型
Stackstr1;
Stackstr2;
//关于缺省值:如果我们能具体知道capacity值,就能这样直接传入
//若不知晓,就可以使用缺省值中给定的。
Stackstr3(100);
//关于初始化列表中的_capacity是为了处理这样的情况:
Stackstr4(0);
//若没有它,上述构造函数中进不了if语句,而_capacity没有初始化,
//那么得到的str4实际上_capacity得到的是默认随机值,
//而我们插入数据时扩容却是看_top==_capacity的
//这样就出了问题
}
细节解释一:自定义类型的模板定义需要显式实例化:即这里需要理解模板定义出来的类型T需要用到哪里。
//用同一个类模板显式实例化
//不过Stack、Stack是不同的两个类型
Stackstr1;
Stackstr2;
细节解释二:初始化列表处,_capacity(0)
这些是否要处理?
Stack(size_t capacity = 4)//缺省值
: _a(nulltpr)//初始化列表:
, _top(0)
, _capacity(0)
{}
回答:需要。构造函数中我们给予的只是缺省值,如果显示穿参并且参数为0时,会导致_capacity
处为随机值,那么后续会遇到各种麻烦。
//关于初始化列表中的_capacity是为了处理这样的情况:
Stackstr4(0);
//若没有它,上述构造函数中进不了if语句,而_capacity没有初始化,
//那么得到的str4实际上_capacity得到的是默认随机值,
//而我们插入数据时扩容却是看_top==_capacity的
//这样就出了问题
细节解释三:如果不在初始化列表给值,也可以在private成员变量中给,即使用C++打的补丁,但本质上是一样的。
private:
T* _a =nullptr;
size_t _top =0;
size_t _capacity =0;
模板不支持声明和定义跨文件分离。若是觉得放在类中形成内联不方便,则可在同文件中进行声明和定义分离。
同文件中的声明和定义分离,注意类模板的写法。
templateclass Stack
{public:
//构造函数
Stack(size_t capacity = 4)//缺省值
:_a(nulltpr)//初始化列表:默认先处理为这样子
, _top(0)
, _capacity(0)
{if (capacity >0)//虽然给了缺省值,但也有可能实参传递进来的是0
{ //为_a开辟空间,此处使用了new,注意new的用法以及new多个空间时[]中传入的数据
_a = new T[capacity];//此处不需要检查new是否失败,因为可以使用抛异常try\catch
_capacity = capacity;
_top = 0;
}
}
//析构函数
~Stack()
{//需要手动释放动态内存空间
delete[] _a;//使用delete时需要注意加括号
//后续置空这个步骤不影响
_a = nullptr;
_capacity = _top = 0;
}
//拷贝构造:此处设计深浅拷贝,后续讲解
void Push(const T& x);//此处再次体现泛型模板,因为插入数据类型为T
//删除
void Pop()
{assert(_top >0);
--_top;
}
//判空
bool Empty()
{return _top == 0;
}
//取栈顶元素
const T& Top()
{//此处_a中的数据属于堆上动态开辟的,因此可以使用引用传值返回
//此处加入const是为了防止引用返回后将栈顶元素修改
assert(_top >0);
return _a[_top - 1];
}
private:
T* _a;
size_t _top;
size_t _capacity;
};
//栈顶插入:
//此处再次体现泛型模板,因为插入数据类型为T
{//检查扩容
//有两种可能性:第一,空间已满;第二,空间未开辟
if (_top == _capacity)
{// 1、开新空间
// 2、拷贝数据
// 3、释放旧空间
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
//扩容需要重新开辟空间,以前我们学习的是malloc和realloc,能原地扩容或者系统自己解决
//现在我们使用new、delete,其不能自动解决空间问题,因此需要我们手动处理
T* tmp = new T[newcapacity];
//引出问题:为什么C++中我们推荐使用new、delet?
//回答:此处使用的是类模板,其模板类型T有可能是自定义类型
//malloc对自定义类型在申请空间时不会初始化。
//开了新空间,需要对旧空间进行处理
if (_a)//这里是用来检查是否为第一次扩容,因为首次扩容就不存在搬动空间问题
{ //要将旧空间中的数据拷贝到新空间里
memcpy(tmp, _a, sizeof(T) * _top);//注意memcpy的使用参数含义
//释放旧空间
delet[] _a;
}
//改变指向关系
_a = tmp;
_capacity = newcapacity;
}
_a[_top] = x;
++_top;
}
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
网页标题:【ONE·C++||内存管理和模板初阶】-创新互联
文章出自:http://myzitong.com/article/codgoo.html