【C++STL】vector类的模拟实现(重新实现的版本)


Blog’s 主页: 白乐天_ξ( ✿>◡❛)
🌈 个人Motto:他强任他强,清风拂山冈!
🔥 所属专栏:C++深入学习笔记
💫 欢迎来到我的学习笔记!

一、框架建立

注意:模板是不能分离到两个文件的,会出现链接错误!

在上一篇文章【链接:】我们就已经知道了迭代器的原貌就是原生指针类型,因此我们也将_start_finish_end_of_storage定义成了三个迭代器类型。

// 定义一个类域
namespace Harper
{
	template<class T>
	class vector 
	{
		// typedef重定义迭代器:
		typedef T* iterator;
		typedef const T* const_iterator;

		// 主要的接口函数:
		// ...
	private:
		// 主要成员函数:
		iteartor _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

二、迭代器

  • 迭代器我们这里实现的是const版本非const版本的,反向版本的迭代器比较复杂,在这里就不实现了。
    1、普通对象的迭代器:
// 普通对象
iterator begin()
{
    return _start;
}
iterator end()
{
    return _finish;
    // 这里的end是指数据结束位置,而_end_of_storage是指空间结束位置		}
}

2、const对象的迭代器:

// const对象
const_iterator begin() const
{
    return _start;
}
const_iterator end() const
{
    return _finish;
}

三、容量

3.1 size、capacity

容量相关接口有size()capacity()

// 容量
size_t size()// 数据开始到结束的大小(总长)
{
    return _finish - _start;// ???
}
size_t capacity()
{
    return _end_of_storage - _start;// ???
}

画图示意:

在这里插入图片描述

  • size就相当于这个容器的数据个数,即_finish_start两个迭代器之间的距离。在此之前我们已经知道迭代器的底层就是指针,计算两个指针之间的数据个数只需要两个指针相减即可。
  • capacity表示整个容器的容量,即_end_of_storage - _start

3.2 reserve

扩容规则:

1、当n大于对象当前的capacity时,将capacity扩大到n或者大于n。

2、当n小于对象当前的capacity时,什么也不用做。

  • 首先在reserve函数中传入一个size_t类型的参数n,函数开始进行判断:如果传入的n值大于当前的容量(通过capacity()函数获取 ),才会执行扩容逻辑。
  • 在扩容逻辑内部,定义了一个类型为T*的临时指针tmp,使用new T[n]根据类型参数T开辟新的空间。如果原空间的起始指针_start不为空,就使用memcpy函数将元空间的数据(从start开始,拷贝size()T类型大小的数据)拷贝到新空间tmp中,然后释放原空间(delete[] _start)。
  • 最后更新成员变量,将_start指向新空间tmp_finish更新为_start + size()
  • 补充一点我一直不懂的: _finish指针表示数据的结束位置,它的值是相对于_start来确定的。 例如_finish可能被定义为_start + n(其中n表示已经存储的元素个数,这里是简化表达)。当_start被释放以后,_finish原本基于_start的相对计算就失去了意义,它仍然保持原来的值,那么_finish就变成了野指针。
// 扩容
void reserve(size_t n)
{
    if (n > capacity())
    {
        T* tmp = new T[n];// 开辟新空间给临时指针tmp
        if (_start)// _start不为空时
        {
            // 拷贝数据:从memcpy开始,拷贝size()个数据
            memcpy(tmp, _start, sizeof(T) * size());// 拷贝的数据个数???????
            delete[] _start;// 释放旧空间
        }
        _start = tmp;// 指向新空间
        _finish = _start + size();
        _end_of_storage = _start + n;// 
    }
}

但是这段代码还存在很多的漏洞! 主要是下面的两个方面:

  1. 内存管理方面
  • 浅拷贝与内存泄漏
memcpy(tmp, _start, sizeof(T) * size());delete[] _start;
  1. 如果T 是复杂对象(如包含指针成员),memcpy执行浅拷贝,只复制指针值。例如,T是一个包含动态分配数组指针的类。
    2. 假设T类有一个int*成员指向动态分配的整数数组。当使用memcpy拷贝时,只是复制了这个指针的值,新对象和原对象的这个指针成员会指向同一块内存。
    3. 然后执行delete[] _start释放原对象内存,新对象中的指针就成为悬空指针。后续使用这个悬空指针会导致未定义行为,并且原对象管理的数组内存被释放,新对象无法正确管理,造成内存泄漏。
  • 异常安全
T* tmp = new T[n];
  1. new T[n]分配内存失败(如系统内存不足),函数没有处理这种情况。
  2. 若内存分配失败,函数会直接抛出异常。如果之前已经执行了if (_start)中的部分代码(如memcpy),原空间_start的状态已被改变,会导致数据不一致和潜在资源泄漏。
    逻辑方面
  • size()函数调用
memcpy(tmp, _start, sizeof(T) * size());
- 在`memcpy`操作中使用`size()`确定拷贝字节数。
- 若`size()`依赖内部状态(如`_finish`和`_start`关系),在`reserve`函数改变容器内部结构时(如重新赋值`_start`之前)调用`size()`可能得到错误结果。调用`size()`函数,
  • 成员变量更新(空指针异常)
_finish = _start + size();
  • 调试可以发现,_finish的出现了问题,值为0X0000000,那么出错的地方应是在它的前面执行的代码上。调试进入扩容就可以将问题锁定在_finish = _start + size();这一句代码上。

在这里插入图片描述

  • 在更新_finish时,_finish = _start + size();可能不正确。原因在于size()
    - 因为size()结果在扩容前后含义或计算方式可能改变,扩容后size()可能未正确更新,导致_finish计算错误,影响后续操作(如push_back依赖_finish的逻辑)。
  • 说明:之前我们使用_finish - _start来计算size(),执行这句话的时候start已经发生变化了,因为我们开辟了一块新空间,但是这是_finish的值还是醉意开始的nullptr,那么size()计算出来的大小即为-_start,此时再和_start去做一个结合,抵消了就是0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

开始进行修改:

  1. _finish的修改更新
  • 解决办法一:先更新_finish:使用新开辟的空间tmp进行更新,在用tmp去更新_start,这样就不会出现问题了。

2:20:00不懂

_finish = tmp + size();
_start = tmp;// 这两句代码不能交换!!!!size()是有新旧的
_end_of_storage = _start + n;
  • 解决办法二: 我们可以在每次没开始扩容之前我们都可以去事先保存一下这个size(),后面的更新顺序就不需要发生变动了,在加的时候加上old_size即可。vector底层也是使用的这种方法。
void reserve(size_t n)
{
    //提前原本记录长度
    size_t old_size = size();
    if (n > capacity())
    {
        T* tmp = new T[n];
        if (_start)
        {
            //深拷贝
            for (size_t i = 0; i < size(); i++)
            {
                tmp[i] = _start[i];//赋值重载
            }
            delete[] _start;
        } 
        
        _start = tmp;
        _finish = _start + old_size;
        _end_of_storage = _start + n;
    }
}
  1. memcpy的修改
memcpy(tmp, _start, sizeof(T) * size());

我们此前已经知道在VS下对于每个string对象的大小都是固定的28Byte,即使是通过不同的构造形式构造出来的对象也是一样的。

在这里就发生了一个浅拷贝问题,导致delete[] _start处发生了一个并发修改问题。

在扩容的时候,我们去开辟了一块新的空间,使用memcpy()函数将数据原封不动地拷贝到另一块空间,再去做一个扩容。因为这个memcpy()原封不动拷贝的问题,就使得新空间和旧空间虽然是两块独立的空间,但是呢每个对象中的_str都和另一个对象指向了那一块同样的空间。
在这里插入图片描述

在接下来执行这句代码时,就会先去调用当前对象的析构函数将每一块空间中的内容先清理掉,然后再去调用delete释放掉整块空间。因为没量过对象所指向的空间都是同一块的,是所以在释放的时候就会造成同时修改的问题。

delete[] _start;

在这里插入图片描述

总结:vector是深拷贝,但是vector空间上存的对象是string的数组,使用memcpy()导致string对象的浅拷贝。

解决办法:换一个拷贝逻辑即可,不用memcpy了,而是使用下面这种方式来拷贝:

for (size_t i = 0; i < size(); i++)
{
	tmp[i] = _start[i];
}

下面就是完整的实现:

void reserve(size_t n)
{
	if (n > capacity())
	{
		// 先保存一下原先的size()
		size_t old_size = size();
		T* tmp = new T[n];// 开一块新空间
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * size());
			for (size_t i = 0; i < size(); i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + old_size;
		_end_of_storage = _start + n;
	}
}

3.3 push_back接口

