☰
C数据结构(顺序表)进阶(遗忘复习跟进C++):C++17标准
2026/10/13 3:21:54 网站建设 项目流程

第一次写这么多内容,可否支持一下🙏。

单链表目录

  • 前言
  • 流程规划
  • 初始编码
    • 1. 第一个问题:`explicit` 是什么
    • 2. `std::size_t` 是什么意思
    • 3. 为什么不把 4 直接写在 `capacity_` 之后
    • 4. `data_ = std::make_unique<int[]>(capacity_);` 是什么意思
      • 4.1 没有写 `new`,怎么开辟内存?
  • 初始状态接口
    • 1.noexcept什么意思
    • const
  • 访问元素函数
    • 允许修改
      • 1.int& at引用问题
      • 2.超出异常out_of_range
    • 只允许读取
  • 动态数组扩容
    • 1.data_[i]难道不是unique_ptr吗,为什么可以for循环复制
    • 2.data_ = std::move(new_data);什么意思
  • 尾部插入与自动扩容
    • 为什么new_capacity = capacity_ * 2;
  • 插入删除查找
    • 1.插入
      • 循环应该从 size_ 开始,而不是 capacity_
    • 删除
    • 查找
  • 完整代码呈现

前言

有没有像我一样,在学完 C 语言后就学习数据结构的同学?之前我一直以为我的数据结构已经可以了,但现在学完了 C++ 和很多知识后,不少内容都有些疏漏和遗忘,但又不想重新学习一遍数据结构。那么我们不妨大胆一点,直接跟进 C++ 和操作系统等知识,以一个未来工作的视角去看待数据结构的编写。
因此,这篇博客中,数据结构的很多基础内容讲的比较少

这篇文章或许会很长,但它会一步一步地把所有顺序表以及新增的 C++ 内容全部讲清楚。这可是我一步一步追着 AI 刨根问底的结果。或许各位大佬都比我厉害,不少知识可能赘述过多,还请谅解,毕竟,这也是对我自己的一次自测。

本篇内容多以代码实现及个人理解为主,理论知识只会偶尔涉及。

流程规划

我们都清楚,顺序表的主要形式是:

写代码之前,首先明确我们顺序表的整体结构、接口数量等,且创建一个cpp文件。

接口功能
push_back尾部插入
insert指定位置插入
erase删除指定位置元素
find查找元素
at访问指定位置的元素
size返回当前元素数量
capacity返回当前容量
reserve扩大容量,减少重复分配

依据这个表格,我们一步一步进行编码。

初始编码

简单过一下代码即可,暂时不用全部理解。

#include<iostream>#include<cstddef>#include<memory>classSeqList{private:// 当前元素数量std::size_t size_;// 当前数组容量std::size_t capacity_;// 管理动态数组std::unique_ptr<int[]>data_;public:// 构造函数:创建一个指定初始容量的顺序表explicitSeqList(std::size_t initial_capacity=4):size_(0),capacity_(initial_capacity){if(capacity_>0){data_=std::make_unique<int[]>(capacity_);}}};

1. 第一个问题:explicit是什么

这在基础课程中几乎不涉及。

这是一个 C++ 关键字,主要用来禁止编译器擅自进行隐式类型转换。

为方便理解,我们先以一个简单代码举例。

在如下代码中,我们设计了一个Student类,其中Student(int age)是一个构造函数。调用它时需要给age一个值。见main函数,可知Student s = 18;把Student类型的变量s当成普通整数去赋值了。这其实不是标准写法,但编译器明白你想干什么,于是用隐式转换的方式,在系统内部转换为Student s = Student(18);,于是成功运行。

#include<iostream>classStudent{public:Student(intage){std::cout<<"年龄:"<<age<<std::endl;}};intmain(){Student s=18;}

而如果我们加上explicit,Student s = 18;就不允许了,因为它禁止隐式转换。

#include<iostream>classStudent{public:explicitStudent(intage){std::cout<<"年龄:"<<age<<std::endl;}};intmain(){// Student s = 18; // 编译错误Students(18);// 正确Student t{18};// 正确}

2.std::size_t是什么意思

std::size_t在这里提一嘴,此类型包含在#include <cstddef>里面,通常表示非负整数。

后面的initial_capacity是一个变量名,表示初始容量。

3. 为什么不把 4 直接写在capacity_之后

