C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现

时间:2022-08-27 15:15:32

前面介绍的模板有关知识大部分都是用顺序表来举例的,现在我们就专门用模板来实现顺序表,其中的很多操作都和之前没有多大区别,只是有几个比较重要的知识点需要做专门的详解。

 #pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
using namespace std; template <class T>
class Vector
{
public:
Vector() //构造函数
:_array(NULL)
,size()
,capacity()
{}
Vector(const Vector<T>& v) //拷贝构造函数
{
_array = (T*)malloc(v._array, sizeof(T)*size); //注:问题一
memcpy(v._array, _array, sizeof(T)*size);
size = v.size;
capacity = v.size;
}

问题一实质同下面的问题3,后面再做详细分析。

 问题2:
1 Vector<T>& operator=(const Vector<T>& v) { //赋值运算符重载
if (this != &v) {
Vector<T> tmp(v);
swap(tmp);
}
6 return *this;
6 }
void swap(Vector<T>& v) {
std::swap(_array, v._array);
std::swap(size, v.size);
std::swap(capacity, v.capacity);
}
11 list1 = list2;

这里很有必要详解实现上面赋值运算符重载的现代写法的实现原理 :首先看上面代码(list1 = list2;),赋值运算符重载中的局部变量tmp是由v即list2拷贝构造而来,函数体内通过swap函数将this指针指向的list1与tmp发生了交换,即list1与list2发生了交换,局部变量tmp现在指向之前list1指向的地址,而list1指向tmp原先指向的地址,也就是list1被赋值成了list2,而局部变量tmp一出函数就会自动销毁,就会调用它的析构函数,会使得它指向的内存释放,从而实现list1与list2的交换。图解如下:C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现

注:swap函数也不能乱用,一般来说,swap函数适用于内置类型,下面看这句代码

 swap(*this,l);   //直接交换两个对象岂不更好?

这段代码会出现什么情况呢?转到定义处,来看一下swap函数内部是如何实现的。C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现我们用的swap函数是这个模板实例化来的,它的倒数第二行,和倒数第三行都调用了赋值运算符重载函数,这样就造成递归调用,一直生成栈帧,直至出现栈溢出问题。为了解决这种问题,一般我们使用自己写的swap函数来实现需要的功能。

 ~Vector() {   //析构函数
if (_array) {
delete[] _array;
_array = NULL;
size = ;
capacity = ;
}
}
void PushBack(const T& x) { //尾插
_CheckCapacity();
_array[size] = x;
++size;
}
void PopBack() { //尾删
if(!Empty())
size--;
}
void PushFront(const T& x) { //头 插
_CheckCapacity();
for (size_t i = size; i > ; i--) {
_array[i] = _array[i - ];
}
_array[] = x;
++size;
}
void PopFront() { //头删
if(!Empty()){
for (size_t i = ; i < size; i++) {
_array[i] = _array[i + ];
}
size--;
}
}
void Insert(size_t pos, const T& x) { //任意位置插入
_CheckCapacity();
for (size_t i = size; i > pos; i--) {
_array[i] = _array[i - ];
}
_array[pos] = x;
++size;
}
void Erase(size_t pos) { //任意位置删除
if (!Empty()) {
if (pos >= size)
return;
else {
for (size_t i = pos; i < size; i++) {
_array[i] = _array[i + ];
}
size--;
}
}
}
size_t Size()const { //返回顺序表的数据个数
return size;
}
size_t Capacity()const { //返回顺序表的容量
return capacity;
}
T& Top() { //取顺序表头值
return _array[];
}
bool Empty() { //清空顺序表
return size == ;
}
void Print() { //打印顺序表
for (size_t i = ; i < size; i++)
{
cout << _array[i] << " ";
}
cout << endl;
}
private:
void CheckCapacity() //注:问题三
{
if (_size > _capacity)
{
_capacity = _capacity * + ;
_a = (T*)realloc(_a, (_capacity) * sizeof(T));
}
}
82

83      T* _array;
  84     size_t size;
  85     size_t capacity;
  86     };

