赞
踩
上一节我们讲了vector的基本使用,现在我们讲解vector的模拟实现,其中有三大重难点:
1.vector是如何进行设计与封装的
2.迭代器失效问题
3.memcpy,memmove导致的浅拷贝问题
#include <iostream> #include <assert.h> using namespace std; namespace myvector { template<class T> class vector { public: // Vector的迭代器是一个原生指针 typedef T* iterator; typedef const T* const_iterator; private: iterator _start; // 开始位置 iterator _finish; // 结束位置 iterator _endofstorage; // end of storage }; }
在 vector 类中,我们通常会使用_指针_来表示迭代器,因为指针天然支持指针算术运算和解引用操作,可以方便地遍历和访问元素。使用 typedef 定义迭代器类型可以使代码更加灵活和可维护。
使用 iterator 类型有以下几个原因:
_start:
_finish:
_endofstorage:
所以由图我们可以知道:_size=_finish-_start
_capacity=_endofstorage-_start
构造函数有三类:无参构造函数,带参构造,迭代器区间构造放在 二:迭代器的实现 中讲述。
//无参默认构造 vector() :_start(nullptr) , _finish(nullptr) , _endofstorage(nullptr) { } // 带参构造函数,创建包含 n 个 val 值的 vector vector(size_t n, const T& val = T()) { resize(n, val); } //带参构造(int) vector(int n, const T& val = T()) { resize(n, val); }
关于 const T& value = T() 作为默认参数的详细解释,我们需要理解以下几个概念:默认参数、匿名对象、以及如何结合使用它们。
默认参数
默认参数允许函数在调用时省略某些参数。编译器会使用提供的默认值来填补这些被省略的参数。对于构造函数来说,默认参数提供了一种灵活的初始化方式。
匿名对象
匿名对象是指没有绑定到任何变量的临时对象。在 C++ 中,可以通过直接调用构造函数来创建匿名对象。例如,T() 就是一个创建类型为 T 的默认构造对象的表达式。
const T& value = T()
现在,让我们将这些概念结合起来看 const T& value = T() 是如何工作的。
思考一下,如果不加vector(int n, const T& val = T()),下面这个代码是否有问题?
vector<string> v1(5, "1111");
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
vector<int> v2(5, 1);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
上面这个代码看似没问题,如果没有vector(int n, const T& val = T())就会报错
原因如下图:
v2优先匹配上迭代器构造,此时会造成 非法间接寻址 错误
再举另外一个例子vector<int> v(10, 5);
编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型
就不会走vector(size_t n, const T& value = T())这个构造方法,最终选择的是:vector(InputIterator first, InputIterator last)。
因为编译器觉得区间构造两个参数类型一致,因此编译器就会将InputIterator实例化为int,但是10和5根本不是一个区间,编译时就报错了,故需要增加该构造方法
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
根据上面的图,很容易的理解size()和capacity()
void push_back(const T& x) { if (_finish == _endofstorage)//如果满了则要扩容 { size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; //如果 capacity() == 0,则让 capacity()=4, //如果 capacity() !=0 ,则让 capacity()=capacity()*2 reserve(newcapacity); } *_finish = x; ++_finish; } void pop_back() { assert(size()>0); --_finish; }
//返回 T& 是因为 operator[] 旨在提供对容器元素的直接访问和修改能力,而不仅仅是读取元素的值。
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
vector<int> v(10, 0);
v[0] = 5; // 可以通过 T& 引用修改值
int x = v[1]; // 读取值
//普通对象的迭代器:提供对容器元素的读写访问。 //begin iterator begin() { return _start; } //end iterator end() { return _finish; } //常量对象的迭代器:提供对容器元素的只读访问。 const_iterator begin() const { return _start; } const_iterator end() const { return _finish; }
测试一下
void test_vector10() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); for (size_t i = 0; i < v.size(); i++) { cout << v[i] << " "; } cout << endl; vector<int>::iterator it = v.begin(); while (it != v.end()) { cout << *it << " "; it++; //注意不要写成*it++ } cout << endl; }
template <class InputInterator>
vector(InputInterator first,InputIterator last)
{
while(first!=last)
{
push_back(*first);
++first;
}
}
详细讲解:
举个例子应用一下:
void test_vector10()
{
int arr[5] = { 2,3,4,5,6 };
vector<int>v1(arr, arr + 5);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
运行结果如下:
此时的first就是arr,last就是arr+5
在动态数组的 reserve 函数中,如果新预留的空间 n 大于当前的容量 capacity(),则需要进行扩容操作。代码如下:
void reserve(size_t n) { if (n > capacity()) { //1.申请新空间 T* tmp = new T[n]; //2.将原有数据拷贝到新空间当中 memmove(tmp, _start, sizeof(T) * size()); //3.释放原有空间 delete _start; //4.指向新空间 _start = tmp; _finish = _start + size(); _endOfStorage = _start + capacity(); } }
乍一看,这段代码似乎是正确的,然而实际上存在一个严重的问题:野指针问题。在扩容之后,_finish 和 _endOfStorage 仍然指向旧空间的对应位置。
问题分析:
我们来详细分析这段代码的执行过程:
问题出在第四步,虽然 _start 更新到了新空间,但 _finish 和 _endOfStorage 指向的是新空间中的不正确位置,因为它们是基于旧空间的 size 和 capacity 计算的。
解决方法:
我们需要在释放旧空间之前保存旧空间的 size,并在新空间中重新计算 _finish 和 _endOfStorage。
正确的实现:
下面是修改后的 reserve 函数,解决了野指针问题:
void reserve(size_t n) { if (n > capacity()) { //1.保存原有空间的size size_t oldSize = size(); //2.开辟新空间 T* tmp = new T[n]; //3.将原有空间的数据拷贝到新空间当中 memmove(tmp, _start, sizeof(T) * oldSize); //4.释放旧空间 delete[] _start; //5.指向新空间 _start = tmp; _finish = _start + oldSize; _endOfStorage = _start + n; } }
详细步骤:
void reserve(size_t n) { if (n > capacity()) { //1.保存原有空间的size size_t oldSize = size(); //2.开辟新空间 T* tmp = new T[n]; //3.将原有空间的数据拷贝到新空间当中 memmove(tmp, _start, sizeof(T) * oldSize); //4.释放旧空间 delete[] _start; //5.指向新空间 _start = tmp; _finish = _start + oldSize; _endOfStorage = _start + n; } }
这段代码在处理内置类型(如 int
)时没有问题,但当处理自定义类型(如 std::string
)时会导致浅拷贝问题,进而引发内存管理错误。
问题分析:
浅拷贝问题:
浅拷贝是指直接拷贝对象的内存内容,而不考虑对象中的指针和动态分配的内存。这在处理内置类型时通常是安全的,但对于包含指针或动态内存的自定义类型,会导致多个对象指向同一块内存,从而引发多次释放同一内存的问题。
例如,当 vector
存储 std::string
类型时:
void test_vector10()
{
vector<string> v1;
v1.push_back("abc");
v1.push_back("def");
v1.push_back("exd");
for (auto& e : v1)
{
cout << e << " ";
}
cout << endl;
}
在 reserve
扩容后,浅拷贝会导致同一块内存被多个 string
对象引用,最终导致程序崩溃和断言错误。
解释如图(放大观看):
解决方法
我们需要避免浅拷贝,确保在扩容时进行深拷贝。对于自定义类型,使用赋值运算符重载来实现深拷贝是关键。
下面是修改后的 reserve
函数,解决了浅拷贝问题:
void reserve(size_t n) { if (n > capacity()) { // 提前保存偏移量oldSize size_t oldSize = size(); // 1.申请新空间 T* tmp = new T[n]; // 2.将原有空间中的数据拷贝到新空间当中 for (size_t i = 0; i < oldSize; i++) { tmp[i] = _start[i]; // 内置类型直接赋值,自定义类型调用赋值运算符重载,进行深拷贝 } // 3.释放原有空间 delete[] _start; // 4.将_start, _finish, _endOfStorage都指向到新空间 _start = tmp; _finish = _start + oldSize; _endOfStorage = _start + n; } }
详细步骤
size
:size_t oldSize = size();
T* tmp = new T[n];
for (size_t i = 0; i < oldSize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
,_finish = _start + oldSize;
,_endOfStorage = _start + n;
_start 是指向动态数组首元素的指针。我们可以通过下标运算符([])来访问动态数组中的元素。例如,_start[i] 表示数组中的第 i 个元素。指针和数组在 C++ 中有很多相似之处,特别是在内存访问上。
当我们使用 _start[i] 时,相当于对指针进行偏移访问。这是因为 T* _start 是指向类型 T 的指针,_start[i] 等价于 *(_start + i),即访问 T 类型数组的第 i 个元素。
假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
int main()
{
myvector::vector<myvector::string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}
问题分析:
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
先看看insert的代码
void insert(iterator pos, const T& x) { assert(pos >= _start && pos <= _finish); // 确保插入位置合法 if (_finish == _endofstorage) // 如果存储空间已满 { reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩充存储空间 } iterator end = _finish - 1; // 初始化end为最后一个元素的位置 while (end >= pos) // 从后向前移动元素,直到插入位置 { *(end + 1) = *end; // 将元素向后移动一位 end--; // 继续向前移动 } *pos = x; // 在插入位置插入新元素 ++_finish; // 更新_finish指针 }
我们来测试一下:
结果如下:
为什么呢?
内部迭代器失效指的是,当容器进行某些操作(例如内存重新分配)时,容器内部的迭代器(即正在被容器使用或操作的迭代器)由于这些操作而失效。在你的代码中,pos 作为插入操作中的一个内部迭代器,在调用 reserve 扩充存储空间时,可能会因为内存重新分配而失效。这种失效主要发生在容器的实现内部,开发者需要在实现容器操作时特别注意。
迭代器失效问题:
解决办法
在调用 reserve 之后,重新计算 pos 和 end 的位置,确保插入操作在新的存储位置上进行。使用更通用和安全的方法来处理迭代器操作。
void insert(iterator pos, const T& x) { assert(pos >= _start); assert(pos <= _finish); // 检查是否需要增容 if (_finish == _endofstorage) { // 扩容会导致迭代器失效,扩容需要更新一下 pos size_t len = pos - _start; reserve(capacity() == 0 ? 4 : capacity() * 2); pos = _start + len; } // 移动数据 iterator end = _finish - 1; while (end >= pos) { *(end + 1) = *end; end--; } // 插入数据 *pos = x; _finish++; }
我们运行一下看看
这样就没问题啦!
但是!这段代码其实还是存在问题的
外部迭代器失效指的是,容器外部的代码在使用容器的迭代器时,由于容器内部发生了某些操作(如内存重新分配、元素删除等),导致这些外部使用的迭代器变得无效。这通常发生在用户代码中,当用户在迭代容器并同时对容器进行修改时,外部的迭代器会失效。
看看下面这个测试函数能不能通过?
void test_vector10()
{
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
vector<int>::iterator it = v.begin();
v.insert(it, 99); // 插入 99
v.insert(it + 1, 111); // 再插入 111
for (auto e = v.begin(); e != v.end(); ++e) {
cout << *e << " ";
}
cout << endl;
}
调试一下
详细解释
push_back
时会将容量扩展为4。容量: 4
内容: [10, 20, 30, 40]
v.insert(it, 99)
时,it
是 v.begin()
,即指向10的位置。insert
过程中,如果容量不够,会调用 reserve
增加容量(假设容量增加到8)。it
仍然指向新分配内存中的位置,但是**外部的 ****it**
迭代器并没有被更新,它仍然指向旧的内存位置,这就导致了 it
成为野指针。容量: 8
内容: [99, 10, 20, 30, 40]
v.insert(it + 1, 111)
时,it + 1
是一个野指针,指向旧内存位置,这会导致未定义行为,程序可能崩溃或插入失败。解决问题
返回有效迭代器
在 insert
方法中返回插入位置的新迭代器,并在调用代码中使用返回的迭代器进行后续操作。
/* 插入 */ iterator insert(iterator pos, const T& x) { assert(pos >= _start); assert(pos <= _finish); // 检查是否需要增容 if (_finish == _endofstorage) { // 扩容会导致迭代器失效,扩容需要更新一下 pos size_t len = pos - _start; reserve(capacity() == 0 ? 4 : capacity() * 2); pos = _start + len; } // 移动数据 iterator end = _finish - 1; while (end >= pos) { *(end + 1) = *end; end--; } // 插入数据 *pos = x; _finish++; return pos; }
调用代码使用返回的迭代器
总结
通过在 insert
方法中返回有效的迭代器,并在调用代码中使用返回的迭代器,可以有效避免迭代器失效问题。这种方法确保即使容器内部进行了扩容操作,外部代码仍然可以使用有效的迭代器进行后续操作,从而避免未定义行为和潜在错误。
erase 代码:
void erase(iterator pos)
{
assert(pos <= _finish);
assert(pos >= _start);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
}
问题分析
在原始的 erase
函数中,当我们删除一个元素时,容器会重新排列后续的元素以填补被删除元素的位置。这种操作会使得原本指向删除位置的迭代器 pos
失效。失效的迭代器在后续操作中继续使用,可能会导致程序出现未预期的行为。
具体来说,在下面的测试函数 test_vector8
中:
void test_vector10() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(4); v.push_back(5); vector<int>::iterator it = v.begin(); //删除所有偶数 while (it != v.end()) { if (*it % 2 == 0) { v.erase(it); } ++it; } for (auto e : v) { cout << e << " "; } cout << endl; }
删除偶数的过程中,存在以下几种情况:
[1, 2, 3, 4, 5]
:程序运行正常,但这只是一个巧合,因为被删除的元素后面恰巧是一个非偶数。[1, 2, 3, 4]
:程序崩溃。删除最后一个偶数后,迭代器 pos
已经失效,再次 pos++
可能导致迭代器越界。[1, 2, 4, 5]
:程序结果不正确。删除一个偶数后,迭代器 pos
指向了下一个元素,此时由于连续的偶数,第二个偶数没有被判断和删除。分析具体情况
[1, 2, 3, 4, 5]
:在第一次删除 2
后,容器重新排列,3
移到原本 2
的位置,迭代器 pos
失效,但因为 pos++
后指向了 3
(新位置),后续操作没有影响,这种情况是巧合。[1, 2, 3, 4]
:删除 2
后,3
移到原来 2
的位置,再次 pos++
指向了 4
。删除 4
后,迭代器指向了 _finish
,再 pos++
会越过容器的边界,导致程序崩溃。[1, 2, 4, 5]
:删除 2
后,4
移到原来 2
的位置,再次 pos++
指向了 5
,跳过了 4
,导致 4
未被删除。根本原因
erase(pos)
操作后,pos
不再指向有效的位置。容器内存重新排列后,pos
的位置已经失效,继续使用失效的迭代器会导致未定义行为。pos
应该被更新为指向下一个有效的位置,而不是简单地递增 pos++
。直接递增失效的迭代器会导致逻辑错误和潜在的崩溃。vector
实现可能会在 erase
操作中进行内存的重新分配(虽然大多数实现不会),这会导致所有迭代器失效,操作失效的迭代器会引发更严重的问题。通过修改 erase
函数使其返回删除操作后下一个有效的迭代器,并在删除操作后更新迭代器来解决这些问题,可以确保程序正确运行。
iterator erase(iterator pos) {
assert(pos >= _start);
assert(pos <= _finish);
iterator begin = pos + 1;
while (begin < _finish) {
*(begin - 1) = *begin;
begin++;
}
_finish--;
return pos;
}
通过这种方式,我们可以确保在删除元素后,迭代器始终指向下一个有效的位置,从而避免迭代器失效导致的未定义行为和程序崩溃。
iterator erase(iterator pos) { assert(pos <= _finish); assert(pos >= _start); iterator begin = pos + 1; while (begin < _finish) { *(begin - 1) = *begin; begin++; } _finish--; return pos; } void test_vector10() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(4); v.push_back(5); vector<int>::iterator it = v.begin(); // 删除所有偶数 while (it != v.end()) { if (*it % 2 == 0) { it = v.erase(it); } else { ++it; } } for (auto e : v) { cout << e << " "; } cout << endl; }
最终结果为 [1, 3, 5],正确删除所有偶数,问题解决。
void resize(size_t n,const T& val= T()) { //如果没传实参,就是一个半缺省,缺省值就是一个匿名对象,那就看默认构造的是什么 if(n>size()) { reserve(n); while(_finish<_start+n) { *_finish=val; ++_finish; } } else {//直接改变_finish 相当于删除 _finish=_start+n; } }
为什么缺省参数要这样写?
兼容模板
在模板类中,成员函数需要适应各种类型的参数。默认构造函数 T() 适用于大多数类型,因此能够确保 resize 函数可以正确处理不同类型的对象。例如:
对于基本类型(如 int),T() 将初始化为 0。
对于自定义类型,如果提供了默认构造函数,T() 将调用该构造函数。
对于指针类型,T() 将初始化为 nullptr。
缺省参数的灵活性
缺省参数 val = T() 允许用户在调用 resize 函数时选择性地提供填充值。如果用户没有提供,resize 函数将使用 T() 作为默认值,这样可以简化函数调用,并使代码更具通用性。
传统写法:
//1.开空间 2.拷贝数据 3。释放原有空间 4.指向新空间 vector <T>& operator=(const vector<T>& v) { if (this != &v) { T* tmp = new T[v.capacity()]; for (int i = 0, i < v.size(), i++) { tmp[i] = v._start[i]; //_start 是一个指向动态分配数组首元素的指针。这个指针类似于数组的首地址,因此可以通过索引访问数组中的元素,就像使用数组一样 } delete[] _start; _start = tmp; _finish = _start + v.size(); _endofstorage = _start + v.capacity(); } return *this; }
现代写法
void swap(const vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstroage, v._endofstroage);
}
vector <T>& operator=(const vector<T> v)
{
swap(v);
return *this;
}
现代写法主要采用了一个名为“拷贝并交换”(copy and swap)的惯用法。这个方法简化了赋值运算符的实现,使代码更易于编写和维护,同时也提高了代码的异常安全性。让我们详细解释一下现代写法的思路以及为什么传参时不能使用引用。
步骤解释:
v
,然后在函数体内操作这个副本。这会自动调用 vector
的拷贝构造函数来创建一个临时对象。swap
函数交换当前对象和临时对象的内部数据指针。这样做的好处是交换操作只涉及指针交换(通常是常量时间操作),而不会涉及到实际数据的移动或拷贝。1. 异常安全性:
现代写法通过值传递来确保异常安全性。在值传递的过程中,会调用拷贝构造函数。如果这个拷贝构造函数抛出异常,赋值操作会被终止,并且不会改变现有对象的状态。这样可以避免资源泄漏或部分更新对象状态的不一致问题。
2. 确保交换的对象是副本:
通过值传递,在进入赋值运算符函数时,会创建一个参数对象的副本。如果我们使用引用传递,我们将直接操作原对象,这可能导致未定义的行为。比如,如果 v
和 this
引用的是同一个对象,swap(v)
将会交换同一个对象的数据,导致不可预测的行为。
3. 简化代码:
这种方法使得赋值运算符实现更加简洁,只需关注如何交换对象的内部数据,而不需要手动管理内存分配和释放。这也减少了代码出错的机会。
示例:
假设我们有如下对象:
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {4, 5, 6};
v1 = v2; // 调用赋值运算符
在赋值运算符内部,会创建一个 v2
的副本,然后 v1
的数据与这个副本进行交换。最终,v1
拥有 v2
的数据,临时副本在交换后被销毁,释放原 v1
的数据。
vector<T>& operator=(const vector<T>& v) {
vector<T> tmp(v); // 创建v的副本
swap(tmp); // 交换当前对象与临时副本的数据
return *this;
}
如果传递的是引用,那么传递的就是原对象。假设在某个操作中发生异常,可能导致原对象 v
状态不一致或资源泄漏。而通过值传递,创建一个临时副本,即使发生异常,临时副本的资源管理和释放都由其自身处理,原对象 v
不会受到影响。
综上所述,现代写法的主要思想是利用“拷贝并交换”技术,通过值传递参数来确保异常安全性、简化代码实现,并避免资源管理的复杂性。
#pragma once #include<assert.h> //底层实现 +做题 //02:22:12 题7. 电话号码字母组合OJ namespace myvector { template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; //无参默认构造 vector() :_start(nullptr) , _finish(nullptr) , _endofstorage(nullptr) { } //析构 ~vector() { if (_start) { delete[] _start; _start = _finish = _endofstorage = nullptr; } } //拷贝构造-两种写法 //1. //vector(const vector<T>& v) //{ // _start = new T[v.capacity()]; // memcpy(_start, v._start, v.size() * sizeof(T)); // _finish = _start + v.size(); // .... //} //2. //v2(v1); vector(const vector<T>& v) { reserve(v.capacity());//先给v2 开和v1同样大的空间 for (const auto& e : v)//取v里面的值依次赋值给e,加引用深拷贝,更好 { push_back(e); } } //赋值运算符重载 //传统写法: //1.开空间 2.拷贝数据 3。释放原有空间 4.指向新空间 vector <T>& operator=(const vector<T>& v) { if (this != &v) { T* tmp = new T[v.capacity()]; for (int i = 0, i < v.size(), i++) { tmp[i] = v._start[i]; //_start 是一个指向动态分配数组首元素的指针。这个指针类似于数组的首地址,因此可以通过索引访问数组中的元素,就像使用数组一样 } delete[] _start; _start = tmp; _finish = _start + v.size(); _endofstorage = _start + v.capacity(); } return *this; } //现代写法 void swap(const vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endofstroage, v._endofstroage); } vector <T>& operator=(const vector<T> v) { swap(v); return *this; } //迭代器区间构造. //构造函数允许你用任何支持迭代器的区间(例如其他容器中的元素)来初始化一个 vector 对象。 template<class InputIterator> vector(InputIterator first, InputIterator last) { while (first != last) { push_back(*first); ++first; } } 带参构造 vector(size_t n, const T& val = T()) { resize(n, val); } 带参构造(int) vector(int n, const T& val = T()) { resize(n, val); } //赋值-现代写法 //v1=v3; void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endofstorage, v._endofstorage); } vector<T>& operator = (vector<T> v) //为什么那里不能用引用,仔细想.02:06:13 { //如果用了引用就是 v1和v3交换,而不是v1和v3带来的那个临时变量v 交换 swap(v); return *this; } //普通对象的迭代器:提供对容器元素的读写访问。 //begin iterator begin() { return _start; } //end iterator end() { return _finish; } //常量对象的迭代器:提供对容器元素的只读访问。 const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } size_t size() const { return _finish - _start; } size_t capacity() const { return _endofstorage - _start; } //这里注意还是要判断n>capacity,因为reserve 也可能单独使用,不能搞成缩容了 //void reserve void reserve(size_t n) { if (n > capacity()) { size_t old = size();//保存当前大小,返回的size()是新的 T* tmp = new T[n];//分配新的内存 if (_start)//复制旧数据到新内存 { //memcpy(tmp, _start, old * sizeof(T)); memcpy会出问题,是一种浅拷贝test7(),02:03:28 for (size_t i = 0; i < old; i++) { tmp[i] = _start[i]; } delete[] _start; } //当你重新分配内存时,_finish 和 _endofstorage 的值会基于 size() 和 capacity() 更新 //更新指针 _start = tmp; _finish = _start + old; //将 _finish重新设置为旧大小的位置 //_finish=_start+size(); 错的 //删除 old 后,_finish 指针将指向新容量的末尾,而不是当前元素的末尾。这意味着 _finish 会指向一个未初始化的位置,导致对向量的操作(如 push_back)出现未定义行为。 _endofstorage = _start + n; // 将 _endofstorage 设置为新容量的位置 //_endofstorage=_start+capacity(); } } void resize(size_t n, T val = T()) {//三种情况假设有5个数据,容量为10.则有resize(3),resize(7),resize(14)三种情况 if (n > size()) { reserve(n); while (_finish < _start + n) { *_finish = val; ++_finish; } } else { _finish = _start + n; } } //为什么缺省参数能这样写? //兼容模板 // 在模板类中,成员函数需要适应各种类型的参数。默认构造函数 T() 适用于大多数类型,因此能够确保 resize 函数可以正确处理不同类型的对象。例如: // 对于基本类型(如 int),T() 将初始化为 0。 // 对于自定义类型,如果提供了默认构造函数,T() 将调用该构造函数。 // 对于指针类型,T() 将初始化为 nullptr。 //缺省参数的灵活性 // 缺省参数 val = T() 允许用户在调用 resize 函数时选择性地提供填充值。如果用户没有提供,resize 函数将使用 T() 作为默认值,这样可以简化函数调用,并使代码更具通用性。 // void push_back(const T& x) { if (_finish == _endofstorage) { size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; //如果 capacity() == 0,则让 capacity()=4, //如果 capacity() !=0 ,则让 capacity()=capacity()*2 reserve(newcapacity); } *_finish = x; ++_finish; } void pop_back() { assert(size()>0); --_finish; } //vector 不支持头插 头删 //pos位置之前插入x //下面这个代码多次运行会失效,原因:迭代器失效。 pos扩容后失效,及时更新即可。 // //void insert(iterator pos, const T& x) //{ // assert(pos >= _start && pos <= _finish); // 确保插入位置合法 // if (_finish == _endofstorage) // 如果存储空间已满 // { // reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩充存储空间 // } // iterator end = _finish - 1; // 初始化end为最后一个元素的位置 // while (end >= pos) // 从后向前移动元素,直到插入位置 // { // *(end + 1) = *end; // 将元素向后移动一位 // end--; // 继续向前移动 // } // *pos = x; // 在插入位置插入新元素 // ++_finish; // 更新_finish指针 //} /* 插入 */ //void insert(iterator pos, const T& x) { // assert(pos >= _start); // assert(pos <= _finish); // // 检查是否需要增容 // if (_finish == _endofstorage) { // // 扩容会导致迭代器失效,扩容需要更新一下 pos // size_t len = pos - _start; // reserve(capacity() == 0 ? 4 : capacity() * 2); // pos = _start + len; // } // // 移动数据 // iterator end = _finish - 1; // while (end >= pos) { // *(end + 1) = *end; // end--; // } // // 插入数据 // *pos = x; // _finish++; //} //解决: /* 插入 */ iterator insert(iterator pos, const T& x) { assert(pos >= _start); assert(pos <= _finish); // 检查是否需要增容 if (_finish == _endofstorage) { // 扩容会导致迭代器失效,扩容需要更新一下 pos size_t len = pos - _start; reserve(capacity() == 0 ? 4 : capacity() * 2); pos = _start + len; } // 移动数据 iterator end = _finish - 1; while (end >= pos) { *(end + 1) = *end; end--; } // 插入数据 *pos = x; _finish++; return pos; } //注意是 从pos起始位置往后的所有数据,挪到pos+1上 //memmove(pos + 1, pos, sizeof(T) * (_finish - pos)); //删除pos位置数据 //void erase(iterator pos) //{ // assert(pos >= _start && pos < _finish); // memmove(pos, pos + 1, sizeof(T) * (_finish - pos)); // --_finish; //} //void erase(iterator pos) //{ // assert(pos <= _finish); // assert(pos >= _start); // iterator begin = pos + 1; // while (begin < _finish) // { // *(begin - 1) = *begin; // begin++; // } // _finish--; //} iterator erase(iterator pos) { assert(pos >= _start && pos < _finish); iterator it = pos + 1; while (it < _finish) { *(it - 1) = *it; ++it; } _finish--; return pos; } //[]访问 //返回可修改的引用,支持读写操作 T& operator[](size_t pos) { assert(pos < size()); return _start[pos]; } //支持读取操作但不能修改。 const T& operator[](size_t pos) const { assert(pos < size()); return _start[pos]; } //返回类型为T&有以下几个原因: //方便读写操作 private: iterator _start=nullptr;//这个成员变量指向动态数组中第一个实际存储元素的内存位置。[ iterator _finish=nullptr;//指向当前已填充元素的末尾,即最后一个有效元素的下一个位置。) iterator _endofstorage=nullptr;//指向动态数组的末尾,即分配的内存块的最后一个位置。 // 1 2 3 4 空 空 // ↑_start ↑ ↑:_endofstorage // _finish }; void print_vector(const vector<int>& v) { //此处为什么不能用范围for? //因为const vector<int>& v 是const,迭代器也要用const的,所以要有const的begin和end for (auto e : v) { cout << e << " "; } cout << endl; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。