// 写法一explicitSeqList(std::size_t initial_capacity=4):size_(0),capacity_(initial_capacity)
// 写法二explicitSeqList():size_(0),capacity_(4)

完全可以!而且如果你只希望顺序表的默认容量固定为 4,这种写法反而更直观。

但是,这两种写法也有区别:

创建方式写法一写法二
SeqList list;容量为 4容量为 4
SeqList list(10);容量为 10编译错误
SeqList list(100);容量为 100编译错误

因此,为了灵活更改,我们一般不要把容量写死。

4.data_ = std::make_unique<int[]>(capacity_);是什么意思

需要#include <memory>。

std::make_unique出现于 C++17。

动态申请一块能够存放capacity_个int元素的数组,并让智能指针data_管理这块内存。

4.1 没有写new,怎么开辟内存?

这是现代 C++ 与所学基础 C++ 的区别。

int*data=newint[4];

这种写法,大家肯定熟悉。

而我们今天不这么写了。

data_=std::make_unique<int[]>(4);

虽然没有直接写new,但std::make_unique在内部负责完成动态内存分配,并把分配得到的资源交给智能指针管理。

而且还有一个好处:make_unique出来的数组元素会被初始化为 0,而new一般不会。

初始状态接口

// 获取当前元素数量std::size_tsize()constnoexcept{returnsize_;}// 获取当前容量std::size_tcapacity()constnoexcept{returncapacity_;}// 判断顺序表是否为空boolempty()constnoexcept{returnsize_==0;}

此段代码主要实现,获取元素数量,获取容量,判断是否为空。
元素数量与容量并不相同,可以向上翻看图片内容

1.noexcept什么意思

noexcept是一个来自C++11的关键字
一般放在函数声明之后。
作用是阻止异常从函数里抛出去。
这里与throw的作用做一个对比。

关键字作用通俗理解
throw抛出异常把问题交出去
noexcept声明函数不会让异常跑出去保证问题不会以异常的形式传出去

为了更好理解,这里给出一个例子(在讲noexcept之前,我们先弄清楚throw)

intdivide(inta,intb){if(b==0){throw1;}returna/b;}

该实例是一个除法问题,其中除数不能为0,因此当输入参数b为0时,明显有异常,throw把异常抛出函数,throw 1 会让程序跳过 divide() 中剩下的代码,去寻找能够处理这个异常的 catch。
如果找到,就进入对应的 catch;如果没有找到,最终会调用std::terminate(),一般会终止。

try//尝试执行代码,并在指定位置接住异常、处理问题{intresult=divide(10,0);std::cout<<result;}catch(inte){std::cout<<"除数不能为 0";}

程序不必因此整体退出,还可以继续执行后面的代码。
所以,抛出异常不是为了让程序报错退出,而是为了把无法正常处理的问题交给其他代码处理。
这样我们清楚了抛出异常的过程
接下来就可以看看noexcept了
回到我们原本的代码

std::size_tsize()constnoexcept{returnsize_;}

它的作用只是返回顺序表当前有多少个元素。
没有必要抛出异常,于是你可以写 noexcept,明确告诉 C++
这个函数保证不会让异常跑到函数外面。
对于 size() 这样的简单查询函数,我们可以保证其执行过程中不会抛出异常。调用者因此可以依赖这一异常保证,编译器也可能利用 noexcept 信息进行优化。
但如果你写了noexcept,又让异常跑了出来,C++ 就会调用std::terminate(),通常直接使程序终止

const

为什么这里要加const,这是为了告诉C++,此段函数不会对顺序表做出修改,只读取顺序表数据。
这算是一道防线,如果没有const。

