C++内存管理+模板

Posted 朱C.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++内存管理+模板相关的知识,希望对你有一定的参考价值。

前言:

本章将详细讲解C++内存管理和模板的实现。

第一部分我们讲解C++内存管理,C语言中有malloc/calloc/realloc等开辟空间和free释放空间,那么C++将符合实现呢?

第二部分我们会一起来初步认识模板与泛型编程,并详细探讨函数模板和类模板,为后续的学习做准备。

目录

(一)C++内存管理

(1)引入

(2)C++内存管理方式

2.1、new和delete操作内置类型

2.2、new和delete操作自定义类型

(3)operator new与operator delete函数

(4)总结

(5)定位new(了解)

(二)模板初阶

(1)泛型编程

(2)函数模板

2.1、隐式实例化

2.2、显式实例化

3.2、模板函数匹配原则

(3)类模板


(一)C++内存管理

(1)引入

有了前面C语言的学习,我们对于栈,堆,静态区等存放的数据也会有初步了解。下面我们通过一个题目来回忆一下栈,堆,静态区等存放的是哪些数据:

问题:

 

分析:

下面是按顺序作答:

  • 1、globalvar是全局变量,所以在静态区;
  • 2、staticGobalVar是全局的静态变量,所以在静态区;
  • 3、staticVar是静态变量,所以在静态区;
  • 4、localVar是int类型的变量,所以在栈;
  • 5、num1是数组名,也就是数组首元素的地址,也就是int*类型的变量,所以在栈;
  • 6、char2是数组名,同上,是局部变量所以在栈;
  • 7、char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上;
  • 8、 pChar3局部变量在栈区   *pChar3得到的是字符串常量字符在代码段;
  • 9、ptr1局部变量在栈区     *ptr1得到的是动态申请空间的数据在堆区
  • 图解:

 

答案:

 

 

说明:

  • 1. 又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  • 2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
  • 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  • 3. 用于程序运行时动态内存分配,堆是可以上增长的。
  • 4. 数据段--存储全局数据和静态数据。
  • 5. 代码段--可执行的代码/只读常量。

(2)C++内存管理方式

C语言内存管理方式在有些地方并不适用于C++,所以C++又提出了自己的内存管理方式:

通过new和delete操作符进行动态内存管理。

2.1、new和delete操作内置类型

new和delete的用法类似于malloc和free,我们来一起探究如何使用吧:

 分析:

 这里要注意的是:

申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。 ——————————————————————————

2.2、new和delete操作自定义类型

上面是最基本的内置类型的用法,下面我们来探讨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()

	A* p1 = (A*)malloc(sizeof(A));
	//A* p2 = new A(1);
	free(p1);
	//delete p2;
    

这里我们发现并没有打印任何结果,下面我们把注释的代码解开,则会发现输出以下结果:

这就说明了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()

	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	free(p3);
	delete p4;

	return 0;

 这里也没有打印任何结果,但是上面不是才分析出new和delete会调用构造和析构函数吗?

这里是因为new int,其中int是内置类型,所以没有调用A的构造函数,这样一来,自然也不调用其析构函数。当是内置类型时,new和malloc,delete和free的作用几乎等同。

我们最后再看一段代码:


class A

public:
	A(int a = 0)
		: _a(a)
	
		cout << "A():" << this << endl;
	
	~A()
	
		cout << "~A():" << this << endl;
	
private:
	int _a;
;
int main()

	
	A* p6 = new A[10];
	delete[] p6;
	return 0;

这里我们发现输出结果如下:

我们可以得出结论:

    new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
    还会调用构造函数和析构函数。

(3)operator newoperator delete函数

new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete 系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。 —————————————————————————————————— 我们通过汇编代码可知(过程省略,汇编代码我们看不懂): operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的 虽然内核也是通过malloc和free实现,但 最大的区别 就是申请失败是抛异常,而不是返回空指针。 (后续我们会讲解抛异常) —————————————————————————————————— 但是new和delete并不等同于operator new和operator delete。 实际上我们通过上面 自定义类型 已经可知: 其实
  • new=operator new(malloc)+调用构造函数;
  • delete=operator delete(free)+调用析构函数;
ps:对于内置类型来说没有什么区别。 样例:


class A

public:
	A(int a = 0)
		: _a(a)
	
		cout << "A():" << this << endl;
	
	~A()
	
		cout << "~A():" << this << endl;
	
private:
	int _a;
;
class Stack

public:
	Stack()
	
		cout << "Stack()" << endl;
		_a = new int[4];
		top = 0;
		capacity = 4;
	

	~Stack()
	
		cout << "~Stack()" << endl;
		delete[]_a;
		top = 0;
		capacity = 0;
	

private:
	int* _a;
	int top;
	int capacity;
;

int main()

		 //失败了抛异常
	int* p1 = (int*)operator new(sizeof(int*));

	// 失败返回nullptr
	int* p2 = (int*)malloc(sizeof(int*));
	if (p2 == nullptr)
	
		perror("malloc fail");
	

	 //申请空间 operator new -> 封装malloc
	 //调用构造函数
	A* p5 = new A;

	// 先调用析构函数
	// 再operator delete p5指向的空间
	// operator delete -> free
	delete p5;

	// 申请空间 operator new[] ->perator new-> 封装malloc
	// 调用10次构造函数
	A* p6 = new A[10];
	
	// 先调用10次析构函数
	// 再operator delete[] p6指向的空间
	delete[] p6;


	int* p7 = new int[10];
	free(p7);  // 正常释放

	A* p8 = new A;
	//free(p8); // 少调用的析构函数
	delete p8;
	//对于无空间开辟的少调用析构函数可以正常运行


	//Stack st;

	Stack* pts = new Stack;
	//free(pts);//这样就少调用了析构函数,会造成内存泄漏
	delete pts;


	return 0;

