0、前言:
- 对于c++的学习,基础阶段已经过去了,打开c++灵活运用大门的钥匙就是c++中的STL和数据结构的知识,学习这部分知识,首先要对c++中的一些库有一定的认知,知道如何用类实现;要达到这个水平,写c++的类就是基本要求了,所以在这部分学习阶段,如果遇到前面遗漏的知识,我会继续补充在"C++查缺补漏"的相关文章。
- 通过对STL库学习,要掌握其中的一些重要函数(会用、明理、能修改)。
- 数据结构的学习会穿插学习算法,因为数据结构和算法本来就不分家,因此学习记录一些算法就至关重要了,同时刷题也就必不可少了。
1、STL:(Standard Template Library,标准模板库)
- 是一套功能强大的模板类和函数集合,提供了通用的数据结构 和算法 ,旨在提高开发效率和代码复用性。STL 的核心思想是 "泛型编程",通过模板实现了数据结构与算法的分离,使其可以适配任意数据类型。
- STL 的设计理念是 "泛型编程",它大大提高了 C++ 代码的复用性和开发效率,是 C++ 程序员必须掌握的核心知识之一。
1.1、STL概览:
-
1、容器:用于存储和管理数据的模板类,【通俗理解:"数据的仓库",专门用来存放数据,就像一个装了转动按钮的动态书架】
- 序列容器:如vector(动态数组)【√】、list(双向链表)【√】、deque(双端队列)
- 关联容器:如set(有序集合)、map(键值对映射)、unordered_set(哈希集合)
- 容器适配器:对现有容器做的二次封装实现的数据结构:
- 如stack(栈)【√】、queue(队列)【√】、priority_queue(优先队列)
-
2、算法【√】:一系列用于操作容器元素的模板函数,如排序(sort)、查找(find)、复制(copy)、交换(swap)等,支持对不同容器进行统一操作。 【通俗理解:"操作数据的工具",专门对容器里的数据做处理(如排序、查找、复制),就像一个使用书架的说明手册】
-
3、迭代器【√】:连接容器和算法的桥梁,提供了类似指针的接口,让算法可以独立于具体容器类型访问元素(如begin()返回容器起始迭代器,end()返回结束迭代器)。 【通俗理解:书架上的转动按钮,一旦使用就可以遍历书架上的书】
-
4、函数对象(仿函数):重载了()运算符的类对象,可作为算法的参数(如less、greater用于指定排序方式),也可以理解为算法的可选规则。 【通俗理解:书架使用说明书的补丁】
-
5、适配器:"组件的'转换器'",把一个组件的接口改成另一个组件需要的样子,让不兼容的组件能一起工作。 【通俗理解:书架的配件,可以根据场景修改书架的结构】
-
6、空间配置器:"容器的'后勤管家'",专门负责给容器分配 / 释放内存,不用容器自己操心内存管理。 【通俗理解:像 "给抽屉 / 书架'找材料、搭框架'的工人"------ 容器要存数据,不用自己去申请内存,而是告诉空间配置器 "我要存 10 个 int",配置器就会分配合适的内存,容器用完后再由配置器回收,避免内存浪费或泄漏。平时用 STL 时几乎不用直接操作它,容器会自动调用。】
1.2、STL中六大组件之间的关系:
- 用 "整理书房" 的场景总结,核心逻辑是 "分工 + 协作":
- 基础支撑:空间配置器(管家)先给容器(抽屉 / 书架)分配内存,容器才能存放数据;
- 核心协作:迭代器(拉手 / 书签)连接容器和算法 ------ 算法(整理方法)通过迭代器,不用关心容器是抽屉还是书架,就能统一遍历数据;
- 灵活扩展:仿函数(额外规则)给算法加自定义行为,适配器(转换器)给组件改接口(比如把抽屉改成栈),让整个工具组更灵活。
- 简化关系链:空间配置器 → 给容器分配内存 → 容器存数据 → 迭代器连接容器与算法 → 算法用迭代器操作数据(仿函数定义算法规则,适配器适配组件接口)
2、STL_迭代器:
2.1、迭代器基本概念:
- 迭代器 本质就是容器的指针,他可以指向容器中的元素;
- 所有迭代器的类名都是iterator;
- 只要容器有迭代器,那么容器就一定有两个成员函数,叫begin和end ,这两个函数会返回容器的第一个元素的迭代器和最后一个元素之后那个位置的迭代器【因为STL中容器都是前闭后开区间】
2.2、 迭代器的分类:
- 普通分类:
1、iterator:普通迭代器
2、const_iterator:只读迭代器
3、reverse_iteraotr:反向迭代器
4、const_reverse_iterator:只读反向迭代器
- 一般而言,连续容器的迭代器会完美模拟指针的一切行为:加减一个偏移量、相减、加加减减、比较大小、解引用(*)、中括号、箭头(对于结构体类型的或指向类对象的指针)。
- 一类迭代器和二类迭代器:通常在连续容器中存在的迭代器可以完美模拟指针的所有操作,包括:加加减减、加减一个数字、相减、比较、解引用、箭头、中括号这类迭代器叫做一类迭代器 ,用于string、vector、array、 deque 等连续容器;但是,如果容器是非连续容器,此时元素之间的地址就不再拥有连续行了,故其迭代器支持的功能也将变少。一般只支持:加加减减、解引用、箭头、比较中的等于和不等,这类迭代器叫做二类迭代器,用于list、forward_list、map、set 等非连续容器只能向后,所以这个容器的迭代器不提供减减功能。由于 forward_list是单链表,只能向后,所以这个容器的迭代器不提供减减功能。
2.3、迭代器遍历:
- 迭代器遍历的写法:
cpp
// 普通写法
// 其中遍历者是:*it
// 通过*it就能遍历容器str中的每一个元素,下面写法调用的是容器 str 自身的 "成员函数"(属于容器类的成员)begin和end,只有容器类成员才能使用,例如:std::string、std::vector 。
for(it = str.begin(); it != str.end(); it++)
// 为了遍历一般数组和valarray容器,所以,遍历头还可以写成下面的形式,下面的遍历头对所有容器生效,而且可以遍历数组:
// 下面写法调用的是begin和end的"全局函数",会根据传入的参数类型(容器、数组等)自动适配,返回对应的迭代器(或指针,对数组而言)。★★★★★下面这种范围适用范围更广:
for(it = begin(arr); it != end(arr); it++)