来看上面代码,这些代码在int, char等一些内置类型下是可以被顺利执行的,但是一旦换成string类型或其他自定义类型就会出现问题,在执行插入操作时,代码就会崩掉,这是什么问题呢?其实很容易看出问题出在新开辟空间时,即代码中标注的问题一与问题三(见代码中标注),在拷贝构造函数Vector(const Vector<T>& v)与扩容函数CheckCapacity()函数中,我们开辟新空间用的是malloc与realloc函数,这里有个问题,malloc和realloc函数只负责开空间,但不初始化,所以在插入操作的赋值语句时挂了,调试你会发现它的_array就是NULL,所以就会出现问题。所以我们用new[]来开辟空间,用delete[]来销毁空间,其实它与malloc和realloc函数最大的区别是new[]开辟新空间时顺便会调用构造函数初始化对象,这是这个问题的重点所在。我们将这个问题一改用new[]和delete[]分别代替malloc/realloc和free。改完之后的代码如下:

        Vector(const Vector<T>& v)   //拷贝构造函数
: _array(new T[sizeof(T) * v.capacity])
, size(v.size)
, capacity(v.capacity)
{
memcpy(_array, v._array, sizeof(T)*size);
}
void _CheckCapacity() { //扩容函数
if (size == capacity) {
size_t newCapacity = * capacity + ;
T* tmp = new T[newCapacity];
if (_array) {
memcpy(tmp._array, _array, sizeof(T)*size);
}
delete[] _array;
_array = tmp;
capacity = newCapacity;
}
}

上面这段代码在int, char等一些内置类型下是可以被顺利执行的,而且貌似string类型或其他自定义类型下也没有问题。而其实这里面还存在另外一个更重要的问题,就是标题说到的更深层次的深浅拷贝的问题。当我用下面这段代码测试的时候,会有乱码出现

 void Test1() {
Vector<string>v1;
v1.PushBack("111");
v1.PushBack("222222222222222222222222222222222222222");
v1.PushBack("");
6 v1.PushBask("444")
v1.Print();
}

输的结果不尽如任意:C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现但是将第二行测试程序改为v1.PushBack(“22222222222”);又会正常输出,或者将v1.PushBaack(“444”)这句给屏蔽屏蔽掉,同样会正常输出。所以有理由相信在一定的范围内,或某种情况下,可以正常输出,超过一定范围就会出现异常。这里详解更深层次的深浅拷贝问题。

这里我们从String类的内部成员说起,其实string里面由下面四部分组成,这是一种以空间换时间的优化,如果String里保存的字符串长度小于15(实际可存16为16这里考虑了‘\0’),它就会将字符串保存于它自带的空间_Buf,而大于等于15时,他就会重新开辟一份空间存放这个字符串,并使用_Pre指向这块内存空间。

 class string
{
string* _Buf[];
string* _Ptr;
size_t _Mysize;
size_t _Myres;
};

可以在vs2008上面验证一下(调试窗口就可以看),我用的vs2015不能展示出来。如此,我们便可以知道上面的代码在拷贝时出现了问题,下面我用图示来解释上面的测试代码测出的问题。 C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现当我们测试的代码往String里存放的字符串长度值小于15时,它保存在字符数组_Buf[16]中,memcpy()函数可以正常拷贝,所以正常输出,当其值大于15时,将会开辟足够的新空间以存放字符串,并使_Pre指向新空间的起始地址,使用memcpy()函数拷贝时仅仅只拷贝了数据,即值拷贝,那么两个指针指向同一块地址,当原来那个String析构掉,开辟的空间销毁时,另一个指针任然指向原来的地址,那么它向后访问到的就是随机值,当析构拷贝过来的String时,又会调用析构函数对那块已经被析构的空间进行析构,所以程序最终崩溃。为了解决这个问题我们再次改进

    Vector(const Vector<T>& v)    //拷贝构造函数