大家可以一一调试试验,验证上面的结论。

(4)总结

对于内置类型:

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL。 对于自定义类型:

new的原理

  • 1. 调用operator new函数申请空间
  • 2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  • 1. 在空间上执行析构函数,完成对象中资源的清理工作
  • 2. 调用operator delete函数释放对象的空间

new T[N]的原理

  • 1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  • 2. 在申请的空间上执行N次构造函数

delete[]的原理

  • 1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  • 2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

(5)定位new(了解)

定义: 定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象 使用格式: new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表 使用场景: 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如 果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
class A

public:
 A(int a = 0)
 : _a(a)
 
 cout << "A():" << this << endl;
 
 ~A()
 
 cout << "~A():" << this << endl;
 
private:
 int _a;
;
// 定位new/replacement new
int main()

 // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
 A* p1 = (A*)malloc(sizeof(A));
 new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
 p1->~A();
 free(p1);
 A* p2 = (A*)operator new(sizeof(A));
 new(p2)A(10);
 p2->~A();
 operator delete(p2);
  return 0;

后面我们在做内存池项目时候会使用,这里大家了解即可。

(二)模板初阶

(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;

但是我们发现,他们的基本思想和实现是差不多的,甚至除了变量的类型,其他都一样。那么我们可不可以使用一个“模具”来代替呢???

答案是可以的!我们会利用泛型编程来实现。

那么什么是泛型编程呢?

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。 这里我们就引入了模板。 模板分为函数模板和类模板,下文我们一一探讨。

 

上面交换函数的模板实现:
// 泛型编程 -- 模板

template<typename T>
void Swap(T& x, T& y)

	T tmp = x;
	x = y;
	y = tmp;

(2)函数模板

函数模板格式:

template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表)

样例参照交换函数的模板实现。

注意:typename是用来定义模板参数关键字也可以使用class

2.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);
 Add(d1, d2);
 
 Add(a1,d2);
 

其中Add(a1,d2)无法通过编译。因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
 编译器无法确定此处到底该将T确定为int 或者 double类型而报错
 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅。

我们可以采用两种处理方式:

  •  1. 用户自己来强制转化 ,如Add(a, (int)d);
  • 2. 使用显式实例化

2.2、显式实例化

我们上面的两种处理方式中提到了显式实例化,那么如何实现呢?

在函数名后的<>中指定模板参数的实际类型 例:
int main(void)

 int a = 10;
 double b = 20.0;
 
 // 显式实例化
 Add<int>(a, b);
 return 0;

ps:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错

3.2、模板函数匹配原则

  • 1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板数;
  • 2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数;
  • 3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

(3)类模板

定义格式:

template<class T1, class T2, ..., class Tn>
class 类模板名

 // 类内成员定义
; 
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
 
public :
 Vector(size_t capacity = 10)
 : _pData(new T[capacity])
 , _size(0)
 , _capacity(capacity)
 
 
 // 使用析构函数演示:在类中声明,在类外定义。
 ~Vector();
 
 void PushBack(const T& data);
 void PopBack();
 // ...
 
 size_t Size() return _size;
 
 T& operator[](size_t pos)


 
 assert(pos < _size);
 return _pData[pos];
 
 
private:
 T* _pData;
 size_t _size;
 size_t _capacity;
;

// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()

 if(_pData)
 delete[] _pData;
 _size = _capacity = 0;

 注意:

1、上述Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具;

2、类模板中函数放在类外进行定义时,需要加模板参数列表;

——————————————————————————————————————

类模板的实例化: 类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类 例:
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

感谢阅读,祝您学业有成!!

c++数组类模板(栈内存)

#ifndef _ARRAY_H_

#define _ARRAY_H_

/*

* 实现一个数组类模板,在栈上

* why

* 2016/9/5

*/

template

< typename T, int N >

class Array

{

private:

T m_array[N];

public:

int length(); //获取数组长度

bool set_array(T value, int index);  //设置数组元素内容

bool get_array(T& value, int index); //获取数组元素内容

T& operator [](int index); //重载[]操作符,方便数组对象的操作

T operator [](int index) const; //如果用户定义的是const的数组对象,那么访问数组元素时,就需要用const修饰的成员函数

virtual ~Array(); //析构函数最好定义成虚函数,可以让继承此类的类重写这个析构函数。当然这里可以不是vitrual的,因为如果构造函数是private而不是protected,

//就是不想让这个类被继承,所以可以不用vitrual,但如果构造函数等是protected的,说明是想被继承的,如果想被继承,那么析构函数最好是virtual的

};


template 

< typename T, int N >

int Array<T, N>::length()

{

return N;

}


template 

< typename T, int N >

bool Array<T, N>::set_array(T value, int index)

{

bool ret = (0 <= index) && (index < N);

if (ret)

{

m_array[index] = value; 

}

return ret;

}


template 

< typename T, int N >

bool Array<T, N>::get_array(T& value, int index)

{

bool ret = (0 <= index) && (index < N);

if (ret)

{

value = m_array[index];

}

return ret;

}


template 

< typename T, int N >

T& Array<T, N>::operator[](int index)

{

return m_array[index];

}


template 

< typename T, int N >

T Array<T, N>::operator[](int index) const

{

return m_array[index];

}



template 

< typename T, int N >

Array<T, N>::~Array()

{

}



#endif


以上是关于C++内存管理+模板的主要内容,如果未能解决你的问题,请参考以下文章

C++内存管理+模板入门知识点

[C++] C++中的内存管理机制

[C/C++]C++中的内存管理

C++从入门到入土第六篇:C/C++内存管理

什么是C语言设计模板结构?

c++师傅领进门,修行靠个人第六篇:内存管理