- 范围for:【它是c++11新增的语法,是迭代器遍历的语法糖】
cpp
// 对于一个容器container,访问其中的元素element,通过范围for的写法如下:
for (auto element : container) { ... }
// 范围for的实现的本质如下:
for (auto it = container.begin(); it != container.end(); ++it) {
auto element = *it;
...
}
-
为什么说范围for是迭代器的语法糖?因为范围for本质上是对迭代器操作的简化封装,底层仍然依赖迭代器来实现遍历逻辑,但提供了更简洁的语法。范围 for 并没有引入新的遍历机制,只是用更友好的语法包装了迭代器的使用流程 ,省去了手动书写迭代器的样板代码。因此,它被称为 "迭代器的语法糖"------ 不改变底层逻辑,只优化语法体验。
-
自身带有迭代器的容器案例:
cpp
#include <iostream>
#include <string>
int main()
{
std::string str = "Hello, Iterator!";
// 1. 使用正向迭代器遍历并打印字符串
std::cout << "原始字符串: ";
for (std::string::iterator it = str.begin(); it != str.end(); ++it) {
std::cout << *it; // 通过解引用迭代器获取字符
}
std::cout << std::endl;
return 0;
}
- 自身不带迭代器通过for循环遍历的案例:下面的案例就是将int*取别名为iterator,然后模仿了一下迭代器是怎么实现的,尤其是主函数中通过"范围for"和普通遍历的方式运行了遍历,需要注意的是:
1、在 C++ 中,iterator 本身并不是关键字(C++ 关键字列表中没有 iterator),它只是一个约定俗成的标识符名称,通常被用来表示 "迭代器类型"。
2、当你使用 iterator 声明变量时(如 iterator it;),编译器会完全等同于 int* it; 来处理 ------ 它就是一个指向字符的指针,没有任何 "额外的迭代器魔法"。
3、C++ 编译器不会把下面代码中的 iterator "编译成特殊的迭代器类型",也不会为它添加任何迭代器专属的功能。它的行为完全由 int* 的特性决定。
cpp
#include <iostream>
// 简单容器:存储整数,无迭代器
namespace brush
{
class IntContainer {
private:
int data[5] = { -1,-1,-1,-1,-1 }; // 固定大小为5的数组
int count = 0; // 实际元素数量
public:
// 迭代器是容器对外暴露行为的核心接口,需保证外部代码可访问其类型定义。
typedef int* iterator;
// 添加元素
void add(int num) {
if (count < 5) {
data[count++] = num;
}
}
// 获取指定索引的元素
int get(int index) const {
return data[index];
}
// 获取元素数量
int size() const {
return count;
}
// begin
iterator begin()
{
return &data[0];
}
// end
iterator end()
{
int i;
for (i = 0; i < 5; i++)
{
if (data[i] == -1)
{
return data+i; // 迭代器是左闭右开
}
}
return data + i;
}
};
}
int main() {
brush::IntContainer container;
// 向容器添加元素
container.add(10);
container.add(20);
container.add(30);
// 遍历容器(通过索引访问)
for (int i = 0; i < container.size(); i++) {
std::cout << container.get(i) << " ";
}
// 输出:10 20 30
std::cout << std::endl;
// 通过迭代器遍历容器
for (auto e : container)
{
std::cout << e << " ";
}
// 输出:10 20 30
// 如果将typedef int* iterator;写在private中,尝试显式声明迭代器类型(下面代码就会编译失败)
// brush::IntContainer::iterator it = container.begin(); // 错误:iterator是私有的
// *it = 20;
return 0;
}
2.4、 可能导致迭代器失效的行为总结:
- 1、 insert导致迭代器失效:在insert时,可能需要扩容,在扩容时,旧空间会被释放,那么指向旧空间的迭代器就会成为野指针,从而全部失效;
- 2、erase导致的迭代器失效:在结点型容器中,例如list、set这样的容器,erase会导致结点被释放,所以指向该结点的迭代器会失效。
- 解决迭代器失效的方式,就是给迭代器重新赋值,返回一个有效的迭代器。
3、c++中的模版:
3.1、概念:
- C++ 中的模板(Template)是一种泛型编程工具 ,它允许你编写不依赖于具体数据类型的代码,从而实现代码的复用和通用化。简单来说,模板就像一个 "代码模板",可以用不同的数据类型 "填充" 它,生成具体的函数或类。
- 概念的引入:【模版的作用】
假设你需要实现一个 "交换两个变量" 的函数,对于 int、double、string 类型,逻辑完全相同,但传统写法需要重复定义多个函数:
cpp
// 交换int
void swap(int& a, int& b) { ... }
// 交换double
void swap(double& a, double& b) { ... }
// 交换string
void swap(string& a, string& b) { ... }
3.2、函数模版
- 函数模板(Function Template):定义一个通用函数,支持多种数据类型。
- 语法:
cpp
// 定义:
template <typename T> // 声明模板参数T(T是类型占位符)
返回值类型 函数名(参数列表) {
// 函数体(使用T作为类型)
}
// 调用
函数名(参数列表);
- 案例:
cpp
#include <iostream>
using namespace std;
// 函数模板:交换任意类型的两个变量
template <typename T> // T是类型参数,可以是int、double、string等
void my_swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int a = 10, b = 20;
my_swap(a, b); // 自动推导出T为int
cout << a << " " << b << endl; // 输出:20 10
double c = 3.14, d = 6.28;
my_swap(c, d); // 自动推导出T为double
cout << c << " " << d << endl; // 输出:6.28 3.14
string s1 = "hello", s2 = "world";
my_swap(s1, s2); // 自动推导出T为string
cout << s1 << " " << s2 << endl; // 输出:world hello
return 0;
}
- 编译器会根据传入的参数类型,自动生成对应类型的函数(称为 "模板实例化")。
3.3、类模版
- 定义一个通用类,类中的成员变量、成员函数可以使用模板参数指定类型。
- 语法
cpp
// 定义:
template <typename T> // 声明模板参数
class 类名 {
// 类成员(可以使用T作为类型)
};
// 调用:
类名<具体类型> 对象名(类属性参数);
- 案例:
cpp
#include <iostream>
using namespace std;
// 类模板:通用动态数组
template <typename T>
class MyArray {
private:
T* data; // 存储任意类型的数组
int size;
public:
// 构造函数
MyArray(int n) : size(n) {
data = new T[size]; // 分配T类型的数组
}
// 设置元素
void set(int index, T value) {
if (index >= 0 && index < size) {
data[index] = value;
}
}
// 获取元素
T get(int index) {
if (index >= 0 && index < size) {
return data[index];
}
return T(); // 返回T类型的默认值
}
// 析构函数
~MyArray() {
delete[] data;
}
};
int main() {
// 创建存储int的数组(指定T为int)
MyArray<int> intArr(3);
intArr.set(0, 10);
intArr.set(1, 20);
cout << intArr.get(0) << " " << intArr.get(1) << endl; // 10 20
// 创建存储string的数组(指定T为string)
MyArray<string> strArr(2);
strArr.set(0, "hello");
strArr.set(1, "template");
cout << strArr.get(0) << " " << strArr.get(1) << endl; // hello template
return 0;
}
3.4、总结:
- 1、模板是 C++ 泛型编程的基础,分为函数模板和类模板。
- 2、函数模板自动推导类型,类模板需显式指定类型。
- 3、STL 容器(如 vector、map)和算法(如 sort)都是基于模板实现的,掌握模板是理解 STL 的关键。
3.5、补充1:
- 在 C++ 中,模板参数列表 可以接受任意数量的参数(理论上无上限,实际受编译器实现限制),每个参数通过逗号分隔。
- 参数类型可以是:类型参数(int、std::string等)、非类型参数(如整数、指针、枚举等)、模板模板参数(接受其他模板作为参数,用于泛型编程)
- 要求:调用模板时,实参需按声明顺序一一对应(除非使用默认参数),如下:
cpp
template<typename T, int N>
class FixedArray {};
FixedArray<double, 100> arr; // 正确:T=double, N=100
// FixedArray<100, double> arr; // 错误:顺序颠倒
- 可为类型或非类型参数提供默认值,简化调用。
cpp
template<typename T = int, int N = 10>
class DefaultArray {};
DefaultArray<> arr; // 自动使用 T=int, N=10
- 允许接受任意数量的模板参数,通过参数包(Parameter Pack)实现:
cpp
template<typename... Args>
class Tuple { ... }; // 可接受 0 到 N 个类型参数
Tuple<int, double, std::string> t; // 实例化 3 个类型参数的元组
- 总之模版的参数写法也相当灵活,很多都是一边遇到,一边学习。
3.6、补充2:
- 在 C++ 中,template <class T1, class T2>和template <typename T1, typename T2>这两种写法完全没有功能上的区别,它们是等价的。class的写法是早期写法了。
3.7、补充3:
- 在 C++ 中,类模板和函数模板都可以作为另一个类的友元,但需要注意正确的声明和语法形式。
cpp
// 将函数模版作为类的友元:
template <typename T>
void func(T x) { /* ... */ }
class MyClass {
private:
int data;
// 声明整个函数模板为友元
template <typename T>
friend void func(T x);
};
cpp
// 将类模版作为类的友元:
template <typename T>
class FriendTemplate {
// ...
};
class MyClass {
private:
int data;
// 声明整个类模板为友元
template <typename T>
friend class FriendTemplate;
// 也可以只声明特定实例为友元
friend class FriendTemplate<int>;
};
- 模版的实例是什么?模版的实例就是该模版生成的固定类型类或者函数。
4、STL_vector:
- std::vector 是 C++ 标准模板库(STL)中最常用的容器 之一,本质是动态数组 ,可以自动管理内存空间,内存连续,支持快速随机访问,是日常开发中替代原生数组的首选工具。
- 使用vector必须调用#include <vector> 头文件,且它位于 std 命名空间中
4.1、定义:
- vector是一个模版类,可存储任意类型(如 int、string、自定义类等),需指定元素类型(如 vector<int>、vector<MyClass>)。
- vecto在代码中初始化方式:
cpp
#include <vector>
using namespace std; // 或显式使用 std::vector
// ① 空vector
vector<int> v1;
// ② 初始大小为5,元素默认值为0(int的默认值)
vector<int> v2(5);
// ③ 初始大小为3,元素均为10
vector<int> v3(3, 10);
// ④ 列表初始化(C++11+),
/*
列表初始化就是花括号初始化,不局限于定义vector
还可以定义其他类型,如:int a {5}; 看到
列表初始化,要能认出来;
*/
vector<int> v4{1, 2, 3, 4};
// ⑤ 拷贝初始化(从另一个vector复制元素)
vector<int> v5(v4);
// ⑥ 从数组/迭代器范围初始化
int arr[] = {5, 6, 7};
vector<int> v6(arr, arr + 3); // 从arr[0]到arr[2],STL中一般都是左闭右开。
4.2、c++中容器分类:
-
一、顺序容器(Sequence Containers)
顺序容器按元素插入顺序存储,支持动态调整大小,包含以下6种:
- 1、std::vector
特性:动态数组,连续内存,支持快速随机访问,尾部插入/删除高效(O(1)),中间插入/删除低效(O(n))。
适用场景:需要频繁随机访问或尾部操作的场景(如数值处理、动态数组)。 - 2、std::deque
特性:双端队列,分段连续内存,支持头部和尾部高效插入/删除(O(1)),随机访问较慢(需检查内存块边界)。
适用场景:需要头尾频繁操作的场景(如队列模拟、双向缓存)。 - 3、std::list
特性:双向链表,非连续内存,支持任意位置高效插入/删除(O(1)),随机访问低效(需遍历)。
适用场景:需要频繁中间插入/删除的场景(如网络节点管理、复杂缓存算法)。 - 4、std::forward_list
特性:单向链表,仅支持单向遍历,比std::list更节省内存,但功能受限。
适用场景:需要单向链表特性的场景(如简单路径遍历)。 - 5、std::array
特性:固定大小数组,编译时确定大小,连续内存,支持随机访问,无动态扩展能力。
适用场景:已知大小的固定数据集合(如RGB颜色值、矩阵行数据)。 - 6、std::string
特性:字符序列,本质是std::vector的特化,支持字符串操作(如拼接、查找、替换)。
适用场景:字符串处理(如文本解析、用户输入)。
- 1、std::vector
-
二、关联容器(Associative Containers)
关联容器通过键(Key)排序存储元素,支持高效查找,包含以下4种:
- 1、std::set
特性:唯一键集合,基于红黑树实现,元素自动排序,插入/删除/查找效率为O(log n)。
适用场景:需要唯一且有序元素的场景(如单词统计、去重排序)。 - 2、std::multiset
特性:允许重复键的集合,其他特性与std::set相同。
适用场景:需要重复且有序元素的场景(如日志分级统计)。 - 3、std::map
- 特性:键值对映射,键唯一,基于红黑树实现,插入/删除/查找效率为O(log n)。
- 适用场景:需要键值对且键唯一的场景(如字典、配置管理)。
- 4、std::multimap
特性:允许重复键的键值对映射,其他特性与std::map相同。
适用场景:需要重复键值对的场景(如电话簿、多值配置)。
- 1、std::set
-
三、无序关联容器(Unordered Associative Containers)
无序关联容器通过哈希表实现,查找效率更高(平均O(1)),但元素无序,包含以下4种:
- 1、std::unordered_set
特性:唯一键集合,基于哈希表实现,插入/删除/查找效率平均为O(1),最坏O(n)。
适用场景:需要唯一元素且无需排序的场景(如快速去重、成员校验)。 - 2、std::unordered_multiset
特性:允许重复键的集合,其他特性与std::unordered_set相同。
适用场景:需要重复元素且无需排序的场景(如高频词统计)。 - 3、std::unordered_map
特性:键值对映射,键唯一,基于哈希表实现,插入/删除/查找效率平均为O(1)。
适用场景:需要键值对且键唯一的场景(如哈希表、缓存系统)。 - 4、std::unordered_multimap
特性:允许重复键的键值对映射,其他特性与std::unordered_map相同。
适用场景:需要重复键值对的场景(如多值哈希表、反向索引)。
- 1、std::unordered_set
-
四、容器适配器(Container Adaptors)
容器适配器通过封装其他容器提供特定接口,包含以下2种:
- 1、std::stack
特性:后进先出(LIFO)结构,默认基于std::deque实现,支持push()、pop()、top()操作。
适用场景:需要栈特性的场景(如函数调用栈、括号匹配)。 - 2、std::queue
特性:先进先出(FIFO)结构,默认基于std::deque实现,支持push()、pop()、front()、back()操作。
适用场景:需要队列特性的场景(如任务调度、广度优先搜索)。 - 3、std::priority_queue
特性:优先级队列,默认基于std::vector实现,元素按优先级排序(默认大顶堆)。
适用场景:需要优先级调度的场景(如Dijkstra算法、任务优先级管理)。
- 1、std::stack
4.3、vector中常用函数