: _array(new T[sizeof(T) * v.capacity])
, size(v.size)
, capacity(v.capacity)
{
for (size_t i = ; i < size; i++) {
_array[i] = v._array[i];
}
}
void _CheckCapacity() { //扩容函数
if (size == capacity) {
size_t newCapacity = * capacity + ;
T* tmp = new T[newCapacity];
if (_array) {
for (size_t i = ; i < size; i++) {
tmp[i] = _array[i];
}
}
delete[] _array;
_array = tmp;
capacity = newCapacity;
}
}

其中调用了赋值运算符的重载,即就是实现深拷贝。

以下是测试程序

 void Test1() {     //深拷贝测试程序
Vector<string>v1;
v1.PushBack("");
v1.PushBack("2222222222222222222222222222222222222222222222");
v1.PushBack("");
v1.PushBack("")
v1.Print();
}
void Test2()
{
Vector<int> list1;
list1.PushBack(); //尾插
list1.PushBack();
list1.PushBack();
list1.PushBack();
list1.PushBack();
list1.Print();
list1.PopBack(); //尾删
list1.PopBack();
list1.PopBack();
list1.PopBack();
list1.Print();
list1.PushFront(); //头插
list1.PushFront();
list1.PushFront();
list1.Print();
list1.PushFront();
list1.Print();
list1.PopFront(); //头删
list1.PopFront();
list1.PopFront();
list1.Print();
Vector<int> list2(list1); //拷贝构造函数测试
list2.Print();
list1.Insert(, ); //任意位置插入
list1.Print();
list1 = list2; //赋值运算符
list1.Print();
list1.Erase(); //任意位置删除
list1.Print();
}
int main()
{
Test1();
Test2();
getchar();
return ;
}

下面是输出结果:C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现

基于顺序表的简单栈的实现

 template<class T,class Container = Vector<T>>
class Stack
{
public:
void Push(const T& x) { //入栈
Vector<T>::PushBack(x);
}
void Pop() { //出栈
Vector<T>::PopBack();
}
const T& Top() { //取栈顶元素值
return Vector<T>::Top();
}
const size_t Size() { //返回栈中元素个数
return Vector<T>::Size();
}
bool Empty() { //判空栈
return Vector<T>::Empty();
}
private:
Container _con;
};

C++模板实现动态顺序表(更深层次的深浅拷贝)与基于顺序表的简单栈的实现的更多相关文章

  1. 聊聊elasticsearch7&period;8的模板和动态映射

    最近想写一篇es的索引的一个设计,由于设计的东西特别多,当然,elasticsearch的模板和动态映射也是其中的一个设计点,所以干脆先来聊聊索引的模板和动态映射,模板,听这个名字就相当于一些公共可用 ...

  2. Elasticsearch7&period;X 入门学习第八课笔记-----索引模板和动态模板

    原文:Elasticsearch7.X 入门学习第八课笔记-----索引模板和动态模板 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接: ...

  3. 用Myisamchk让MySQL数据表更健康

    用Myisamchk让MySQL数据表更健康 2011-03-15 09:15 水太深 ITPUB 字号:T | T 为了让MySQL数据库中的数据表“更健康”,就需要对其进行定期体检.在这里笔者推荐 ...

  4. Hive分区表新增字段及修改表名,列名,列注释,表注释,增加列,调整列顺序,属性名等操作

    一.Hive分区表新增字段 参考博客:https://blog.csdn.net/yeweiouyang/article/details/44851459 二.Hive修改表名,列名,列注释,表注释, ...

  5. LG4719 【模板】动态dp 及 LG4751 动态dp【加强版】

    题意 题目描述 给定一棵\(n\)个点的树,点带点权. 有\(m\)次操作,每次操作给定\(x,y\),表示修改点\(x\)的权值为\(y\). 你需要在每次操作之后求出这棵树的最大权独立集的权值大小 ...

  6. java方法调用之动态调用多态(重写override)的实现原理——方法表(三)

    上两篇篇博文讨论了java的重载(overload)与重写(override).静态分派与动态分派.这篇博文讨论下动态分派的实现方法,即多态override的实现原理. java方法调用之重载.重写的 ...

  7. 顺序表添加与删除元素以及 php实现顺序表实例

    对顺序表的操作,添加与删除元素. 增加元素 如下图所示  对顺序列表 Li [1328,693,2529,254]  添加一个元素 111 ,有三种方式: a)尾部端插入元素,时间复杂度O(1);  ...

  8. 基于JS正则实现模板数据动态渲染

    最近业务上需要动态渲染模板数据: 一.业务需求: 1.前端后端定义好模板以及变量名,根据打印机类型转换成对应sdk需要的标签模板,保存数据库 2.订单数据是前端根据支付结果获取的,最终渲染完的数据模板 ...

  9. 洛谷3834 hdu2665主席树模板,动态查询区间第k小

    题目链接:https://www.luogu.com.cn/problem/P3834 对于区间查询第k小的问题,在区间数量达到5e5的时候是难以用朴素数据结构实现的,这时候主席树就应运而生了,主席树 ...

