@TOC
vector的模拟实现
一.vector.h
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
namespace yl
{
template<typename T>
class vector {
private:
iterator _start;
iterator _finsh;
iterator _endofstorage;
以下 C++ 代码模块说明:
1. 命名空间封装(namespace yl
)
- 定义
namespace yl
,将自定义vector
类封装其中,避免与其他代码同名类冲突。
2. 模板类声明(template
)
- 通过
template <class T>
声明模板类,T
为类型参数,支持灵活实例化:- 如
yl::vector<int>
存储int
类型、yl::vector<double>
存储double
类型。
- 如
3. 模板类定义(class vector
)
结合模板声明,定义 class vector
,实现自定义动态数组功能,核心逻辑依赖以下成员。
4. 私有成员变量(private
部分)
包含 3 个迭代器,标记容器不同边界:
iterator _start
:指向数据起始位置,是容器第一个元素的指针/迭代器。iterator _finish
:指向已使用数据的末尾(即最后一个有效元素的下一个位置 )。iterator _endofstorage
:指向底层存储空间末尾,标记容器总容量边界(_finish
不能超过它 )。
public:
using iterator=T*;//C++11标准
using const_iterator = const T*;
vector()
:_start(nullptr)
,_finsh(nullptr)
,_endofstorage(nullptr)
{ }
iterator begin()
{
return _start;
}
iterator end()
{
return _finsh;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finsh;
}
以上代码展示的内容:
5. 类型别名定义
using iterator=T*;
- 在 C++11 标准下,利用
using
关键字为T*
定义类型别名iterator
。在自定义vector
类中,iterator
即指向模板参数T
类型的指针,便于在后续代码中表示可修改元素的迭代器类型,用于遍历容器元素 。
- 在 C++11 标准下,利用
using const_iterator = const T*;
- 同样借助
using
定义类型别名,const_iterator
被定义为指向const T
类型的指针,即常迭代器类型。其作用是在不修改容器元素的前提下遍历容器 。
- 同样借助
6. 构造函数
vector() :_start(nullptr), _finsh(nullptr), _endofstorage(nullptr)
- 此为
vector
类的默认构造函数,采用成员初始化列表的方式,将三个私有成员变量_start
、_finsh
和_endofstorage
初始化为nullptr
。这表明在默认构造时,该自定义vector
容器尚未分配实际存储空间,也没有存储任何元素 。
- 此为
7. 迭代器相关成员函数
iterator begin()
- 属于非
const
成员函数,返回指向容器起始位置的迭代器,即私有成员变量_start
,用于支持非const
对象的正向遍历 。
- 属于非
iterator end()
- 也是非
const
成员函数,返回指向容器末尾(已使用元素的末尾 )位置的迭代器,也就是_finsh
。与begin()
函数配合,可实现对容器内元素的遍历 。
- 也是非
const_iterator begin() const
- 是
begin()
函数的const
版本,适用于const
对象。由于const
对象不能调用非const
成员函数,通过该版本可在不修改容器内容的情况下,获取指向起始位置的常迭代器 。
- 是
const_iterator end() const
- 为
end()
函数的const
版本,为const
对象提供指向容器末尾的常迭代器 。
- 为
~vector()
{
if (_start)
{
delete[] _start;
_start = _finsh = _endofstorage = nullptr;
}
}
vector(initializer_list li)//支持任意多个值初始化
{
reserve(li.size());
for (auto& e : li)
{
push_back(e);
}
}
template<typename inputiterator>
vector(inputiterator begin, inputiterator end)
{
while (begin < end)
{
push_back(*begin);
++begin;
}
}
8.vector的析构函数及拷贝构造函数:
1. 析构函数 ~vector()
:
- 核心逻辑:释放容器动态分配的内存,并将指针置空,防止野指针。
- 关键点:
- 内存释放:通过
delete[] _start
释放数组内存。 - 安全处理:检查
_start
是否为空,避免释放空指针。 - 指针重置:将
_start
、_finish
、_endofstorage
全部置为nullptr
,确保析构后对象状态安全。
- 内存释放:通过
2. 初始化列表构造函数 vector(initializer_list li)
:
- 核心逻辑:支持使用花括号初始化器(如
vector<int> v = {1, 2, 3};
)初始化容器。 - 关键点:
- 预分配内存:通过
reserve(li.size())
预先分配足够空间,避免多次扩容。 - 元素添加:遍历初始化列表,通过
push_back
将元素逐个添加到容器。
- 预分配内存:通过
3. 迭代器构造函数 vector(inputiterator begin, inputiterator end)
:
- 核心逻辑:使用任意迭代器范围初始化容器,支持从其他容器或数组拷贝元素。
- 关键点:
- 范围遍历:通过
while (begin < end)
遍历迭代器范围,逐个添加元素。 - 适用性:适用于所有支持
<
比较和++
递增的迭代器(如随机访问迭代器)。
- 范围遍历:通过
8.1.注意事项:
- 潜在问题:迭代器构造函数假设
begin
和end
支持<
比较,这对输入迭代器(如istream_iterator
)不成立,可能导致编译错误。 - 优化方向:可通过迭代器分类(如
random_access_iterator_tag
)区分不同迭代器类型,针对随机访问迭代器预先分配内存以提升效率。
size_t size()const{
return _finsh - _start;
}
size_t capacity() const{
return _endofstorage - _start;
}
bool empty(){
return _finsh == _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
// 拷贝旧空间数据到新空间
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * old_size);
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + old_size;
_endofstorage = _start + n;
}
}
9.size 成员函数
size_t size() const
- 这是一个
const
成员函数,作用是获取当前容器中已存储元素的数量。返回值类型size_t
是无符号整型,常用于表示大小、计数等场景。 - 函数体为
return _finsh - _start;
。因为_start
和_finsh
是指向同一块连续内存区域的指针类型,二者相减得到的就是它们之间元素的个数,也就是已存储元素的数量。
- 这是一个
10.capacity 成员函数
size_t capacity() const
- 作为
const
成员函数,用于获取容器当前已分配的存储空间总共能容纳元素的数量,即容器的总容量。 - 函数体
return _endofstorage - _start;
,通过表示容器底层存储空间末尾位置的迭代器_endofstorage
减去起始位置迭代器_start
,直接计算出整个底层内存空间的总长度,该长度即为容器能够容纳的最大元素数量(总容量)。
- 作为
11.empty 成员函数
bool empty()
- 此函数用于判断容器是否为空,返回值类型为
bool
。 - 函数体
return _finsh == _start;
,当表示已使用数据末尾位置的迭代器_finsh
和起始位置迭代器_start
相等时,意味着容器中没有存储任何元素,此时返回true
;否则返回false
。
- 此函数用于判断容器是否为空,返回值类型为
12.reserve 成员函数
void reserve()
内存分配:
- 使用
new T[n]
进行类型安全的对象构造 - 相较
memcpy
或原始内存操作,确保非平凡类型正确初始化
数据迁移:
- 通过赋值操作符
tmp[i] = _start[i]
逐个迁移元素 - 支持包含资源管理的复杂对象(如
std::string
、智能指针) - 避免浅拷贝风险,保证深拷贝语义
指针更新:
_start = tmp
:指向新内存起始位置_finish = _start + old_size
:维持原有元素数量不变_endofstorage = _start + n
:标记新的容量上限
异常安全:
- 若元素赋值期间抛出异常:
- 新内存自动释放(通过作用域内临时指针
tmp
) - 旧内存
_start
保持完整,容器状态不变
- 新内存自动释放(通过作用域内临时指针
- 提供基本异常安全保证(Basic Guarantee)
void push_back(const T x)
{
if (_finsh == _endofstorage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finsh = x;
_finsh++;
}
void pop_back()
{
assert(!empty());
--_finsh;
}
void insert(iterator pos, const T x)
{
assert(pos>= _start && pos <= _finsh);
if (_finsh == _endofstorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;//更新扩容之后的pos
}
iterator end = _finsh - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
}
void erase(iterator pos)
{
assert(pos >= _start && pos < _finsh&&!empty());
iterator it = pos+1;
while (it < _finsh)
{
*(it-1) = *it;
++it;
}
--_finsh;
}
以下是对新增代码部分的分析:
13.push_back
成员函数
- 功能:向容器尾部添加一个元素。
- 实现逻辑:
- 容量检查:若当前已使用空间(
_finsh
)等于总容量(_endofstorage
),则调用reserve
扩容。首次扩容为4,后续每次扩容为当前容量的2倍。 - 元素添加:将元素
x
赋值到_finsh
指向的位置,并将_finsh
指针后移一位。
- 容量检查:若当前已使用空间(
- 注意点:使用
const T x
接收参数,避免修改传入值。
14.pop_back
成员函数
- 功能:删除容器尾部的元素。
- 实现逻辑:
- 空容器检查:使用
assert
确保容器非空。 - 指针调整:将
_finsh
指针前移一位,逻辑上删除最后一个元素。
- 空容器检查:使用
- 注意点:
- 未释放内存,仅调整指针。
- 若容器为空时调用,
assert
会触发程序终止(调试模式下)。
15.insert
成员函数
- 功能:在指定位置
pos
前插入元素x
。 - 实现逻辑:
- 边界检查:确保
pos
在有效范围内(_start
到_finsh
之间)。 - 容量检查与扩容:若容量不足,先扩容。扩容后需重新计算
pos
位置(因内存可能已重新分配)。 - 元素后移:从
_finsh-1
开始,将元素依次后移一位,直至pos
。 - 插入元素:在
pos
位置赋值x
。
- 边界检查:确保
- 注意点:
- 扩容后需更新
pos
指针,否则可能导致野指针。 - 时间复杂度为 O(n),适用于少量插入操作。
- 扩容后需更新
16.erase
成员函数
- 功能:删除指定位置
pos
的元素。 - 实现逻辑:
- 边界检查:确保
pos
在有效范围内(_start
到_finsh-1
)且容器非空。 - 元素前移:从
pos+1
开始,将后续元素依次前移一位,覆盖pos
位置的元素。 - 指针调整:将
_finsh
指针前移一位。
- 边界检查:确保
- 注意点:
- 未释放内存,仅覆盖元素。
- 时间复杂度为 O(n),适用于少量删除操作。
void swap(vector<T> v)
{
std::swap(_start, v._start);
std::swap(_finsh, v.finsh);
std::swap(_endofstorage, v._endofstorage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i)const
{
assert(i < size());
return _start[i];
}
};
}
17.赋值运算符 operator=
- swap 函数:借助交换内部指针(
_start
、_finish
、_endofstorage
) ,达成常数时间的容器内容交换,高效且直接。 - 拷贝并交换惯用法 :
- 赋值运算符(
operator=
)采用值传递参数方式,利用类的拷贝构造函数自动创建临时对象。 - 随后调用
swap
函数交换资源,这种方式确保了自我赋值安全(即便对象给自己赋值也不会出错),同时提供异常安全保证(若在交换过程中出现异常,原对象状态可维持不变 )。
- 赋值运算符(
18.重载下标运算符 operator[]
为自定义 vector
容器实现下标访问功能,类似原生数组的 []
操作,分为两种版本以适配不同场景:
18.1.非 const 版本
- 适用对象:非 const 修饰的
vector
对象。 - 功能:允许对容器中的元素进行读写操作。
- 参数:索引值
i
(无符号整数类型),表示要访问的元素位置。 - 实现逻辑:
- 先检查索引
i
是否在有效范围内(即i
小于当前容器的元素数量),若越界则在调试模式下触发错误提示。 - 直接返回容器中第
i
个元素的引用,通过这个引用可以直接修改元素的值。
- 先检查索引
- 用途:支持类似“
容器名[索引] = 值
”的赋值操作,比如vec[2] = 10
。
18.2.const 版本
- 适用对象:被 const 修饰的
vector
对象(即不能修改的容器)。 - 功能:仅允许对容器中的元素进行读取操作,不允许修改。
- 参数:与非 const 版本相同,即索引值
i
。 - 实现逻辑:
- 同样先检查索引
i
的有效性,确保不越界。 - 返回容器中第
i
个元素的常量引用,通过这个引用只能读取元素值,无法修改。
- 同样先检查索引
- 用途:支持类似“
变量 = 容器名[索引]
”的只读访问,比如int x = const_vec[1]
。
18.3.设计特点
- 版本区分:两个版本通过是否带
const
修饰符区分,编译器会根据容器对象是否为const
自动选择对应的版本。 - 直接访问:通过元素在内存中的起始位置和索引计算偏移量,直接定位到目标元素,访问效率极高(和原生数组一样快)。
- 安全性:通过检查索引是否在有效范围内(调试模式下)避免越界访问,但这种检查仅在调试时生效,正式环境中需额外处理。
- 灵活性:非 const 版本支持修改元素,const 版本仅允许读取,既满足了修改需求,又保证了 const 对象的不可变性。
二.test.cpp
#include "vector.h"
// 不展开yl命名空间,避免与其他命名空间冲突
// 辅助函数:打印vector状态
template<typename T>
void print_vector(const yl::vector<T>& v, const std::string& name) {
std::cout << "[" << name << "] size:" << v.size()
<< " capacity:" << v.capacity() << " elements: ";
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
}
// 1. 测试各类构造函数
void test01() {
std::cout << "=== 测试构造函数 ===" << std::endl;
// 1.1 默认构造
yl::vector<int> v1;
assert(v1.size() == 0 && v1.capacity() == 0 && v1.empty());
// 1.2 初始化列表构造
yl::vector<int> v2 = {1, 2, 3, 4};
assert(v2.size() == 4 && v2[2] == 3);
// 1.3 n个值构造(size_t版本)
yl::vector<int> v3(5, 10);
assert(v3.size() == 5 && v3[3] == 10);
// 1.4 迭代器范围构造
yl::vector<int> v4(v2.begin(), v2.end());
assert(v4.size() == 4 && v4[0] == 1);
print_vector(v4, "v4(迭代器构造)");
std::cout << "test01 测试通过!\n" << std::endl;
}
// 2. 测试push_back/pop_back
void test02() {
std::cout << "=== 测试push_back/pop_back ===" << std::endl;
yl::vector<int> v;
v.push_back(10);
v.push_back(20);
assert(v.size() == 2 && v[1] == 20);
v.pop_back();
assert(v.size() == 1 && v[0] == 10);
print_vector(v, "v(push_back后)");
std::cout << "test02 测试通过!\n" << std::endl;
}
// 3. 测试reserve/resize
void test03() {
std::cout << "=== 测试reserve/resize ===" << std::endl;
yl::vector<int> v;
v.push_back(1);
// 测试reserve(只改容量,不改大小)
v.reserve(10);
assert(v.capacity() >= 10 && v.size() == 1);
// 测试resize(扩大并填充)
v.resize(5, 30);
assert(v.size() == 5 && v[4] == 30);
// 测试resize(缩小截断)
v.resize(3);
assert(v.size() == 3);
print_vector(v, "v(resize后)");
std::cout << "test03 测试通过!\n" << std::endl;
}
// 4. 测试insert/erase
void test04() {
std::cout << "=== 测试insert/erase ===" << std::endl;
yl::vector<int> v = {1, 2, 3, 4};
// 测试insert
v.insert(v.begin() + 1, 99);
assert(v.size() == 5 && v[1] == 99);
// 测试erase
v.erase(v.begin() + 3);
assert(v.size() == 4 && v[3] == 4);
print_vector(v, "v(insert/erase后)");
std::cout << "test04 测试通过!\n" << std::endl;
}
// 5. 测试operator[]访问
void test05() {
std::cout << "=== 测试operator[] ===" << std::endl;
yl::vector<int> v = {10, 20, 30};
// 测试修改
v[1] = 999;
assert(v[1] == 999);
// 测试常量对象访问
const yl::vector<int> cv = {100, 200};
assert(cv[0] == 100);
std::cout << "test05 测试通过!\n" << std::endl;
}
// 6. 测试clear/swap
void test06() {
std::cout << "=== 测试clear/swap ===" << std::endl;
yl::vector<int> v = {1, 2, 3};
// 测试clear(清空元素,保留容量)
v.clear();
assert(v.size() == 0 && v.capacity() != 0);
// 测试swap
yl::vector<int> v2 = {100, 200};
v.swap(v2); // 调用yl::vector的swap成员函数
assert(v.size() == 2 && v[0] == 100);
print_vector(v, "v(swap后)");
std::cout << "test06 测试通过!\n" << std::endl;
}
// 7. 测试拷贝构造和赋值运算符
void test07() {
std::cout << "=== 测试拷贝构造和赋值 ===" << std::endl;
yl::vector<int> v = {10, 20, 30};
// 测试拷贝构造
yl::vector<int> v_copy(v);
assert(v_copy.size() == 3 && v_copy[1] == 20);
// 测试赋值运算符
yl::vector<int> v_assign;
v_assign = v;
assert(v_assign.size() == 3 && v_assign[2] == 30);
std::cout << "test07 测试通过!\n" << std::endl;
}
// 8. 测试析构函数(间接验证)
void test08() {
std::cout << "=== 测试析构函数 ===" << std::endl;
// 动态创建对象,手动析构(验证无崩溃)
yl::vector<int>* v = new yl::vector<int>{1, 2, 3};
delete v; // 若析构函数有误,此处会崩溃
std::cout << "test08 测试通过!\n" << std::endl;
}
int main() {
// 依次执行所有测试
test01();
test02();
test03();
test04();
test05();
test06();
test07();
test08();
std::cout << "所有测试执行完毕!" << std::endl;
return 0;
}
1.代码优点:
- 功能完整性:覆盖了自定义
vector
的核心功能,包括构造函数、元素操作、容量管理、迭代器、拷贝/赋值等,测试全面。 - 模块化设计:采用
test01()
至test08()
的封装方式,每个函数专注测试一类功能,逻辑清晰,便于维护和定位问题。 - 边界条件验证:针对常见边界情况(如空容器、容量扩展、自赋值等)进行了测试,增强代码健壮性。
- 命名空间管理:通过显式使用
yl::vector
而非展开命名空间,保持命名空间隔离,避免潜在冲突。 - 异常安全测试:通过动态内存分配(如
new/delete
)验证析构函数的正确性,确保无内存泄漏。
2.潜在问题:
- 原代码依赖问题:测试代码依赖原自定义
vector
实现,但原代码存在以下问题:- 拼写错误:
_finsh
应为_finish
,v.finsh
应为v._finish
。 - swap参数问题:
swap(vector<T> v)
采用值传递,会触发拷贝构造,建议改为引用传递swap(vector<T>& v)
。
- 拼写错误:
- 测试覆盖不足:
- 未测试移动构造函数和移动赋值运算符(若原代码实现了这些功能)。
- 对迭代器失效场景(如插入/删除后迭代器是否失效)验证不足。
- 断言局限性:依赖
assert
进行验证,在发布版本中可能被忽略,建议结合更完善的测试框架(如Google Test)。
3.优化建议:
- 修正原代码问题:
- 修复拼写错误和容量计算逻辑。
- 将
swap
参数改为引用传递,避免不必要的拷贝。
- 增强测试覆盖:
- 添加移动语义测试(如
std::move
)。 - 验证迭代器失效规则(如插入后原迭代器是否仍有效)。
- 添加移动语义测试(如
- 引入测试框架:使用专业测试框架替代简单的
assert
,提供更详细的测试报告和错误信息。 - 性能测试:对关键操作(如扩容策略、批量插入)进行性能测试,确保符合预期。
4.总结:
该测试代码为自定义vector
提供了全面的功能验证框架,结构清晰且模块化,但需注意原代码存在的拼写和逻辑错误。通过修正原代码问题并扩展测试覆盖,可以进一步提升代码质量和可靠性。