- 容器遍历方式:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v{ 1, 2, 3, 4, 5 };
// ① 下标遍历 【数组老办法,这也能体现vector随机访问的特性】
for (int i = 0; i < v.size(); ++i) {
cout << v[i] << " ";
} // 1 2 3 4 5
// ② 迭代器遍历 【用了迭代器的begin()和end()】
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
} // 1 2 3 4 5
// ③ 范围for循环(C++11+,推荐)【本质还是第二种方式实现的,只不过换了写法】
for (int num : v) {
cout << num << " ";
} // 1 2 3 4 5
}
4.4、通过模版类模拟实现vector:【目的:掌握vector中的函数的作用】
- ★一般模版类文件 #define 的时候是以_HPP结尾,也通过.hpp命名文件
- ★不论函数模版还是类模版,都需要写在VS的头文件中,并且声明和定义需要写在一起,因为模版本身不是实际代码,编译器需要根据模版生成实例化代码,所以编译器需要看到模版的完整定义。具体原因就涉及编译原理,不做深究。
- 设计架构:
- 优化建议:在设计之初,可以把迭代器当做一个嵌套类写在这个容器模版类中,代码的可读性更高;还有就是把 << 重载写在类中,使用的时候,就要反着调用,才能正确传参进去,建议还是把它写成友元函数。
- 代码实现:
cpp
// 这是vs头文件中.hpp文件的内容
#ifndef BRUSH_VECTOR_HPP_
#define BRUSH_VECTOR_HPP_
#include <iostream> // <iostream>)可能在内部隐式包含了<initializer_list>。
#include <algorithm>
#include <initializer_list> // 必须包含,用于支持 std::initializer_list
#include <cassert> // 新增,用于 assert 越界检查
#define DFTCAPA 16
namespace brush
{
// 定义一个仿照STL中的vector
template<class T>
class vector
{
public:
// 先定义对应类型的迭代器
typedef T* iterator;
// 默认构造函数(构造容器,第一件事就是开辟一个空间)
vector()
: m_start(new T[DFTCAPA])
{
m_finish = m_start;
m_end_of_storage = m_start + DFTCAPA; // 左闭右开
}
// 给定空间大小的构造函数(传入默认参数,先开辟一个空间)
vector(size_t n, const T& val = T())
{
// DFTCAPA是宏定义,当然要先赋值
size_t tmpcapa = DFTCAPA;
// 开辟空间
while (tmpcapa < n)
{
tmpcapa *= 2;
}
m_start = new T[tmpcapa];
m_finish = m_start + n;
m_end_of_storage = m_start + tmpcapa; // 左闭右开
std::fill(m_start, m_finish, val);
}
// 给定迭代器开头和结尾,构造函数
vector(iterator b, iterator e)
{
size_t n = e - b;
size_t tmpcapa = DFTCAPA;
while (tmpcapa < n)
{
tmpcapa *= 2;
}
m_start = new T[tmpcapa];
m_finish = m_start + n;
m_end_of_storage = m_start + tmpcapa; // 左闭右开
std::copy(b, e, m_start); // 把从b到e的左闭右开区间中所有的内容,拷贝到m_start位置
}
// 通过传入列表初始化构造函数
vector(std::initializer_list<T> li)
{
size_t n = li.size();
size_t tmpcapa = DFTCAPA;
while (tmpcapa < n)
{
tmpcapa *= 2;
}
m_start = new T[tmpcapa];
m_finish = m_start + n;
m_end_of_storage = m_start + tmpcapa; // 左闭右开
std::copy(li.begin(), li.end(), m_start);
}
// 拷贝构造函数
vector(const vector<T>& v)
{
m_start = new T[v.capacity()];
/*
在C++代码中,如果我引入了std标准库中的它里面有size函数,
我自己在这个命名空间,也有size函数,那么编译器是如何判定,
到底用哪个size?【就近原则】
*/
m_finish = m_start + v.size();
m_end_of_storage = m_start + v.capacity();
// 复制v中的元素到新空间
std::copy(v.m_start, v.m_finish, m_start); // 添加这行
}
// 析构函数
~vector()
{
delete[] m_start;
}
// 赋值符号重载
vector<T>& operator=(const vector<T>& v)
{
if (this != &v) // 避免自赋值
{
// 释放旧空间
delete[] m_start;
// 分配新空间并复制元素
m_start = new T[v.capacity()];
m_finish = m_start + v.size();
m_end_of_storage = m_start + v.capacity();
std::copy(v.m_start, v.m_finish, m_start);
}
return *this;
}
// 方括号运算符重载
// 建议添加 const 版本(支持 const vector 对象调用)
const T& operator[](size_t i) const
{
assert(i < size() && "vector subscript out of range");
return m_start[i]; // 指针的方括号运算符语法糖
}
// << 输出重载运算符
/*
1、为什么<<运算符重载返回的必须是输出流:因为流运算符是左关联的(即表达式std::cout << a << b实际被解析为(std::cout << a) << b)
如果返回值是其他类型,比如void,那么上面链式传递中 b 就找不到可以左操作数,无法编译;
2、为什么<<运算符重载必须传入一个 std::ostream& 类型的对象,因为 << 运算符的左操作数是流对象,右操作数是待输出数据。
td::ostream 是标准库定义的类,std::ostream 类显式禁用了拷贝构造函数和拷贝赋值运算符,这意味着无法创建该类对象的副本,
只能通过引用或指针来操作已有对象。
3、主函数调用该类型对象a,使用 cout << a ,最终cout作为ot的实参引用传入,a作为m的实参引用传入。
4、我刚开始写错写成了:std::ostream& operator<<(std::ostream& ot, vector<T>& m) const,成员函数本类就默认有一个传入的对象this。
5、★★写成下面的类,<<重载就变的有趣起来了,在主函数中只有反着写输出对象和cout,才会重载。
*/
std::ostream& operator<<(std::ostream& ot) const
{
for (auto e : *this)
{
ot << e;
}
return ot;
}
// 插入任何能转化为T类型的值或者对象
/*
1、"const T& val 作为模板参数形式,能接收的实参类型非常广泛,核心原则是:任何能隐式转换为 T 类型的对象或值都可以"
- 1、数组也可以当做一种类型传入,但是要注意数组作为类型传入时数组大小要明确,因为数组类型包含了数组中元素的类型和数组大小
int a[] = { 1,2,3 };
cout << typeid(a).name() << endl; // int [3]
- 2、还可以直接传入数值,T会自动判定其类型。
2、在c++中形参加了const,那么实参可以是同类型的非const参数,实参在函数中被隐式转换为const类型了,但并没有影响实参原本类型。
3、当形参为const T&(const引用)时,非const实参可以通过隐式转换绑定到该引用。
4、形如const T*(指向const的指针)的形参可以接受T*(非const指针)的实参。编译器会隐式将T*转换为const T*,禁止通过指针修改对象。
*/
iterator insert(iterator pos, int n, const T& val)
{
size_t oldn = size();
// 审查空间
reserve(oldn + n);
// 移动之前的内容:
for (auto it = m_start + size() - 1, i = m_start + oldn - 1; i != pos-1; --it, --i)
{
*it = *i;
}
// 通过fill插入
std::fill(pos, pos + n, val); // 左闭右开,实际是在pos位置------pos+n-1位置之间闭区间插入
return pos;
}
// 插入一个该类型的容器
iterator insert(iterator pos, iterator b, iterator e)
{
size_t old_size = size();
size_t new_n = e - b;
// 先审查空间
reserve(old_size + new_n);
// 从pos开始向后移动,这里操作的应该一直都是this对象
for (auto i = m_finish-1, j = m_start + old_size - 1; j != pos - 1; --i, --j)
{
*i = *j;
}
// 通过c++标准库中的copy函数插入
std::copy(b, e, pos);
return pos;
}
// 删除一个值
iterator erase(iterator pos)
{
// 判断位置
int p;
pos == m_finish - 1 ? p = 0 : p = 1;
// 如果在中间移动其他值覆盖,重置容器指针
if (p)
{
/*
for循环终止条件写错过,写的是pos != m_finish - 2,
到m_finish - 2的时候for循环就停止执行了,最后一个就
传不到倒数第二个。
*/
for (pos; pos != m_finish - 1; pos++)
{
*pos = *(pos + 1);
}
// 重置指针
m_finish -= 1;
}
// 如果在尾巴上直接重置容器指针
else
{
m_finish -= 1;
}
return pos;
}
// 删除一个区间
iterator erase(iterator b, iterator e)
{
size_t rn = e - b;
// 先确定这个区间合法,不能超
if ( e <= m_finish)
{
// 将要删除的区间之后的元素拷贝进删除区间,然后调整容器指针
if (e < m_finish)
{
std::copy(e, m_finish, b);
m_finish -= rn;
}
else
{
m_finish -= rn;
}
return b;
}
}
// 给容器尾巴添加一个元素
void push_back(const T& a)
{
reserve(size() + 1); // 因为我在reserve中扩容的时候就已经改变了m_finish
*(m_finish - 1) = a;
}
// 将容器尾巴元素删除
void pop_back()
{
m_finish -= 1;
}
// 获取容器首元
/*
1、T& 明确指定了返回值类型是 "T 类型的引用"
2、const 修饰的是函数本身,表示这是一个常量成员函数(不能修改类的成员变量),
但不影响返回值类型。
*/
T& front() const
{
return *m_start;
}
// 获取容器尾元
T& back() const
{
return m_finish[-1]; // c++中指针的方括号语法糖,实际执行:*(m_finish-1);
}
// 判断容器是否为空
bool empty() const
{
return m_start == m_finish;
}
// 清空容器
void clear()
{
m_finish = m_start;
}
// 返回容器尺寸
size_t size() const
{
size_t tem_size = m_finish - m_start;
return tem_size;
}
// 返回容器容量【容量一定是大于容器尺寸的】
size_t capacity() const
{
size_t tem_capacity = m_end_of_storage - m_start;
return tem_capacity;
}
// 空间审查:够不做操作,不够重新开辟
void reserve(size_t s)
{
if (capacity() < s)
{
size_t old_size = size();
T* old_start = m_start;
// 原错误:int tmpcapa = capacity();
size_t tmpcapa = capacity(); // 改为 size_t,与 s 类型匹配
while (tmpcapa < s)
{
tmpcapa *= 2;
}
T* newspace = new T[tmpcapa];
std::copy(begin(), end(), newspace);
// 原错误:m_finish = newspace + size(); (size() 是 size_t,newspace 是 T*,加法没问题,但需确保逻辑一致)
m_start = newspace; // 之前遗漏了这行!导致 m_start 仍指向旧空间
m_finish = newspace + old_size; // 用 old_size 更清晰(避免 size() 依赖旧 m_start)
m_end_of_storage = newspace + tmpcapa;
delete[] old_start;
}
if(size() < s)
{
// 考虑到添加元素后,仍然不需要扩容,但是没有更新尾指针的情况;
m_finish += (s - size());
}
}
iterator begin() { return m_start; }
iterator end() { return m_finish; }
/*
建议同时添加const版本,
1、你想想const成员函数是干啥的?它不就是给const对象调用的,
如果你没写const版本,那么你初始化了一个const类型的对象,它怎么调用begin?
2、如果你写了 iterator begin() const { return m_start; } 却不给iterator前面加
const,那么编译是没有问题,但是问题是你的常对象通过常函数可以改容器当中的值。和
设计逻辑矛盾了。
*/
const iterator begin() const { return m_start; }
const iterator end() const { return m_finish; }
protected:
T* m_start;
T* m_finish; // 容器中内容最后位置的指针
T* m_end_of_storage; // 容器大小位置的指针
};
}
#endif // !BRUSH_VECTOR_HPP_
cpp
// 这是vs源文件中.main()文件的内容
#include <iostream>
#include "vactor.hpp"
int main()
{
brush::vector <int> c ({ 1,2,3,4 }); // // 隐式推导为 std::initializer_list<int>
//std::cout << c[1] << std::endl; // 2
//c.insert(c.begin() + 1, 2, 5); // 给容器c下标为1的位置插入2个5
//c << std::cout; // 155234 【因为<<重载函数写在了类当中,所以要通过调换位置写的方式把参数成功传递进去】
//brush::vector <int> a({ 6,6,6 }); // // 隐式推导为 std::initializer_list<int>
//c.insert(c.begin() + 1, a.begin(), a.end());
//c << std::cout; // 1666234
//c.erase(c.begin() + 1);
//c << std::cout; // 134
//c.erase(c.begin() + 1, c.begin() + 2);
//c << std::cout; // 134
//c.erase(c.begin() + 1, c.begin() + 4);
//c << std::cout; // 1
//c.push_back(5);
//c << std::cout; // 12345
//c.pop_back();
//c << std::cout; // 123
//int a = c.front();
//std::cout << a; // 1
//int a = c.back();
//std::cout << a; // 4
//c.erase(c.begin(), c.end());
//std::cout << c.empty(); // 1
c.clear();
std::cout << c.empty(); // 1
return 0;
}
4.5、c++中的arry容器:静态容器