std::size_tsize()noexcept{size_=0;// 编译器允许returnsize_;}

因此,加上const限制,防止意外修改。

访问元素函数

允许修改

#include<stdexcept>//添加头文件//使用 std::out_of_range 需要包含头文件:// 访问元素:只允许读取constint&at(std::size_t index)const{if(index>=size_){throwstd::out_of_range("index out of range");}returndata_[index];}

1.int& at引用问题

以引用的方式传入顺序表数据,不需要复制原件,直接对顺序表本身做修改。

2.超出异常out_of_range

std::out_of_range是C++标准库提供的一种异常类型,用来表示访问的数据超出了允许的范围。

至于为什么要用out_of_range,正常情况下

throw1;// 抛出一个整数

别人不知道1是什么,因此有局限性。
而

throwstd::out_of_range("Index out of range");

一眼就可以知道是什么异常,更重要的是调用者可以专门捕获这种异常。

try{intvalue=list.get(5);}catch(conststd::out_of_range&e){std::cout<<e.what();//e.what():获取异常附带的错误描述。}

const std::out_of_range& e用引用接收这个异常对象。

只允许读取

// 访问元素:只允许读取constint&at(std::size_t index)const{if(index>=size_){throwstd::out_of_range("index out of range");}returndata_[index];}

此段函数与上面的相似,不再做细致解析

动态数组扩容

#include<utility>// 扩大容量voidreserve(std::size_t new_capacity){if(new_capacity<=capacity_){return;}autonew_data=std::make_unique<int[]>(new_capacity);for(std::size_t i=0;i<size_;++i){new_data[i]=data_[i];}data_=std::move(new_data);// move需要添加 #include <utility>capacity_=new_capacity;}

扩容结构,用户传进来一个新容量,判断,如果该容量小于原容量,直接撤掉。如果大于原容量,就建立一个新的new_data并为其开辟空间,方法在初始编码中有涉及。
然后通过for遍历容器,并将原本data里面的数据调整到new_data里面

1.data_[i]难道不是unique_ptr吗,为什么可以for循环复制

data_是管理整个数组的智能指针,而 data_[i] 是数组中第 i+1 个元素。它们不是同一个东西

写法含义类型
data_管理数组的智能指针std::unique_ptr<int[]>
data_[0]第一个元素int
data_[1]第二个元素int

正因此,可以通过for循环去复制元素。

2.data_ = std::move(new_data);什么意思

让new_data接管data的内存。

std::unique_ptr<int[]>data_;

它负责管理顺序表当前使用的数组。
现在可以把它们理解成:
data_ 原来的数组管理者。
new_data 新数组的管理者。
通过move,转接管理权。
而由于data_和new_data都是unique_ptr
因此不可复制
即

data_=new_data;// 错误:试图复制data_=std::move(new_data);// 正确:转移管理权

尾部插入与自动扩容

// 在末尾添加元素voidpush_back(intvalue){if(size_==capacity_){std::size_t new_capacity=1;if(capacity_>0){new_capacity=capacity_*2;}reserve(new_capacity);}data_[size_]=value;++size_;}

这段代码逻辑比较简单,主要就是检查容量,扩展容量,更新元素
size_ 表示当前元素数量,capacity_ 表示当前容量。
当两者相等时,说明数组已经装满,不能直接添加新元素,需要先扩容。

为什么new_capacity = capacity_ * 2;

这里先把新容量设为 1。
如果当前容量大于 0,就把容量扩大为原来的两倍。
例如:当前容量为 0,新容量设为 1。当前容量为 4,新容量变为 8。当前容量为 8,新容量变为 16。
这样做可以避免每添加一个元素就重新申请数组,减少频繁扩容带来的开销。

插入删除查找

1.插入

voidinsert(std::size_t index,intvalue){if(index>size_){throwstd::out_of_range("index out of range");}if(size_==capacity_){std::size_t new_capacity=1;if(capacity_>0){new_capacity=capacity_*2;}reserve(new_capacity);}for(std::size_t i=size_;i>index;i--){data_[i]=data_[i-1];}data_[index]=value;size_++;}

有关插入涉及更多的是数据结构基础与函数调用。
我们设计两个传入参数,分别是插入位置和插入元素值。
函数的最开始是要检查输入是否异常,如果index大于size_直接抛出越界异常即可。
如果size_等于capacity_,我们需要扩大容量,方法与push_back函数类似,不再重复。

把异常情况排除之后,我们就可以考虑真正的插入操作了。
从后往前遍历,直到插入位置,这期间的元素都要向后移动一位,腾出插入位置。

循环应该从 size_ 开始,而不是 capacity_

有没有过这种想法,如果容量为10,大小为4,那么如果我要在位置7插入数据的话,难道就不对吗
这里恰好涉及顺序表的一个核心概念:容量 capacity_ 和元素数量 size_ 是两个不同的东西,但合法插入位置由 size_ 决定,而不是由 capacity_ 决定。
capacity_ = 10
size_ = 4

下标: 0 1 2 3. 4 5 6 7 8 9
数据: 10 20 30 40 · · · · · ·
└──── 有效元素 ────┘
合法位置为0,1,2,3,4
4为末位,因此也是合法的
如果想在7位置插入数据呢。
顺序表的规则是在已有的序列中插入数据,而隔位插入是非法的,如果你确实想让下标 7 存储 ,那是另一种接口设计,需要明确规定中间空位如何处理。

删除

voiderase(std::size_t index){if(index>=size_){throwstd::out_of_range("index out of range");}for(std::size_t i=index+1;i<size_;i++){data_[i-1]=data_[i];}size_--;}

你的底层数组由std::unique_ptr<int[]>管理。

这里的data_[index]是一个 int 元素,不是通过 new 单独创建的对象。
因此:既不符合删除单个元素的方式,也无法实现需要的效果。顺序表删除元素,本质上是移动元素并减少 size_,不需要手动释放单个元素的内存。

查找

intfind(intvalue)constnoexcept{for(std::size_t i=0;i<size_;i++){if(data_[i]==value){returni;}}return-1;}

完整代码呈现

#include<utility>#include<cstddef>#include<iostream>#include<memory>#include<stdexcept>classSeqList{private:// 当前元素数量std::size_t size_;// 当前数组容量std::size_t capacity_;// 管理动态数组std::unique_ptr<int[]>data_;public:// 构造函数explicitSeqList(std::size_t initial_capacity=4):size_(0),capacity_(initial_capacity){if(capacity_>0){data_=std::make_unique<int[]>(capacity_);}}// 获取当前元素数量std::size_tsize()constnoexcept{returnsize_;}// 获取当前容量std::size_tcapacity()constnoexcept{returncapacity_;}// 判断顺序表是否为空boolempty()constnoexcept{returnsize_==0;}// 访问元素:允许修改int&at(std::size_t index){if(index>=size_){throwstd::out_of_range("index out of range");}returndata_[index];}// 访问元素:只允许读取constint&at(std::size_t index)const{if(index>=size_){throwstd::out_of_range("index out of range");}returndata_[index];}// 扩大容量voidreserve(std::size_t new_capacity){if(new_capacity<=capacity_){return;}autonew_data=std::make_unique<int[]>(new_capacity);for(std::size_t i=0;i<size_;++i){new_data[i]=data_[i];}data_=std::move(new_data);capacity_=new_capacity;}// 在末尾添加元素voidpush_back(intvalue){if(size_==capacity_){std::size_t new_capacity=1;if(capacity_>0){new_capacity=capacity_*2;}reserve(new_capacity);}data_[size_]=value;++size_;}// 在指定位置插入元素voidinsert(std::size_t index,intvalue){if(index>size_){throwstd::out_of_range("index out of range");}if(size_==capacity_){std::size_t new_capacity=1;if(capacity_>0){new_capacity=capacity_*2;}reserve(new_capacity);}for(std::size_t i=size_;i>index;i--){data_[i]=data_[i-1];}data_[index]=value;size_++;}// 删除指定位置的元素voiderase(std::size_t index){if(index>=size_){throwstd::out_of_range("index out of range");}for(std::size_t i=index+1;i<size_;i++){data_[i-1]=data_[i];}size_--;}// 查找元素,返回下标;不存在时返回 -1intfind(intvalue)constnoexcept{for(std::size_t i=0;i<size_;i++){if(data_[i]==value){returni;}}return-1;}// 遍历输出顺序表voidprint()const{for(std::size_t i=0;i<size_;++i){std::cout<<data_[i]<<" ";}std::cout<<'\n';}};intmain(){SeqList list;list.push_back(10);list.push_back(20);list.push_back(30);list.print();list.insert(2,88);list.print();list.erase(3);list.print();list.find(10);//测试自动扩容list.push_back(10);list.push_back(20);list.push_back(30);// 修改元素list.at(1)=25;list.print();// 测试越界访问boolexception_caught=false;// 测试越界访问try{list.at(list.size());std::cout<<"Error: expected an exception\n";}catch(conststd::out_of_range&){std::cout<<"Out-of-range test passed\n";}list.print();std::cout<<"Basic tests passed.\n";}

如果这篇内容对你有帮助,欢迎点赞支持一下 🙏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询