总言
内存管理和模板初阶:介绍了C++中的内存区域划分,以及C++中的动态内存管理(new/delete对内置类型/自定义类型的使用区别,与malloc/free的区别,底层原理等),介绍了泛型编程(函数模板/类模板的基本写法、实例化细节等)。
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)
= ____
2、 sizeof(char2)
= ____ strlen(char2)
= ____
3、 sizeof(pChar3)
= ____ strlen(pChar3)
= ____
4、 sizeof(ptr1)
= ____
2)、解答(区域示意图):
sizeof(num1) = __40__;//数组大小,10个整形数据一共40字节
sizeof(char2) = __5__;//包括\0的空间
strlen(char2) = __4__;//不包括\0的长度
sizeof(pChar3) = __4__;//pChar3为指针
strlen(pChar3) = __4__;//字符串“abcd”的长度,不包括\0的长度
sizeof(ptr1) = __4__;//ptr1是指针
2、动态内存分布管理
2.1、C语言中动态内存管理:malloc、calloc、realloc、free
此处详细讲解请看C || 动态内存管理。
2.2、C++中动态内存管理 :new、delete的基本使用说明
1)、C++可以兼容C,为什么还需要提出自己的内存管理方式?
1、首先要明确的是,C++兼容C,故malloc
、calloc
、realloc
、free
这些在C++中照样可以用。
2、但有些地方,C中的动态内存管理方式就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new
和delete
操作符进行动态内存管理。
2.2.1、针对内置类型(如何申请空间、如何释放空间)
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;
}
2)、如何释放空间?
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
用法相对简化了。
2.2.2、针对自定义类型(如何申请空间、如何释放空间)
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、若类不提供默认构造,若要编译通过,则需要自己传参。
//new做的动作: 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;
}
2.2.3、new申请动态内存失败后如何处理
说明: new
失败不需要检查返回值,而是会抛异常(相关内容见异常处理章节)。
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;
}
2.2、operator new与operator delete全局函数
2.2.1、关于new、delete功能的底层原理
1、new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过 operator delete
全局函数来释放空间。
2、而 operator new
实际也是通过malloc
来申请空间 ,如果 malloc
申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete
最终是通过free
来释放空间的。
2.2.2、重载operator new与operator delete
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 allocator<ListNode> alloc;//静态成员变量
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以后会讲,现在先会用即可
allocator<ListNode> ListNode::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;
}
2.2.3、new、delete实现原理总结
2.3、定位new
2.3.1、是什么和为什么
1)、场景引入:
问题:有时我们需要对已经定义的类进行初始化。而我们知道,类初始化需要调用构造函数,目前已知的两种构造函数调用方式是,①在定义类时会调用构造函数初始化成员;②通过new开辟自定义类型也会经过构造函数。
那么,针对下述p2
这样已经申请好的类,成员变量私有情况下,如何为它初始化?
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;
}
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;
//p2现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p2 = (A*)malloc(sizeof(A));
if (p2 == nullptr)
{
perror("malloc fail");
}
new(p2)A;//方法一: 如果A类的构造函数有参数时,此处需要传参
new(p2)A(10);//方法二
return 0;
}
3)、为什么用:
使用场景: 定位new表达式在实际中一般是配合内存池使用。 因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
此部分(动态内存)衔接内容:智能指针、异常处理
3、模板初阶
3.1、泛型编程
1)、关于为什么要有泛型编程的引入说明:
问题:若要实现Swap
交换函数,满足各类型需求,可以如何做?
1、虽然我们也可以通过typedef
重命名来更换类型,但如果出现多个类型同时需要使用Swap
函数时,typedef
仍旧不能很好的解决问题。
2、根据之前所学,可以使用函数重载,但是有一下几个不好的地方:
①重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
② 代码的可维护性比较低,一个出错可能所有的重载均出错。
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;
}
因此,我们提出了泛型编程。在C++中,泛型编程是一种编程技术,它允许程序员编写与特定数据类型无关的代码,从而提高代码的复用性和灵活性。泛型编程通过使用模板(templates)来实现。
2)、模板简述
模板是一种允许程序员定义处理各种数据类型的函数或类的机制。在C++中,模板分为两类:函数模板和类模板。
函数模板: 函数模板允许程序员定义可以处理多种数据类型的函数。通过在函数声明中添加模板参数列表,可以定义函数模板。
类模板: 类模板允许程序员定义可以处理多种数据类型的类。与函数模板类似,通过在类声明中添加模板参数列表,可以定义类模板。
3.2、函数模板
🎯如何声明和使用一个函数模板?
🎯函数模板的实例化是如何发生的?
🎯函数模板和重载函数之间有什么区别?
3.2.1、函数模板的使用方式
格式: 此处template
、typename
是两个关键字。
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
实例演示: 以上述swap
交换函数为例。
template<typename T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
说明:
1、template
关键字:用于声明模板,它告诉编译器接下来的代码是一个模板定义,而不是一个普通的函数或类定义。
2、typename
关键字在模板编程中有两个主要用途:
①在模板参数列表中声明类型参数: 如上所述,typename
用于声明模板参数的类型(虚拟类型)。typename
后面类型名字T
是自定义的,也可以使用y、K、V
等等,一般是大写字母或者单词首字母大写。
②在模板代码内部指定依赖名称的类型: 当模板代码涉及到嵌套依赖类型时(例如,模板类的成员模板),typename
用于告诉编译器,接下来的名称是一个类型名,而不是一个成员变量或函数。
3、注意:typename
是用来定义模板参数关键字,也可以使用class
(切记:不能使用struct代替class)
template<class T>//此处typename与class只有一些细微区别,后面再讲述
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
演示如下: 此处有一个问题,我们用不同类型的变量去调用这个函数模板时,所生成的Swap
函数是不是同一个?
要回答此问题,需要理解函数模板的底层原理。
3.2.2、函数模板底层原理
1)、基本说明
接上文:先看观察其汇编。
说明:在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double
类型使用函数模板时,编译器通过对实参类型的推演,将T
确定为double
类型,然后产生一份专门处理double
类型的代码,对于字符类型也是如此。
结论:当使用不同类型的变量去调用一个函数模板时,编译器会为每种类型生成一个独立的函数实例。 这并不是同一个函数,而是针对每种类型参数生成的特化版本。这些特化版本在编译时被生成,并被视为不同的实体。
2)、函数模板与函数重载
函数重载是指在同一个作用域内可以定义多个同名函数,只要它们的参数列表(参数类型、参数数量或参数顺序)不同即可。 编译器在调用重载函数时,会根据提供的参数类型和数量来选择正确的函数。(这里就引出一个区别,函数重载出的实例,可以参时数量不同,而同一个函数模板特化出来的函数,参时类型相同)
函数模板使用template
关键字,并在尖括号< >
中指定类型参数(通常是typename
或class
关键字后跟一个标识符)。当函数模板被调用时,编译器会根据提供的参数类型生成该函数的特化版本。
函数模板和函数重载两者允许并存,C++编译器优先考虑普通函数,如果函数模板可以产生一个更好的匹配,那么选择模板(见下述3.2.4)。
在编译时,每个重载函数都是一个独立的实例,编译器会为每个重载版本生成具体的函数代码。函数模板本身不产生具体的函数代码。 只有当它被实例化(即,用具体类型替换模板参数)时,编译器才会生成具体的函数代码。
3.2.3、函数模板的实例化:隐式实例化、显式实例化
1)、函数模板·隐式实例化中一个容易出错的问题
如下述代码:
template<class T>
T 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)、如何解决隐式实例化中这类问题?
解决方案一:强制类型转换
演示如下:
template<class T>
T 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;
}
解决方案二:显示实例化
显式实例化:在函数名后的<>
中指定模板参数的实际类型
template<class T>
T 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<double>(1.1, 2) << endl;
return 0;
}
1、此处模板类型有几个,显示实例化时,实际类型就要写几个。
2、如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
解决方案三:使用多个模板类型
template<typename T1, typename T2>
T1 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)、一些必须显示实例化的场景说明
template<class T>
T* Func(int n)
{
T* a = new T[n];
return a;
}
class A{};
int main()
{
// 必须显示实例化才能调用
Func<A>(10);//此处是因为传入的参数为int型,而实际T需要返回类型为类A,因此在此处指定实际类型为A。
return 0;
}
3.2.4、模板参数的匹配原则
1)、关于函数模板、非模板函数同时存在时,如何调用的问题?
基本规则:若有匹配的非模板函数,率先调用该函数,若没有则调用模板函数。
演示实例一:
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理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(1.1, 2.2);// 没有匹配的非模板函数,会去调用模板
Add<int>(1, 2); // 调用编译器特化的模板
}
int main()
{
Test();
return 0;
}
演示实例二:
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
int main()
{
Test();
return 0;
}
3.3、类模板
3.3.1、为什么和是什么
1)、为什么需要类模板的引入?
2)、类模板如何使用?
类模板格式说明:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
3)、以类模板定义Stack举例说明使用细节:各类知识杂糅与理解
总体演示:
template<typename T> //用模板定义一个类
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<int>、Stack<char>是不同的两个类型
Stack<int> str1;
Stack<char> str2;
//关于缺省值:如果我们能具体知道capacity值,就能这样直接传入
//若不知晓,就可以使用缺省值中给定的。
Stack<int> str3(100);
//关于初始化列表中的_capacity是为了处理这样的情况:
Stack<int> str4(0);
//若没有它,上述构造函数中进不了if语句,而_capacity没有初始化,
//那么得到的str4实际上_capacity得到的是默认随机值,
//而我们插入数据时扩容却是看_top==_capacity的
//这样就出了问题
}
细节解释一:自定义类型的模板定义需要显式实例化,即这里需要理解模板定义出来的类型T
需要用到哪里。
//用同一个类模板显式实例化
//不过Stack<int>、Stack<char>是不同的两个类型
Stack<int> str1;
Stack<char> str2;
细节解释二:初始化列表处,_capacity(0)
这些是否要处理?
Stack(size_t capacity = 4)//缺省值
: _a(nulltpr)//初始化列表:
, _top(0)
, _capacity(0)
{}
回答:需要。构造函数中我们给予的只是缺省值,如果显示传参并且参数为0
时,会导致_capacity
处为随机值,那么后续会遇到各种麻烦。
//关于初始化列表中的_capacity是为了处理这样的情况:
Stack<int> str4(0);
//若没有它,上述构造函数中进不了if语句,而_capacity没有初始化,
//那么得到的str4实际上_capacity得到的是默认随机值,
//而我们插入数据时扩容却是看_top==_capacity的
//这样就出了问题
细节解释三:如果不在初始化列表给值,也可以在private
成员变量中给,即使用C++打的补丁,但本质上是一样的。
private:
T* _a =nullptr;
size_t _top =0;
size_t _capacity =0;
3.3.2、模板实例化说明
模板不支持声明和定义跨文件分离。 若是觉得放在类中形成内联不方便,则可在同文件中进行声明和定义分离。
同文件中的声明和定义分离,注意类模板的写法。
3.3.3、以Stack的实现演示模板使用
template<typename T>
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;
}
}
//析构函数
~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
void Stack::Push(const T& x)
{
//检查扩容
//有两种可能性:第一,空间已满;第二,空间未开辟
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;
}
模板初阶后续衔接内容:模板进阶
4、自测
4.1、题一:C++内存区域划分
下面有关C++内存分配堆栈说法错误的是( )
A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制
B.对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长
C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题
D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的
D:32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用,故错误。
4.2、题二:C++中的堆和栈
C++中关于堆和栈的说法,哪个是错误的:( )
A.堆的大小仅受操作系统的限制,栈的大小一般较小
B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题
C.堆和栈都可以静态分配
D.堆和栈都可以动态分配
选C。
A:堆大小受限于操作系统,而栈空间一般有系统直接分配
B:频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题
C:堆无法静态分配,只能动态分配
D:栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放
4.3、题三:自定义类型中,new、delete申请释放空间
c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( )
ClassA *pclassa=new ClassA[5];
delete pclassa;
回答:程序可能崩溃。
申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃。
4.4、题四:malloc和new
下面有关malloc和new,说法错误的是? ( )
A.new 是创建一个对象(先分配空间,再调构造函数初始化), malloc分配的是一块内存
B.new 初始化对象,调用对象的构造函数,对应的delete调用相应的析构函数,malloc仅仅分配内存,free仅仅回收内存
C.new和malloc都是保留字,不需要头文件支持
D.new和malloc都可用于申请动态内存,new是一个操作符,malloc是是一个函数
选C:需要头文件malloc.h,只是平时这个头文件已经被其他头文件所包含了,用的时候很少单独引入,故错误
4.5、题五:构造函数和析构函数顺序
设已经有A、B、C、D,4个类的定义,下述程序中A,B,C,D析构函数调用顺序为? ( )
C c;
int main()
{
A* pa = new A();
B b;
static D d;
delete pa;
}
A B D C。
首先手动释放pa, 所以会先调用A的析构函数。
其次会跟定义相反的顺序释放局部对象。
这里只有b,就释放b。
再释放静态局部对象d。
再释放全局对象c
4.6、题六:内置类型中,new、delete申请释放空间
使用 char* p = new char[100]
申请一段内存,然后使用delete p
释放,有什么问题?( )
A.会有内存泄露
B.不会有内存泄露,但不建议用
C.编译就会报错,必须使用delete []p
D.编译没问题,运行会直接崩溃
选B。
对内置类型,此时delete就相当于free,因此不会造成内存泄漏,编译不会报错,但建议针对数组释放使用delete[]。
如果是自定义类型,不使用方括号就会运行时错误
4.7、题七:函数模板实例化(隐式、现实实例化问题)
在下列对fun的调用中,错误的是( )
template <class T>
T fun(T x, T y) {
return x * x + y * y;
}
A.fun(1, 2)
B.fun(1.0, 2)
C.fun(2.0, 1.0)
D.fun<float>(1, 2.0)
B:由于参数类型不一样,模板不支持类型转换,推导参数会产生二义性,编译错误
4.8、题八:考察模板概念
下列关于模板的说法正确的是( )
A.模板的实参在任何时候都可以省略
B.类模板与模板类所指的是同一概念
C.类模板的参数必须是虚拟类型的
D.类模板中的成员函数全是模板函数
A.不一定,参数类型不同时有时需要显示指定类型参数
B.类模板是一个类家族,模板类是通过类模板实例化的具体类
C.C++中类模板的声明格式为template<模板形参表声明><类声明>,并且类模板的成员函数都是模板函数
D.正确,定义时都必须通过完整的模板语法进行定义