  • push_back函数中,接受一个const T&类型的参数x。首先判断_finish是否等于_end_of_storage,如果相等,表示当前空间已满。
  • 若空间已满,计算新的容量newCapacity,如果当前容量为 0,则新容量设为 4,否则新容量为当前容量的 2 倍。然后调用reserve函数进行扩容。
  • 最后将参数x赋值给_finish指向的位置,并将_finish指针后移一位。
void push_back(const T& x)
{
    if (_finish == _end_of_storage)
    {
        size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newCapacity);
    }
    *_finish = x;
    _finish++;
}

运行结果:

在这里插入图片描述

  • push_back函数的修改
    • 在调用reserve后,添加了if (_start)的判断,确保_start不为空(即reserve成功执行)后再进行push_back的操作。这避免了在reserve失败时执行可能导致错误的操作。
// 改成一层模板,实例化,编译器自动推导传入参数的类型
template<class T>
void print_vector(const vector<T>& v)// const对象的迭代器,不能调用非const的成员函数
{
    // 打印输出v的内容
    vector<int>::const_iterator it = v.begin();
    while (it != v.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    for (auto e : v)// 支持了迭代器就支持了范围for
    {
        cout << e << " ";
    }
}

3.4 resize接口

void resize(size_type n, value_type = value_type());
  1. 先分类情况:
  • n < _finish的情况
    • n小于当前容器中的元素个数(即_finish_start之间的距离)时,直接将_finish指针移动到_start + n的位置,这意味着截断容器,使容器中的元素数量变为**n**。(相当于只留下前n个数据,后面的数据全部都删除掉)
  • n > _finish && n <= _end_of_storagen > _end_of_storage的情况
    • 这两种情况进行了合并处理。首先调用reserve函数检查是否需要扩容。如果n大于当前的_end_of_storage(容器容量),reserve函数会进行扩容操作以满足新的容量需求。
    • 在确保容量足够后(如果需要扩容已经完成扩容),通过循环将val(默认值或者传入的值)赋给从_finish开始到_start + n之间的元素,同时移动_finish指针,直到_finish到达_start + n的位置,从而将容器的元素数量调整为n

在这里插入图片描述

  • 代码如下:
void resize(size_t n, const T& val = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		// 先使用reserve()去检查一下是否需要扩容
		reserve(n);// 这里会进行检查:n > capacity就扩容
		while (_finish != _start + n)// 继续插入数据
		{
			*_finish = val;
			_finish++;
		}
	}
}
  1. 关于默认参数T()
const T& val = T()// T可能string、vector等自定义类型,也可能是int、double等内置类型
// 在这里就是调用默认构造函数
  • resize函数的参数const T& val = T()中,T()是一个默认缺省参数。由于形参val的类型是模板参数类型,采用自动推导形式。如果Tint的美女和内置类型时,我们在此之前就说:“内置类型没有构造函数的概念”,但是在这里为了实现兼容,内置类型便有了构造函数的概念。
void test_vector()
{
    // 相当于是构造出一个匿名对象
    int i = int();   // 0
    int j = int(1);  // 1
    int k(2);        // 2
}
  • T()在这里是一个匿名对象,它根据T的类型生成相应的默认值。不能简单地给0作为默认值,因为T的类型不一定是整型,通过T()可以根据不同的类型生成合适的默认值。

四、元素访问

  • 下标 +[]形式:
T& operator[](size_t pos)
{
	assert(pos < size());
	return _start[pos];
}

T& operator[](size_t pos) const
{
	assert(pos < size());
	return _start[pos];
}

五、修改操作

5.1 push_back接口

  • 扩容在VS编译器下呈现1.5倍的增长趋势,但是在g++编译器下是2倍扩容趋势,在这里扩容使用reserve来实现。前面已经讲解过了,可以往前看。
void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);
	}
	*_finish = x;
	_finish++;
}

5.2 insert接口与迭代器失效

pos位置插入元素x