随机推荐

  1. Python &ast;与&ast;&ast; 参数问题

    问题:     Python的函数定义中有两种特殊的情况,即出现*,**的形式.     如:def myfun1(username, *keys)或def myfun2(username, **ke ...

  2. paip&period;spring 获取bean getBean 没有beanid的情况下

    paip.spring 获取bean  getBean 没有beanid的情况下 spring能自动扫描带有注解的bean文件.. 作者Attilax  艾龙,  EMAIL:1466519819@q ...

  3. contentProvider 内容提供者

    http://blog.csdn.net/woshixuye/article/details/8280879 实例代码当数据需要在应用程序间共享时,我们就可以利用ContentProvider为数据定 ...

  4. u盘安装ubuntu10&period;04 server&period;txt

    10.04 先将 ubuntu server 的 iso 放到优盘上,然后在提示无法找到光驱时,按 alt+f2 打开一个新的 console 窗口,将 iso mount 上,具体操作如下: ls ...

  5. C语言高效位操作

    思考: 1. 如何将一个数据中的多个不连续位清位? 1. 如何将一个数据中的多个不连续位置位? 1. 如何反转一个数据中的多个不连续位(1->0, 0->1)? 基础知识:C 语言位操作 ...

  6. WebStorm设置左侧菜单栏背景和字体设置

    WebStorm左侧菜单栏 webstorm是一款前端IDE利器,个人感觉黑色的背景比较炫酷,刚开始从网上下载的主题只能修改编辑窗口的背景色,经过查询资料终于把左边菜单栏的背景色也修改了. 第一步:点 ...

  7. dock使用方法

    Docker 是一个开源项目,为开发者和系统管理员提供了一个开放的平台,在任何地方通过打包和运行应用程序作为一个轻量级的容器.Docker 在软件容器内自动部署应用程序.Docker 最开始由 Sol ...

  8. 第九篇:Map&sol;Reduce 工作机制分析 - 作业的执行流程

    前言 从运行我们的 Map/Reduce 程序,到结果的提交,Hadoop 平台其实做了很多事情. 那么 Hadoop 平台到底做了什么事情,让 Map/Reduce 程序可以如此 "轻易& ...

  9. Linux内存描述之高端内存--Linux内存管理&lpar;五&rpar;

    1. 内核空间和用户空间 过去,CPU的地址总线只有32位, 32的地址总线无论是从逻辑上还是从物理上都只能描述4G的地址空间(232=4Gbit),在物理上理论上最多拥有4G内存(除了IO地址空间, ...

  10. 3&period;让linux 增加 wget 命令

    Wget主要用于下载文件,在安装软件时会经常用到   直接执行命令 : sudo yum -y install wget   就可以使用wget了