5、列表初始化:
5.1、概念:
- 列表初始化(List Initialization)并非仅限于容器,它是 C++11 引入的一种通用初始化语法,适用于几乎所有类型的对象初始化,包括基本类型、自定义类、数组、容器等。【不需要引入任何额外的库】
- 补充:iostream只和cout与cin有关;
- 列表初始化案例
cpp
// 普通类型通过列表初始化:
int a{5}; // 初始化 int 变量为 5
double b{3.14}; // 初始化 double 变量为 3.14
// 数组的列表初始化
int arr[]{1, 2, 3, 4}; // 初始化数组,元素为 1,2,3,4
// 自定义类列表初始化:
class Point {
private:
int x, y;
public:
// 构造函数接收两个 int 参数
Point(int a, int b) : x(a), y(b) {}
};
Point p{10, 20}; // 用列表初始化调用构造函数
// 容器列表初始化
std::vector<int> vi{1, 2, 3}; // 初始化 vector【需要包含 C++ 标准库中的 <vector> 头文件。】
map<string, int> mp{{"a",1}, {"b",2}}; // 初始化 map
5.2、特点:
- 1、列表初始化的核心是使用花括号 {} 包裹初始化值
- 2、语法统一:无论初始化什么类型(基本类型、类、容器等),都可以用相同的 {} 语法。
- 3、明确性:相比 = 初始化或构造函数调用,{} 更直观地表达 "用这些值初始化对象" 的意图。
- 4、列表初始化是 C++ 中一种通用的初始化机制,适用于各种类型的对象。
- 把花括号初始化数据往函数中传递的方式:
- 1、在 C++ 中,std::initializer_list li 是用于处理列表初始化语法的一个关键工具类,它的核心作用是 封装 由花括号 {} 给出的初始化列表,让函数、构造函数可以统一接收 "花括号列表形式的参数"。
- 2、C++ 语法规定:当你在函数调用中使用 {...} 传递参数时,编译器会将这个花括号列表隐式转换为 std::initializer_list 类型(T 是列表中元素的类型)。因此,只有当函数的形参是 std::initializer_list 时,才能匹配这种调用方式。【std::initializer_list 作为形参的好处是可以随传入的实参隐式推到T的类型】
- 补充1:使用 typeid(a).name() 可以获取变量 a 的类型名称(需要包含 <typeinfo> 头文件)。
- 补充2:下面是 initializer_list 类的说明,通过这个类分析其作用:
- 1、函数参数传递:允许函数接受任意长度的同类型参数列表,无需可变参数模板(...)或重载。
- 2、标准库容器(如 std::vector、std::list、std::map)提供 initializer_list 构造函数,简化初始化:std::vector v = {1, 2, 3}; // 调用 vector(initializer_list)
- 3、通过 begin() 和 end() 方法,std::initializer_list 可直接用于范围 for 循环
cpp
// initializer_list 类:轻量级模版类
template <typename T>
class initializer_list {
public:
const T* begin() const; // 获取起始迭代器
const T* end() const; // 获取结束迭代器
size_t size() const; // 获取元素数量
};
4、三个算法:
4.1、cout_if
- 通过模版函数,将函数作为记录条件传入另一个函数中,获取某个容器中满足某个条件的值的数量。
cpp
#include <iostream>
#include <vector>
using namespace std;
/*
1、将条件函数通过指针传参的方式传入作为判断
依据,
*/
// count_if函数:数出满足条件的元素的个数
template <typename T1, typename _pr, typename c>
int count_if(T1 b, T1 e, _pr pred, c compare_val)
{
int ct = 0;
for (auto t = b; t != e; t++)
{
ct += pred(*t,compare_val);
}
return ct;
}
int prif(int a , int val)
{
return a == val ? 1:0 ;
}
int main()
{
vector<int> v{ 1, 2, 1, 4, 1 };
// 定义可以指向函数的指针
/*
在 C++ 中,函数名本身就代表该函数的地址。当你使用函数名时(不添加调用运算符()),
它会被隐式转换为指向该函数的指针(函数指针)。这一点与数组名类似(数组名本身代
表数组首元素的地址)。
*/
cout << count_if(v.begin(), v.end(), prif, 1) << endl; // 3
int (*p)(int, int); // 定义函数指针
p = prif;
cout << count_if(v.begin(), v.end(), p, 3) << endl; // 0
}
4.2、remove
- 这个函数的作用是删除掉区间内所有值为指定值的函数【快慢指针法】;
- 具体方法,慢指针记录不包含指定元素的序列,列尾,快指针一直走,遇到非指定元素,就和慢指针发生交换,慢指针后移,这样就可以把指定元素挑出来,扔到序列最后去了。【把不是指定元素的换到前面,把是的换到后面】;
- 如下图所示,最终返回的应该是mp慢指针作为容器的尾指针。【符合SLT容器中左闭右开的规则】;
- 代码实现:在真实代码实现的时候,不用交换会好一些,直接把快指针的值给到慢指针,慢指针存放就行了。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename _it, typename _val>
_it b_remove(_it b, _it e, const _val& val)
{
_it kp = b, mp = b;
for (; kp != e; ++kp)
{
// 快指针指向非指定元素val时,交换,并且慢指针后移
if (*kp != val)
{
swap(*kp, *mp); // 这里还有一种做法,不用换直接给慢指针位置存放就行了,后面的值不用管 *mp = *kp;
++mp;
}
}
return mp;
}
int main()
{
vector<int> v{ 1, 2, 1, 4, 1 };
vector<int>::iterator p = b_remove(v.begin(), v.end(), 1); // 其实这里p的类型,可以是auto,省去类型推导的麻烦
for (auto it = v.begin(); it != p; ++it)
{
cout << *it << " "; // 2 4
}
}
4.3、merge
- 这个函数的功能是将两个有序区间合并为一个新的有序区间。一共5个基本参数,前两个是有序区间1的首尾,第三四个是有序区间2的首尾,第五个是新有序区间的首部。
- 思路:三指针法,如果要得到从小到大序列,每次要要取最小值,反之,每次取最大值,如果一个序列已经走完了,另一个序列直接连在主序列后即可。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename _it, typename _end>
_end merge(_it b1, _it e1, _it b2, _it e2, _end b3)
{
_end tem_p = b3;
// 开始比较b1和b2,开始合并
while (b1 != e1 && b2 != e2)
{
if (*b1 < *b2)
{
*tem_p = *b1;
++b1;
}
else
{
*tem_p = *b2;
++b2;
}
++tem_p;
}
// 检查是否有还未合并的内容,添加在b3的末尾
while (b1 != e1)
{
*tem_p = *b1;
++b1;
++tem_p;
}
while (b2 != e2)
{
*tem_p = *b2;
++b2;
++tem_p;
}
return tem_p;
}
int main()
{
vector<int> v1{ 1, 4, 5, 7 };
vector<int> v2{ 2, 3, 5, 9 };
size_t n1 = v1.size();
size_t n2 = v2.size();
const size_t total_size = n1 + n2;
// ①老办法
int* b3 = new int [total_size];
int * end_p1 = merge(v1.begin(),v1.end(),v2.begin(),v2.end(),b3);
size_t i;
for (i = 0; i < total_size; i++)
{
cout << b3[i] << " ";
} // 1 2 3 4 5 5 7 9
delete[] b3;
cout << endl;
// ②容器的办法
vector<int> v3(total_size);
vector<int>::iterator end_p2 = merge(v1.begin(),v1.end(),v2.begin(),v2.end(),v3.begin());
for (auto e = v3.begin(); e != end_p2; ++e)
{
cout << *e << " ";
} // 1 2 3 4 5 5 7 9
// 释放空间?
// 1、如果b3空间是容器创建,则容器自身使用RAII机制(vector自动管理内存),可以释放空间
// 2、如果b3空间不是容器创建,是手动创建,则需要释放
}
4.4、两种算法思想:
- 双指针法【快慢指针法】:用于筛选保留单个序列中的条件元素,快指针查询,慢指针筛选保留。
- 三指针法:用于两个有序序列合并
6、数据结构_链表:
6.1、基本概念:
-
链表定义:链表是一种由结点构成的线性表。这些结点之间用指针相连,所以其结构在物理空间上并不连续。
-
链表特点:由于其不连续,所以无法像数组那样直接随机访问,只能顺序访问。
-
链表结点结构:在c++中通过结构体对象实现
cpp
struct listNode
{
int data;
listNode* Next;
};
6.2、链表的基本操作:
- 遍历:通过链表头指针和链表最后一个元素指向空(nullptr)的特点
cpp
// 遍历方式
for(cur=head; cur; cur = cur->next)
- 后插:单链表中只能在某个结点后面进行插入【先连后断】
cpp
// 往结点A后面插入结点B的范式,p_A是指向结点A的指针,p_B是指向结点B的指针
p_B->next = p_A->next;
p_A->next = p_B;
- 后删:单链表只能在某个结点后删除,删除p_A结点时,要找其后的结点。
cpp
Node* tmp = p_A->next; // 保存要删除的节点指针
p_A->next = p_A->next->next; // 将A节点的next指向要删除节点的下一个节点
delete tmp; // 释放要删除节点的内存
- 头插:将插入的结点作为链表的新"头"
cpp
cur->next = head;
head = cur;
- 头删:将头结点后移,然后删除旧头结点。
cpp
tmp = head;
head = head->next;
delete tmp;
6.3、链表的分类:
- 带头链表和无头链表:带头链表指的是在链表中存在一个无效数据的数据头,这个数据头专门用来标的链表的开始位置。在单链表中,一旦存在了这样一个无效数据头,则可以杜绝头插和头删操作,只剩下了尾插和尾删操作。
- 循环链表和无环链表:循环链表指的是链表的尾部也指向了链表中的某个结点,此时链表中就有了环。如果再按照一般的方法遍历链表,则会产生死循环。【如何判断链表中是否有环? 使用快慢指针法,定义两个指针都指向链表头,然后让快指针一次走两步,慢指针一次走一步,如果快指针出去了,则表示无环,如果两个指针相遇了则表示有环。】
- 单向链表和双向链表:双向链表在单链表的基础上多了一个指向前一个prev指针,使它可以向前遍历。
6.4、单链表的逆序:
-
头删头插法:将链表中所有的逐个头删,然后头插到一个新链表中去。
-
后删头插法:让一个指针A始终指向原来头的位置,然后对这个结点A不断执行后删操作,并把删掉的结点对当前链表做头插。直到A的下一个已经为空为止。
-
向后转法:先将第一个结点的next 置空,然后让剩余的所有结点都指向自己的前一个结点即可。
6.5、用c++的hpp文件实现一个链表:
-
设计节点类
-
设计迭代器类
-
设计链表类
-
实现代码
cpp
// 写在vs头文件中的.hpp 文件
#ifndef _FORWORDLISTNODE_ // 修正宏名拼写:FORDWORDLISTNODE -> FORWORDLISTNODE
#define _FORWORDLISTNODE_
#include <iostream>
#include <initializer_list> // 包含初始化列表头文件
namespace brush {
// 1. 节点类:存储数据和下一个节点指针
template <typename T>
class sListNode {
private:
T data;
sListNode<T>* Next;
// 私有构造:仅友元可创建
sListNode(T da) : data(da), Next(nullptr) {}
// 友元声明:允许迭代器和链表操作类访问
template <typename U>
friend class Iterator;
template <typename U>
friend class ForwordList;
};
// 2. 迭代器类:提供链表遍历接口
template <typename T>
class Iterator {
protected:
sListNode<T>* current; // 指向当前节点
// 构造函数:支持默认初始化(尾后位置)
Iterator(sListNode<T>* ct = nullptr) : current(ct) {}
// 友元:允许链表类访问
template <typename U> // 避免参数名冲突(U 和 T 是两个独立的模板参数),这样T和U实际传入是否一致,都可以顺利编译。
friend class ForwordList;
public:
// 重载+:移动n步,返回新迭代器(不修改自身)
Iterator<T> operator+(int b) const {
Iterator<T> temp = *this; // 创建临时迭代器
for (int i = 0; i < b && temp.current != nullptr; ++i) { // 定义i,避免越界
++temp; // 复用前置++
}
return temp;
}
// 重载->:返回当前节点数据的指针
T* operator->() const {
return &(current->data);
}
// 重载*:返回当前节点数据的引用(支持修改)
T& operator*() const {
return current->data;
}
// 前置++:移动到下一个节点,返回自身引用(修改自身)
Iterator<T>& operator++() {
if (current != nullptr) { // 避免空指针访问
current = current->Next;
}
return *this;
}
// 后置++:返回移动前的副本(不修改自身)
Iterator<T> operator++(int) {
Iterator<T> temp = *this; // 保存当前状态
++(*this); // 复用前置++
return temp;
}
// 重载==:比较当前节点指针
bool operator==(const Iterator<T>& other) const {
return current == other.current;
}
// 重载!=:复用==,判断迭代器是否不同
bool operator!=(const Iterator<T>& other) const {
return !(*this == other);
}
// 供链表类访问current的接口(可选,也可通过友元直接访问)
sListNode<T>* get_current() const {
return current;
}
};
// 3. 链表操作类:对外提供链表功能
template <typename T>
class ForwordList {
public:
typedef Iterator<T> iterator; // 对外暴露迭代器类型
private:
iterator head; // 头迭代器(指向第一个节点)
iterator tail; // 尾迭代器(指向最后一个节点)
size_t l_size; // 节点数量
public:
// ① 默认构造:空链表
ForwordList()
: head(iterator(nullptr)), // 显式初始化迭代器为尾后位置
tail(head),
l_size(0)
{
}
// ② 初始化列表构造:尾插方式初始化
ForwordList(std::initializer_list<T> init_list)
: head(iterator(nullptr)),
tail(head),
l_size(0)
{
for (const auto& val : init_list) {
tail_insert(val); // 复用尾插函数
}
}
// ③ 析构函数:清空链表,释放内存
~ForwordList() {
clear();
}
// 4. 插入操作
// 头部插入:在链表头部添加新元素【注意传入的是一个基本类型,不是结点类对象】
void head_insert(const T& val) {
// 用new创建堆节点(友元权限访问sListNode私有构造)
sListNode<T>* newNode = new sListNode<T>(val);
// 包装为迭代器
iterator newIt(newNode);
if (empty()) { // 空链表:头和尾都指向新节点
head = newIt;
tail = newIt;
}
else { // 非空链表:新节点next指向原头,更新头
newNode->Next = head.get_current(); // 获取原头的节点指针
head = newIt;
}
l_size++;
}
// 尾部插入:在链表尾部添加新元素
void tail_insert(const T& val) {
sListNode<T>* newNode = new sListNode<T>(val);
iterator newIt(newNode);
if (empty()) { // 空链表:头和尾都指向新节点
head = newIt;
tail = newIt;
}
else { // 非空链表:原尾节点next指向新节点,更新尾
tail.get_current()->Next = newNode;
tail = newIt;
}
l_size++;
}
// 指定位置插入(pos从0开始,0=头前,l_size=尾后)
bool pos_insert(size_t pos, const T& val) {
if (pos > l_size) { // pos越界(最大为l_size)
return false;
}
if (pos == 0) { // 等同于头部插入
head_insert(val);
return true;
}
if (pos == l_size) { // 等同于尾部插入
tail_insert(val);
return true;
}
// 找到pos-1位置的迭代器(前驱节点)
iterator prevIt = head + (pos - 1);
sListNode<T>* prevNode = prevIt.get_current();
// 创建新节点并插入
sListNode<T>* newNode = new sListNode<T>(val);
newNode->Next = prevNode->Next; // 新节点next指向前驱的next 【后插】
prevNode->Next = newNode; // 前驱next指向新节点
l_size++;
return true;
}
// 5. 删除操作
// 头部删除:返回是否成功,用引用返回删除的值
bool head_delete(T& out_val) {
if (empty()) {
return false;
}
sListNode<T>* delNode = head.get_current(); // 要删除的节点
out_val = delNode->data; // 保存删除的值
if (head == tail) { // 只有一个节点:重置为空链表
head = iterator(nullptr);
tail = head;
}
else { // 多个节点:头迭代器后移
head++;
}
delete delNode; // 释放堆内存
l_size--;
return true;
}
// 尾部删除:返回是否成功,用引用返回删除的值【用了引用传参,这样函数内部就可以修改函数外的被引用的实参了】
bool tail_delete(T& out_val) {
if (empty()) {
return false;
}
sListNode<T>* delNode = tail.get_current(); // 要删除的节点
out_val = delNode->data; // 保存删除的值
if (head == tail) { // 只有一个节点:重置为空链表
head = iterator(nullptr);
tail = head;
}
else { // 多个节点:找到尾的前驱
iterator prevIt = head;
while (prevIt.get_current()->Next != delNode) {
prevIt++;
}
prevIt.get_current()->Next = nullptr; // 前驱next置空
tail = prevIt; // 更新尾迭代器
}
delete delNode; // 释放堆内存
l_size--;
return true;
}
// 指定位置删除(pos从0开始)
bool pos_delete(size_t pos, T& out_val) {
if (empty() || pos >= l_size) { // 空链表或pos越界
return false;
}
if (pos == 0) { // 等同于头部删除
return head_delete(out_val);
}
// 找到pos-1位置的前驱迭代器
iterator prevIt = head + (pos - 1);
sListNode<T>* prevNode = prevIt.get_current();
sListNode<T>* delNode = prevNode->Next; // 要删除的节点
out_val = delNode->data; // 保存删除的值
prevNode->Next = delNode->Next; // 前驱next指向删除节点的next
// 如果删除的是尾节点,更新尾迭代器
if (delNode == tail.get_current()) {
tail = prevIt;
}
delete delNode; // 释放堆内存
l_size--;
return true;
}
// 6. 清空链表:释放所有节点
void clear() {
while (!empty()) {
T dummy; // 临时变量接收删除的值(未使用)
head_delete(dummy);
}
}
// 7. 迭代器接口:供外部遍历
iterator begin() {
return head; // 指向第一个节点
}
iterator end() {
return iterator(nullptr); // 尾后位置(nullptr)
}
// 8. 辅助函数
bool empty() const {
return l_size == 0;
}
size_t size() const {
return l_size;
}
};
}
#endif // !_FORWORDLISTNODE_
- 补充1、为什么设计中,只有最后一个类有析构函数?sListNode:内存由 ForwordList 管理,无额外资源;Iterator:仅持有指针,不拥有节点所有权,无额外资源;ForwordList:管理 new 分配的节点,必须写析构函数释放资源。
- 补充2、这个代码是不是还能够优化?还有工厂设计方案,可以用智能指针【学习技术的某一个模块并不难,难在把知识融合使用】