void insert(iterator pos, const T& x)
  • 断言检查:
    • 首先assert断言pos为合法的迭代器,即pos_start_finish之间(包含两端)。
    • 这是因为pos是指向容器内部有效空间的迭代器(类似于地址),不同于string类中基于无符号整数的我只表示,这里不可能为0。
assert(pos >= _start);
assert(pos < _finish);
  • 扩容逻辑:
    • 如果容器已满(_finish == _end_of_storage),则复用push_back中的扩容逻辑。
    • 按照规则(容量为 0 时新容量设为 4,否则为当前容量的 2 倍)计算新容量并调用reserve函数进行扩容。
  • 数据挪动与插入:
    • 确定要挪动数据的范围,将_finish - 1作为末尾迭代器end。通过循环从后往前将元素依次后移一位(*(end + 1) = *end),直到end到达pos的位置。这样做可以避免覆盖数据。
    • 然后将元素x插入到pos位置(*pos = x),最后将_finish指针向后移动一位,表示容器中的元素数量增加了一个。
void insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);
	// 1.首先考虑扩容逻辑
	if (_finish == _end_of_storage)// 空间满了就扩容
	{
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);// reserve()函数
	}

	// 2.挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = x;
	++_finish;
}
  • 那么在push_back中就可以复用insert接口了。
void push_back(const T& x)
{
	/*if (_finish == _end_of_storage)
	{
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);
	}
	*_finish = x;
	_finish++;*/
	insert(end(), x);
}
  • 测试:
void testvector()
{
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    //v.push_back(5);

    print_vector(v);

    v.insert(v.begin() + 2, 30);

    print_vector(v);
}
  • 调试发现:出了问题,循环本应结束。却是在继续循环。其实这里就是典型的迭代器失效的问题。
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		memcpy(tmp, _start, size() * sizeof(T));
		delete[] _start;

		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}
  • 结合reserve()函数发现,这里扩容结束了,pos还指向原来的空间原来的位置,但是原空间已经被reserve()中的delete操作释放掉了,因此后续的数据挪动操作就错乱了。而且,这里的pos其实就是迭代器,因此有效迭代器pos的值不会为0。
  • 解决办法:记录好相对位置,更新pos
void insert(iterator pos, const T& x)
{
	if (_finish == _end_of_storage)
	{
		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;
}
  • 迭代器失效的原因:迭代器指向的位置的空间因为扩容等原因已经被释放掉了,因此使用时就要小心,避免迭代器失效的问题。
  • 此外,迭代器失效还有一种场景:给出一个x,要求在x之前进行插入数据,但是vector中没有提供find函数。因此这里就可以使用<algorithm>中的find模板函数。(C++中传入迭代器区间时都是要传入左闭右开区间的。)
void testvector()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	print_vector(v);

	v.insert(v.begin() + 2, 30);
	print_vector();

	int x;
	cin >> x;
	auto p = find(v.begin(), v.end(), x);
	if (p != v.end())
	{
        // insert以后就是迭代器失效,不要进行过访问
        v.insert(p, 40);
		(*p) *= 10;
	}
	print_vector(v);
}
  • 分析原因:insert操作以后pos由于扩容)就失效了,不要进行访问,pos还指向的是旧空间,但是旧空间已经被释放了,因此pos就变成了野指针,后续对它进行了访问,因此error。在此之前还有上面解决的迭代器失效,所以迭代器已经失效了一次,但是纠正过来了的。**由于行参改变不影响实参,pos改变了不影响p。因此这里仍然是一个迭代器失效。**这里不能加引用。

在这里插入图片描述

- `v.insert(v.bigin(),v.end(),x);`调用完`insert()`函数返回的都是临时变量,具有常性,不能给普通的引用,否则就是权限放大。
  • 所以解决办法就是:迭代器失效了,就不要再去访问p了。实在是需要进行访问的话,就要更新这个失效的迭代器的值。
void testvector()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	print_vector(v);
    
	v.insert(v.begin() + 2, 30);
	print_vector();

	int x;
	cin >> x;
	auto p = find(v.begin(), v.end(), x);
	if (p != v.end())
	{
		// insert以后就是迭代器失效,不要进行过访问
		//v.insert(p, 40);
		//(*p) *= 10;

		p = v.insert(p, 40);
		(*(p + 1)) *= 10;
	}
	print_vector(v);
}
  • 那如果没有发生扩容呢?没有发生扩容的情况下,执行(*P) *= 10;本来是想让2变成20,结果发现是40变成了400。原因就是位置意义变了:不同平台扩容时间不一样,不确定这里进行访问时访问的值是不是有效的。
