概述
这是一个完整的模板类 yyq::vector 的实现,模仿 C++ 标准库中的 std::vector。作为STL最重要的容器之一,vector的动态数组实现展示了C++模板编程、内存管理和迭代器设计的核心技术。本文将全面分析该实现,不遗漏任何细节。
目录
[1.1 命名空间与头文件保护](#1.1 命名空间与头文件保护)
[1.2 类成员变量设计](#1.2 类成员变量设计)
[2.1 迭代器类型定义](#2.1 迭代器类型定义)
[2.2 迭代器访问函数](#2.2 迭代器访问函数)
[3.1 默认构造函数](#3.1 默认构造函数)
[3.2 拷贝构造函数](#3.2 拷贝构造函数)
[3.3 指定大小和初始值的构造函数](#3.3 指定大小和初始值的构造函数)
[3.4 析构函数](#3.4 析构函数)
[4.1 交换函数](#4.1 交换函数)
[4.2 赋值操作符(现代实现)](#4.2 赋值操作符(现代实现))
[4.3 被注释的传统实现](#4.3 被注释的传统实现)
[5.1 size和capacity](#5.1 size和capacity)
[5.2 empty函数](#5.2 empty函数)
[5.3 reserve函数(重要)](#5.3 reserve函数(重要))
[5.4 resize函数](#5.4 resize函数)
[6.1 operator[]重载](#6.1 operator[]重载)
[7.1 push_back函数](#7.1 push_back函数)
[7.2 pop_back函数](#7.2 pop_back函数)
[7.3 insert函数](#7.3 insert函数)
[7.4 erase函数](#7.4 erase函数)
[7.5 被注释的clear函数](#7.5 被注释的clear函数)
[8.1 print_vector函数](#8.1 print_vector函数)
[8.2 print_container函数(通用版本)](#8.2 print_container函数(通用版本))
[10.1 设计亮点](#10.1 设计亮点)
[10.2 存在的问题与改进建议](#10.2 存在的问题与改进建议)
[10.3 改进的reserve实现示例](#10.3 改进的reserve实现示例)
一、整体架构与设计
1.1 命名空间与头文件保护
cpp
namespace yyq
{
template<class T>
class vector { ... };
}
-
使用自定义命名空间
yyq避免与标准库冲突 -
模板类设计,支持任意类型的元素
-
#pragma once防止头文件重复包含
1.2 类成员变量设计
cpp
private:
iterator _start = nullptr; // 指向数组起始位置
iterator _finish = nullptr; // 指向最后一个元素的下一个位置
iterator _end_of_storage = nullptr; // 指向分配内存的末尾
采用三指针设计,这是vector的经典实现方式:
-
_start:数组起始位置 -
_finish:有效元素末尾的下一个位置(size = _finish - _start) -
_end_of_storage:分配内存的末尾(capacity = _end_of_storage - _start)
二、迭代器与类型定义
2.1 迭代器类型定义
cpp
typedef T* iterator;
typedef const T* const_iterator;
-
简单地将指针作为迭代器,因为vector在内存中是连续的
-
提供const和非const版本,支持常对象
2.2 迭代器访问函数
cpp
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin()const { return _start; }
const_iterator end()const { return _finish; }
-
提供完整的迭代器支持
-
使vector可以与STL算法配合使用
-
支持基于范围的for循环
三、构造函数与析构函数
3.1 默认构造函数
cpp
// C++11 默认生成默认构造函数
vector() = default;
-
使用C++11的
= default语法 -
成员变量已初始化为
nullptr,确保安全
3.2 拷贝构造函数
cpp
vector(const vector<T>& v)
{
reserve(v.size());
for (auto e : v)
{
push_back(e);
}
}
-
先分配与源vector相同大小的内存
-
使用范围for循环遍历并复制每个元素
-
注意:这里使用了
push_back,会调用元素的赋值操作
3.3 指定大小和初始值的构造函数
cpp
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
重要细节:
-
两个重载版本:
size_t和int参数 -
这是为了解决模板推导时的歧义问题
-
使用
T()作为默认初始值(调用类型的默认构造函数) -
例如:
vector<int> v(10)创建包含10个0的vector -
例如:
vector<int> v(10, 5)创建包含10个5的vector
3.4 析构函数
cpp
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage;
}
}
-
检查
_start是否为nullptr再删除 -
删除后将所有指针置为
nullptr,避免悬空指针 -
注意:对于自定义类型,会调用每个元素的析构函数
四、赋值操作符与交换函数
4.1 交换函数
cpp
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
-
使用
std::swap交换三个指针 -
效率高,仅交换指针,不复制元素
-
是拷贝交换技法的核心
4.2 赋值操作符(现代实现)
cpp
vector<T>& operator=(vector<T> temp)
{
swap(temp);
return *this;
}
这是经典的"拷贝并交换"技法:
-
参数
temp是值传递,会自动调用拷贝构造函数 -
交换当前对象和
temp的资源 -
temp离开作用域时自动析构,释放原资源 -
异常安全且自动处理自赋值
4.3 被注释的传统实现
cpp
// 传统赋值操作符实现
//vector<T>& operator=(const vector<T>& v)
//{
// if (this != &v) // 检查自赋值
// {
// clear(); // 清空当前内容
// reserve(v.size()); // 分配足够空间
// for (auto e : v) // 复制元素
// {
// push_back(e);
// }
// }
// return *this;
//}
-
显示检查自赋值
-
先清空再重新分配和复制
-
现代实现更简洁优雅
五、容量管理
5.1 size和capacity
cpp
size_t size()const
{
return _finish - _start; // 有效元素个数
}
size_t capacity()const
{
return _end_of_storage - _start; // 分配的内存容量
}
-
使用指针算术计算大小和容量
-
都是const成员函数
5.2 empty函数
cpp
bool empty()
{
return _start == _finish; // 起始等于结束表示空
}
-
检查vector是否为空
-
注意:这里应该是
const成员函数
5.3 reserve函数(重要)
cpp
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* temp = new T[n];
// 重要:不能使用memcpy,因为T可能是自定义类型
for (size_t i = 0; i < size(); i++)
{
temp[i] = _start[i]; // 使用赋值操作符
}
delete[] _start;
_start = temp;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
关键细节:
-
为什么不用memcpy?
cpp
// 被注释的错误实现: // memcpy(temp, _start, size() * sizeof(T));-
memcpy是浅拷贝,对于包含指针或资源的自定义类型会出问题 -
使用循环赋值可以调用元素的赋值操作符,正确处理深拷贝
-
-
扩容策略
-
只扩容不缩容
-
先分配新内存,再复制元素,最后释放旧内存
-
保存旧的大小,因为
size()在释放后可能失效
-
5.4 resize函数
cpp
void resize(size_t n, T val = T())
{
if (n < size()) // 缩小
{
_finish = _start + n; // 简单截断
}
else // 扩大
{
reserve(n); // 确保有足够容量
while (_finish < _start + n)
{
*_finish = val; // 用val填充新元素
++_finish;
}
}
}
-
如果新大小小于当前大小,简单截断
-
如果新大小大于当前大小,用指定值填充新位置
-
使用
T()作为默认填充值
六、元素访问
6.1 operator[]重载
cpp
T& operator[](size_t i)
{
assert(i < size());
return _start[i]; // 返回引用,可修改
}
const T& operator[](size_t i)const
{
assert(i < size());
return _start[i]; // 返回const引用,只读
}
-
提供非常量和常量两个版本
-
使用assert进行边界检查
-
支持随机访问,时间复杂度O(1)
七、修改操作
7.1 push_back函数
cpp
void push_back(const T& x)
{
if (_finish == _end_of_storage) // 需要扩容
{
reserve(capacity() == 0 ? 4 : 2 * capacity()); // 2倍扩容
}
*_finish = x; // 在末尾添加元素
++_finish; // 更新_finish指针
}
扩容策略:
-
初始容量为0时,扩容到4
-
后续按2倍扩容(vector的经典策略)
-
使用引用参数避免不必要的复制
7.2 pop_back函数
cpp
void pop_back()
{
assert(!empty()); // 确保不为空
--_finish; // 简单递减_finish
}
-
仅减少大小,不释放内存
-
元素会被保留在内存中但不再可访问
-
对于自定义类型,需要显式调用析构函数(当前实现有缺陷)
7.3 insert函数
cpp
iterator insert(iterator pos, const T& x)
{
// 检查是否需要扩容
if (_finish == _end_of_storage)
{
size_t len = pos - _start; // 保存偏移量
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len; // 重新计算pos(因为_start可能改变)
}
// 向后移动元素
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x; // 插入新元素
++_finish; // 更新大小
return pos; // 返回插入位置的迭代器
}
重要细节:
-
迭代器失效问题 :扩容后,原来的
pos会失效,需要重新计算 -
保存偏移量 :
len = pos - _start,扩容后pos = _start + len -
从后向前移动:避免覆盖未处理的元素
-
返回值:返回指向新插入元素的迭代器(符合STL规范)
7.4 erase函数
cpp
iterator erase(iterator pos)
{
assert(pos >= _start); // 确保pos有效
assert(pos < _finish);
iterator it = pos + 1;
while (it != end()) // 向前移动元素
{
*(it - 1) = *it;
++it;
}
--_finish; // 更新大小
return pos; // 返回被删除元素的下一个位置
}
-
检查pos的合法性
-
从前向后移动元素覆盖要删除的位置
-
返回被删除元素的下一个位置(符合STL规范)
-
注意:对于自定义类型,应该显式调用析构函数
7.5 被注释的clear函数
cpp
//void clear()
//{
// _finish = _start; // 简单重置_finish
//}
-
简单实现:仅重置
_finish指针 -
问题:对于自定义类型,没有调用析构函数
-
更好的实现应该调用每个元素的析构函数
八、辅助函数
8.1 print_vector函数
cpp
template<class T>
void print_vector(const vector<T>& v)
{
// 方法1:使用迭代器
typename vector<T>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 方法2:使用范围for循环
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
关键细节:
-
typename关键字:告诉编译器vector<T>::const_iterator是一个类型 -
两种遍历方式:传统迭代器和范围for循环
-
模板函数,可以打印任意类型的vector
8.2 print_container函数(通用版本)
cpp
template<class Container>
void print_container(const Container& v)
{
// 与方法1相同,但更通用
typename Container::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
-
更通用的版本,可以打印任何支持迭代器的容器
-
使用了模板模板参数
-
展示了泛型编程的思想
十、设计亮点与改进建议
10.1 设计亮点
-
现代C++风格 :使用
= default、拷贝交换技法 -
完整的迭代器支持:与STL完全兼容
-
异常安全:赋值操作符的现代实现是异常安全的
-
模板设计:支持任意类型
-
合理的扩容策略:2倍扩容,平衡性能和内存使用
10.2 存在的问题与改进建议
-
析构问题:
-
pop_back、erase、clear没有调用元素的析构函数 -
对于自定义类型可能造成资源泄漏
-
-
异常安全:
-
reserve中如果元素赋值抛出异常,会内存泄漏 -
应该先复制到临时内存,成功后再交换
-
-
缺少的功能:
cpp
// 应该添加以下功能: T& front(); // 访问第一个元素 T& back(); // 访问最后一个元素 void shrink_to_fit(); // 缩减内存 -
性能优化:
-
可以使用移动语义(C++11)
-
push_back可以添加右值引用版本 -
可以添加emplace_back直接构造
-
-
const正确性:
cpp
bool empty() const // 应该是const成员函数 -
迭代器失效:
-
需要更完善的迭代器失效处理
-
文档说明哪些操作会使迭代器失效
-
10.3 改进的reserve实现示例
cpp
void reserve(size_t n)
{
if (n > capacity())
{
T* temp = new T[n];
size_t i = 0;
try
{
for (; i < size(); i++)
{
temp[i] = _start[i]; // 可能抛出异常
}
}
catch (...)
{
// 异常时清理已构造的元素
for (size_t j = 0; j < i; j++)
{
temp[j].~T(); // 显式调用析构
}
delete[] temp;
throw; // 重新抛出异常
}
// 成功则替换
for (size_t j = 0; j < size(); j++)
{
_start[j].~T(); // 析构旧元素
}
delete[] _start;
_finish = temp + i;
_start = temp;
_end_of_storage = _start + n;
}
}
十一、完整性与学习价值
这个yyq::vector实现涵盖了动态数组的核心功能:
-
✅ 模板类设计
-
✅ 动态内存管理
-
✅ 迭代器支持
-
✅ 基本操作(增删改查)
-
✅ 容量管理
-
✅ 拷贝控制
-
✅ 运算符重载
十二、总结
这个vector实现展示了C++模板编程和容器设计的多个重要方面:
-
模板元编程:通用的容器设计
-
内存管理:动态数组的分配与释放
-
迭代器设计:连续容器的迭代器实现
-
异常安全:拷贝交换技法的应用
-
性能考虑:2倍扩容策略
虽然与标准库的std::vector相比还有差距(如缺少移动语义、异常安全不够完善),但它作为一个教学实现,完美展示了vector的核心原理,是学习C++容器设计和模板编程的优秀范例。
通过分析这个实现,我们可以深入理解:
-
vector的内部存储机制
-
动态扩容的原理和代价
-
迭代器失效的原因和应对
-
模板类设计的基本模式
-
现代C++编程的最佳实践