C++ Vector容器实现

引言

本文主要内容
(1) C数组
(2)C指针 malloc 动态数组
(3) 类
(4)泛型编程 类模板
(5)动态分配

数组

引用《C++ Primer Plus》中关于数组的描述:数组是一种数据格式,能够存储多个同类型的值。
以下以一维数组为例:
a.定义数组:
元素类型名 数组名[元素个数]
例:

int nxMyArry[10];      // 定义一个叫做nxMyArry的int型数组,可以存储10个元素

b.使用数组
数组名[下标]
c.数组使用问题
我们都知道,在C++中,数组遵循先定义后使用的原则,在定义数组时,元素个数必须使用整型常数(如上述例子中10)或 const 值,也可以是常量表达式,即数组大小在编译时是已知的。这样带来的问题是,定义时不知道需要使用多大的数组,定义过小不够使用,定义过大造成浪费。解决此问题可以使用动态数组。

动态数组

动态数组,顾名思义就是数组的大小是动态的,是可变的。动态数组的原理是当原有数组大小不够时,定义一个新的数组,将原有数组中的数据拷贝至新的数组中,完成数组的扩充。
在C语言中,可以用库函数 malloc() 来分配内存 ;C++11也保留了这一特性,但C++还有更好的方法——new运算符。
以下是malloc使用的大致代码,主要思想是根据变量n的值来开辟对应大小的内存,达到不浪费内存的目的。

// 动态数组使用
int n;								// 数组大小
int* p = null;						// 定义p存放数组地址

p = malloc(n * sizeof(int));		// 更严格的写法  p = (int*)malloc(n * sizeof(int));
//中间使用...
free(p);							// 使用完成后释放内存

下面以一个实际例子来说明:定义一个数组用来记录一组整型数据,数据的个数0-10000。
【分析】由于不知道数据的具体数量,因此在定义大小时会面临不够使用或者浪费的问题,数组大小不够(程序运行期间出现越界问题)是不能够容忍的,因此稳妥型编程如下:

#define ArrySize 10000
int _nxMyArry[_nArrySize];

这样定义的好处是不用担心数组不够用的情况发生,但是,当只有50个数据时,这样定义明显存在内存浪费的情况。(PS:这里的数据量只是为了说明存在浪费),当使用动态数组时:

int _nArrySize = 0;					// _nArrySize为决定数组大小的变量
int* p = NULL;						// 定义p存放数组地址

// _nArrySize 可以由其它参数给定,这里以键盘输入为例
cin >> _nArrySize;

p = malloc(_nArrySize  * sizeof(int));		// 更严格的写法  p = (int*)malloc(n * sizeof(int));
//中间使用...

free(p);							// 使用完成后释放内存

这样就完全没有浪费的情况发生,也不存在不够的情况,开辟的空间完全由需求决定。为方便后续理解,上述内存情况如图 1 所示:
内存空间使用情况

图1 内存分配情况
使用动态数组的内存使用情况如上图所示,一个 int 型变量 `_nArrySize` 用于记录当前数组的大小,指针p指向malloc开辟内存空间的**首地址**,要管理这块内存空间仅有首地址是不够的,还需知道这块内存空间有多大,即`_nArrySize`。

数组类封装

当我们提供一个容器的时候,为方便消费者使用,需要提供一些功能,例如push、pop等最基本的操作,下面开始一步一步的进行完善和封装。

容器功能—插入数据

假设我们现在有一个函数push,功能是将 int 型数据 data 插入首地址为 p 的数组中存储,则插入该数据的流程图如图 2 所示:
插入数据流程图

图2 插入数据流程图
该流程对应的C++代码如下
int* p = NULL;       // 指向内存段,用于记录存储数据的首地址
int num = 0;		 // 用于记录当前数据的数量