void testvector()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	print_vector(v);

	//v.insert(v.begin() + 2, 30);
	//print_vector();

	int x;
	cin >> x;
	auto p = find(v.begin(), v.end(), x);
	if (p != v.end())
	{
		// insert以后就是迭代器失效,不要进行过访问
		v.insert(p, 40);
		(*p) *= 10;

		// p = v.insert(p, 40);
		// (*(p + 1)) *= 10;
	}
	print_vector(v);
}
  • 运行结果:

在这里插入图片描述

  • 做一个修改:
template<class Comtainer>
void print_container(const Container& v)
{
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
  • 在Linux下不会强制检查,这样可能导致:扩容后,修改了不知道谁的数据,但是仍然运行的,此时非常危险的,可能这个位置被分配给别人使用了。VS即使不扩容,也会报错,会更加安全。

5.3 erase接口

挪动数据覆盖。

注意:

1、一定要判断该位置是否还能被访问。

2、erase删除了数据会不会进行缩容?不会??????

// 删除pos位置的数据
void erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	for (size_t i = 0; i < pos; i++)
	{
		// 先挪动数据
		iterator it = pos + 1;
		while (it != end())
		{
			*(it - 1) = *it;
			++it;// 往前挪动数据
		}
		--_finish;
	}
}
  • VS下认为p指向的位置是失效的:

在这里插入图片描述

  • 测试:删除所有的偶数

void test_vector()
{
	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);

	print_container(v);

	// 删除所有的偶数:
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0) 
		{
			v.erase(it);
		}
		++it;
	}
	print_container(v);
}
  • 开始进行测试:

在这里插入图片描述

  • Linux也是一样的结果VS下进行了强制检查。
  • 总结:erase操作了还继续对迭代器进行访问。
  • 解决办法:已知erase指向新元素的位置即刚刚被删除元素的下一个位置。迭代器失效了就先利用返回值接收、更新一下,再去访问。注意:erase操作删除元素后,发生数据挪动,相当于已经间接++操作过了。

在这里插入图片描述

  • 改成下面这样:++操作走else,避免连续偶数被跳过。
void test_vector()
{
	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);

	print_container(v);

	// 删除所有的偶数:
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			++it;
		}
	}
	print_container(v);
}
  • 几乎所有的容器erase操作后,都认为迭代器失效了。string也有迭代器失效,但是不怎么用迭代器,用的基本上都是下标。

5.4 pop_back接口

  • 删除数据(尾删)。
bool empty()
{
    return _start == _finish;
}

void pop_back()
{
    assert(empty());
    --_finish;
}

六、默认成员函数

6.1 构造函数

6.1.1 基于resize复用的有参构造函数

  • 自定义vector类中的有参构造函数vector(size_t n, const T& val = T())通过复用resize函数来初始化容器。例如创建Harper::vector<int> v(10, 0);时,构造函数内部调用v.resize(10, 0)
// 有参构造
vector(size_t n, const T& val = T())
{
	resize(n, val);
}
  • 对于vector类中的_start_finish_end_of_storage这三个私有成员变量,它们在定义时被初始化为nullptr,避免内置类型未初始化的问题。

6.1.2 基于迭代器区间的构造函数

  • 下面的函数是通过迭代器区间初始化vector的构造函数原型。
template<class InputIterator> vector(InputIterator first, InputIterator last)
  • 举例:
template<class InputIterator>
vector(InputIterator first, InputIterator last) 
{
    while (first != last) 
    {
        push_back(*first);
        ++first;
    }
}
  • 可以用已存在的vector对象结合迭代器区间初始化新的vector对象。
Harper::vector<int> v2(v.begin(), v.end());
  • 也可用于string对象迭代器或数组指针的初始化。
string s("abcdef"); 
Harper::vector<int> v2(s.begin(), s.end());

int a[] = {1, 2, 3, 4}; 
Harper::vector<int> v2(a, a + 4);