// 插入数据
void push(int  data)
{
	// p 为空时,表明不存在数据,直接开辟一个空间存储新数据即可
	// p 不为空时,表明已存在数据,需要将空间扩大,再新添加数据
	if (p != NULL)
	{
		// 1.1 新开内存
		int* pNew = (int*)malloc((num + 1) * sizeof(int));
	
		// 1.2 拷贝原有数据并释放原内存
		memcpy(pNew, p, sizeof(int*) * num;
		free(p);
	
		// 1.3 p指向新开的内存
		p = pNew;
	
		// 1.4 添加新数据
		p[num] = data;
	
		// 1.5 更新容器中数据的数量
		num++;
	}
	else
	{
		// 2.1 开内存
		// 2.2 p指向开辟的内存空间
		p = (int*)malloc(sizeof(int));
		
		// 2.3 添加数据
		p[num] = data;
		
		// 2.4 更新容器中数据的数量
		num++;
	}
}

从上述流程图及代码可以看出,p 为空时,表明之前没有存储过数据,仅需要任意开辟一段内存空间存储需要 push 的数据即可;而当 p 不为空时,表明之前已经 push 过数据, 这时候的做法是重新开辟一块更大的空间,将原本存在的数据拷贝至新空间,然后再新加入本次需要 push 的数据。
细心的可能看出来了,上述代码存在问题:每次 push 一个数据都需要去开辟一次空间、拷贝一次数据、释放一次空间,这极大的拖慢了效率,而且还会造成额外的性能开销,解决办法后续再说。当已有数据存在是,重新开辟空间的示意图如下图所示,有人可能有疑惑,为什么不在原内存段后面直接增加一些空间,这样不就能不拷贝数据了吗?这是由于:

  • (1)原地址后续地址可能已经分配给其他程序;
  • (2)操作系统分配地址是随机的,只能给你一块未占用的大小足够的地址空间,而不能保证是你想要的具体地址。这就好比预定酒店的时候,不知道是具体哪间房,只知道是按你预定要求的某一间。
    内存空间使用
图3 内存空间分配情况
写到这里,已经基本完成了插入功能,为了让大家看清楚,上面的代码可能有一些多余,if 分支有的是重复的操作是可以合并的,懒得写一些重复的代码,先进行一波优化。 > [!NOTE] 下面开始用类和对象了,因此将 malloc 换 new,更方便使用。
// 插入数据
void push(int  data)
{
	// 1 新开内存
	int* pNew = new int[num + 1];

	if (p != NULL)
	{
		// 2 拷贝原有数据并释放原内存
		memcpy(pNew, p, sizeof(int*) * num;
		delete[] p;
	}
	
	// 3 p 指向新开的内存
	p = pNew;

	// 4 添加新数据进来。数据量增1
	p[num++] = data;
}

下面开始对这个功能进行封装,使之成为一个类。
这个类成员是数组首地址和数组的数据个数,类函数即插入数据,后续再增加一些删除、查找等功能,逐渐完善,直接看下面代码吧。

class MyIntArry{
prvite:
	int* m_pArry;       		// 指向内存段,用于记录存储数据的首地址
	int m_nNum;		    		// 用于记录当前数据的数量
public:
	MyIntArry()
	{
		m_pArry = NULL;
		m_nNum = 0;
	}
	
	~MyIntArry()
	{
		if(m_pArry != NULL)
		{
			delete[] m_pArry;
			m_pArry = NULL;
		}
	}
	
public:
	void push(int nData_);		// 插入数据
	void pop();					// 删除数据
};

void MyIntArry::push(int nData_)
{
	// 1 新开内存
	int* pNew = new int[m_nNum + 1];

	if (m_pArry != NULL)
	{
		// 2 拷贝原有数据并释放原内存
		memcpy(pNew, m_pArry, sizeof(int*) * m_nNum;
		delete[] m_pArry;
	}
	
	// 3 p 指向新开的内存
	m_pArry = pNew;

	// 4 添加新数据进来。数据量增1
	m_pArry[num++] = nData_;
}

完成以上工作,这个类就已经完成封装了,需要什么功能再完善一下就好。当然,这不是我们这篇文章的终极目的,我们的终极目的是做一个容器,把这个类变成泛型的、动态分配的。

类模板封装

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。容器、迭代器都是泛型编程的例子。模板是C++中泛型编程的基础。下面将我们刚才封装好的类改为类模板:

template<class T>
class MyArry{
prvite:
	T* m_pArry;       			// 指向内存段,用于记录存储数据的首地址
	int m_nNum;		    		// 用于记录当前数据的数量
public:
	MyArry()
	{
		m_pArry = NULL;
		m_nNum = 0;
	}
	
	~MyArry()
	{
		if(m_pArry != NULL)
		{
			delete[] m_pArry;
			m_pArry = NULL;
		}
	}
	
public:
	void push(const T& nData_);		// 插入数据     const T& nData_ 这种写法更安全
	void pop();						// 删除数据
};

void MyArry<T>::push(const T& nData_)
{
	// 1 新开内存
	T* pNew = new T[m_nNum + 1];

	if (m_pArry != NULL)
	{
		// 2 拷贝原有数据并释放原内存
		memcpy(pNew, m_pArry, sizeof(T*) * m_nNum;
		delete[] m_pArry;
	}
	
	// 3 p 指向新开的内存
	m_pArry = pNew;

	// 4 添加新数据进来。数据量增1
	m_pArry[m_nNum++] = nData_;
}

// 下面使用的例子
int main()
{
	// 需要什么类型就写什么类型,不管什么类型这个容器都能使用
	MyArry<int> intArry;
	MyArry<double> doubleArry;

	for (int i = 0; i < 10; i++)
	{
		intArry.push(i);
		doubleArry.push(i + 0.1);
	}
}

写到这,容器的基本功能就快完成了,接下来解决另一个问题—动态分配。刚刚已经提到,我们现在的写法是,每一次 push 一个数据,就需要完成以下步骤:开辟新空间、拷贝数据、释放空间,能不能空间不够的时候,多开辟一些空间呢,这样就不用每次都重复这个步骤,答案当然是可以的,请看以下代码:

double g_nNewSpaceRatio = 1.5      			// 每次开辟新空间的倍数

template<class T>
class MyArry{
prvite:
	T* m_pArry;       			// 指向内存段,用于记录存储数据的首地址
	int m_nNum;		    		// 用于记录当前数据的数量
	int m_nMaxSize;				// 用于记录当前容器能容纳的最大元素个数
public:
	MyArry()
	{
		m_pArry = NULL;
		m_nNum = 0;
		m_nMaxSize = 5;			// 默认值先给个5
	}
	
	~MyArry()
	{
		if(m_pArry != NULL)
		{
			delete[] m_pArry;
			m_pArry = NULL;
		}
	}
	
public:
	void push(const T& nData_);		// 插入数据     const T& nData_ 这种写法更安全
	void pop();						// 删除数据
};

void MyArry<T>::push(const T& nData_)
{
	// 没有申请过空间时申请空间
	if (m_pArry == NULL)
	{
		T* pNew = new T[m_nMaxSize];
	}
	
	// 表明空间不够用了,需要开辟新的空间
	if (m_nNum > m_nMaxSize)
	{
		// 1 扩大m_nMaxSize为原来的g_nNewSpaceRatio倍;这里g_nNewSpaceRatio取1.5
		m_nMaxSize = (int)(m_nMaxSize * g_nNewSpaceRatio);      
		
		// 2 开辟新空间
		T* pNew = new T[m_nMaxSize];

		// 3 拷贝原有数据并释放原内存
		memcpy(pNew, m_pArry, sizeof(T*) * m_nNum;
		delete[] m_pArry;

		// 4 m_pArry指向新开的内存
		m_pArry = pNew;
	}

	// 添加新数据进来。数据量增1
	m_pArry[m_nNum++] = nData_;
}

PS:上面的代码还有不完美的地方,只是为了说明过程,缺少一些保护机制,比如m_nMaxSize应该开辟内存成功后再更新,开辟内存失败的处理等等。

结束

作为一个容器,上面写的还远远不够,只是简单介绍了一下插入数据的功能。
下面一些 vector 待完善的点:

  • 无参构造函数
  • 有参构造函数
  • 拷贝构造函数
  • 迭代器
  • reserve
  • resize
  • insert
  • erase
  • size
  • capacity
  • front
  • clear

以后再慢慢写吧!先更新到这~

[!CAUTION] 第一次写文章,有错误或者建议欢迎在评论区指出!

想更深入了解 vector 可以看看以下博主的文章:
C++ vector函数接口及其底层原理:http://t.csdn.cn/plaSu

2023/07 第二次更新

之前存在没写全的功能已补充,具体代码可见 github 仓库:
https://github.com/DesperateBreaker/MySTL.git

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值