6.1.3 构造函数调用歧义及解决

  • 当执行bit::vector<int> v5(10, 1);时,会出现 “非法的间接寻址” 问题。这是因为模板参数自动类型推导时,传入的101int类型,而原有的有参构造函数第一个形参为size_t类型,不会优先匹配该构造函数,而是可能错误匹配到迭代器区间构造函数(其参数为模板类型,匹配度更高)。
  • 通过重载有参构造函数,新增vector(int n, const T& val = T())版本:
vector(int n, const T& val = T()) 
{
    resize(n, val);
}
  • 这样就与原vector(size_t n, const T& val = T())形成重载关系,避免了调用歧义。同时,若要调用size_t类型的构造函数,可在参数后加u,如bit::vector<int> v6(10u, 6);

6.2 拷贝构造函数

  • 最初的拷贝构造函数vector(vector<int>& v)实现中存在浅拷贝问题,在调试时可发现。
  • 最初的代码如下:
vector(vector<int>& v) 
{
    _start = new T[v.capacity()];
    memcpy(tmp, v._start, sizeof(T) * v.size());
    _finish = tmp + v.size();
    _end_of_storage = tmp + v.capacity();
}
  • vector对象存储string数组时,memcpy会导致浅拷贝问题。
  • 正确的深拷贝实现方式是逐个拷贝元素:
vector(vector<T>& v) 
{
    _start = new T[v.capacity()];
    for (size_t i = 0; i < v.size(); i++) 
    {
        _start[i] = v._start[i];
    }
    _finish = _start + v.size();
    _end_of_storage = _start + v.capacity();
}
  • 也可复用reservepush_back接口实现拷贝构造函数:
vector(vector<int>& v) 
{
    // 根据v的capacity()去开出对应的空间
    reserve(v.capacity());
    for (size_t i = 0; i < v.size(); i++) 
    {
        push_back(v[i]);
    }
}

6.3 赋值重载函数

  • 赋值重载函数const vector<T>& operator=(vector<T> v)利用swap接口实现。通过传值传参,先调用拷贝构造函数创建临时对象,然后用swap交换临时对象和当前对象内容。临时对象出作用域后自动销毁。以下是代码:
const vector<T>& operator=(vector<T> v) 
{
    swap(v);
    return *this;
}
void swap(vector<T>& v)
{
    std::swap(_start);
    std::swap(_finish);
    std::swap(_end_of_storage); 
}
  • 在调试时可看到调用赋值重载函数前会先调用拷贝构造函数。

6.4 析构函数

  • 析构函数~vector()用于释放容器占用的空间,将资源归还给操作系统。代码如下:
~vector() 
{
    delete[] _start;
    _start = _finish = _end_of_storage = nullptr;
} 

七、打印输出函数

将打印输出函数设置成一个模板:

template<class T>
void print_vector(const vector<T>& v)
{
	vector<T>::const_iterator it = v.begin();
	while (it!=v.end())
	{
		cout<< <<*it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

注意:

  • 类模板有一个原则就是不去未实例化的模板里面取内容。
  • 原因:编译器无法区分是类型还是静态成员变量:const_iterator
  • 解决办法:在前面加一个typename关键字。
  • 作用:去没有进行过实例化的类模板里面取类里面的东西,编译器不能秋分这里的const_iterator是类型还是静态成员变量,让程序员自己来确定:加一个typename来确认。有了它,编译器就可以认为是程序员自己已经确认了这是一个类型,合乎语法,出错了就不管编译器的错,错在程序员。编译器的甩锅。编译器检查时只会检查初步的语法。
template<class T>
void print_vector(const vector<T>& v)
{
	typename vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout<< <<*it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

下面的这一段代码就相当于是已经实例化好的(相当于是用类模板实例化出来的),从这里面提取所需要的内容是可以的。

void print_vector(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it!=v.emd())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

还有一种简单的方式:auto关键字。

template<class T>
void print_vector(const vector<T>& v)
{
	// typename vector<T>::const_iterator it = v.begin();
	auto it = v.begin();
	while (it != v.end())
	{
		cout<< <<*it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

或者实现成下面这样:这个就是不仅在vector里面可以使用,还可以在其他容器里面使用。

template<class Comtainer>
void print_container(const Container& v)
{
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值