C++初阶-vector的模拟实现3

发布于:2025-05-22 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

1.预备知识:initializer_list

1.1初步了解

1.2关于initializer_list的deepseek的回答

C++中的 std::initializer_list

主要特性

常见用途

1. 接受列表的构造函数和函数

2. 基于范围的 for 循环

重要注意事项

实现示例

2.vector::vector(initializer_list il)(C++11新构造函数的实现)

2.1vector::vector(initializer_list il)简单应用

2.2vector::vector(initializer_list il)模拟实现

3.template vector(InputIterator first, InputIterator last)(构造函数)的模拟实现

4.vector(size_t n, const T& val = T())的模拟实现

T()什么意思

缺省值 T() 的含义

实际使用示例

为什么这样设计

注意事项

5.问题分析

解决方案

6.模拟实现vector的代码总结

7.总结



1.预备知识:initializer_list

1.1初步了解

这个东西在以后用得地方也比较多,也是C++11新增加的,有了这个东西的基础我们之后就可以按照我们自己想要的方式进行初始化了。那么这是个什么呢?这里只简单介绍一下用法,感兴趣的可以自己去搜索一下这个。

这是在C++官网中有的initializer_list的解释后的中文界面。我们是看不懂它的意思的,但是如果说,这个与数组有关会怎么样?

之前见过很多数组,但是那些数组好像都是内置类型,而这个可以用来存储自定义类型。

其实现在我们还是有些不懂的,我们来看一下示例:

auto il={10,20,30};

我们就觉得这个代码好像有些不对。因为之前我们用这个的时候不是这样定义的吗?

auto il[]={10,20,30};

为什么这个就不一样呢?

这个是把花括号({})里面的数传递给一个对象,它会默认任务这个对象il的类型为initializer_list。因为它是模板,会根据花括号内的数据类型来知道类型。我们可以看出这个是一个类模板,也就是说它是系统库的一个容器,它支持begin、end、size构造:

其中begin和end也支持迭代器版本。如果不信il的类型为initializer_list,那么我们可以运行一下以下代码:

int main()
{
	auto il = { 1,2,3,4,5 };
	cout << typeid(il).name() << endl;
}

这个代码可以打印出il的真实类型,所以最终运行结果如下:

我们通过打印的结果推断出il是根据initializer_list类实例化出的int对象。这样相对于传统写法我们就有改进了。

此外我们如果想要访问il中的元素,就可以直接:il.begin()或者il.begin()+n即可。

那么对于这个东西的详细解释是什么?

我们来根据deepseek的生成来更加了解一下它:

1.2关于initializer_list的deepseek的回答

C++中的 std::initializer_list

std::initializer_list 是 C++11 引入的一个轻量级代理对象,它提供对同一类型对象数组的访问。主要用于支持统一的初始化语法,并允许函数以类型安全的方式接受可变数量的同类型参数。

主要特性
  1. 初始化:用于列表初始化(花括号初始化语法)

  2. 轻量级:通常实现为一对指针或一个指针加大小

  3. 不可变:不能通过 initializer_list 修改元素

  4. 临时生命周期:底层数组与 initializer_list 对象具有相同的生命周期

常见用途
1. 接受列表的构造函数和函数
#include <initializer_list>
#include <vector>

class MyClass 
{
public:
    MyClass(std::initializer_list<int> list) 
    {
        // 从列表初始化
    }
};

void func(std::initializer_list<std::string> args) 
{
    for (const auto& arg : args) 
    {
        // 处理每个参数
    }
}

int main() 
{
    MyClass obj{1, 2, 3, 4};  // 使用 initializer_list 构造函数
    func({"你好", "世界"});    // 传递 initializer_list 给函数
    
    std::vector<int> v = {1, 2, 3, 4};  // vector 使用 initializer_list
}
2. 基于范围的 for 循环
for (int x : {1, 2, 3, 4}) 
{  // 创建临时 initializer_list
    std::cout << x << " ";
}
重要注意事项
  1. 底层数组始终是 const - 不能通过 initializer_list 修改元素

  2. 创建 initializer_list 时会复制元素(不会发生移动)

  3. 底层数组的生命周期与 initializer_list 对象相同

  4. 标准库容器(如 std::vectorstd::map 等)常用它来支持花括号列表初始化

实现示例

下面是一个在自己类中使用 initializer_list 的简单例子:

#include <initializer_list>
#include <iostream>

class NumberList 
{
    std::size_t count;
    const int* data;
public:
    NumberList(std::initializer_list<int> list) 
        : count(list.size()), data(list.begin()) {}
        
    void print() const 
    {
        for (std::size_t i = 0; i < count; ++i) 
        {
            std::cout << data[i] << " ";
        }
        std::cout << "\n";
    }
};

int main() 
{
    NumberList nums{1, 2, 3, 4, 5};
    nums.print();  // 输出: 1 2 3 4 5
}

std::initializer_list 是一个强大的特性,它实现了简洁直观的初始化语法,同时保持了类型安全。

2.vector::vector(initializer_list<T> il)(C++11新构造函数的实现)

2.1vector::vector(initializer_list<T> il)简单应用

这个构造函数才是我们用得最多的构造函数,其他的那些无参构造还有那种用几个相同的元素构造都太低级了,我们完全可以用这个代替,所以刚刚讲的initializer_list就派上了用场了,我们可以用这个构造函数这样写:

void test()
{
	std::vector<int> v = { 1,2,3,4,5,6,7 };
	for (const auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

那么运行结果为:

所以我说这个构造函数才是最常用的,因为它很方便啊,不用我们一个一个push_back了。那这个函数如何实现呢?

2.2vector::vector(initializer_list<T> il)模拟实现

这个构造函数相对于其他的函数有些不一样,但是我们可以先reserve   il个大小的空间,然后遍历il,并把il的数据一个一个push_back到对象结尾,则:

vector(initializer_list<T> il)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

我们来测试一下这个函数:

void test()
{
	td::vector<int> i = { 4,6,2,4,8,9 };
	for (const auto& e : i)
	{
		cout << e << " ";
	}
	cout << endl;
}

那么运行结果为:

则之前实现的函数没有问题。

3.template <class InputIterator>
vector(InputIterator first, InputIterator last)(构造函数)的模拟实现

这个可以是一个类模板的成员函数,也可以是一个函数模板。此外,这个InputIterator就是只读迭代器的意思,所以我们可以传递任何有效的迭代器,在list中我会额外讲解一个Iterator的分类,其中有每个迭代器如何正确使用,为什么每个容器的迭代器类型不同的原因。而我们由于是插入一段迭代器,所以可以传递除了OutputIterator外的迭代器:

这个不是重点,重点是我们要知道该种构造函数是如何实现的。

这个构造函数是用一段迭代器区间的值来初始化,我们是先的时候直接通过迭代器来遍历first到lat这段区间,并尾插数据至该对象即可,那么最终实现为:

template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

我们来测试一下这个函数:

void test()
{
	td::vector<int> v1 = { 3,5,3,4,6,7,2,0 };
	td::vector<int> v2(v1.begin() + 3, v1.end() - 2);
	//也可以这样使用
	int v3[] = {4,5,6,2,4};
	td::vector<int> v4(v3 + 1, v3 + 4);
	for (const auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	for (const auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	for (const auto& e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
}

那么运行结果为:

为什么我们可以用第二种方式来进行传递?

因为这相当于原生指针,迭代器模拟的是指针的行为。而指向数组元素的指针是可以当做迭代器用的,我们也可以用来进行sort来进行数组排序,而不是再创建一个迭代器来进行排序。

4.vector(size_t n, const T& val = T())的模拟实现

这个也是构造函数,只是是用n个val进行构造的,这个函数的功能和那个resize功能完全一样,所以我们直接复用那个resize函数即可:

vector(size_t n, const T& val = T())
{
	resize(n, val);
}

那么这个缺省值又是什么意思,我这个也不太理解,我们可以直接问deepseek寻求答案:

T()什么意思

缺省值 T() 的含义

T() 是类型 T 的值初始化表达式,具体行为取决于类型 T 的性质:

  1. 对于内置类型

    • int() → 0

    • double() → 0.0

    • bool() → false

    • 指针类型 → nullptr

  2. 对于类类型

    • 调用该类的默认构造函数

    • 如果类没有默认构造函数,则编译错误

实际使用示例

#include <vector>
#include <iostream>

int main() 
{
    // 使用显式初始值
    std::vector<int> v1(5, 10);  // 5个元素,每个都是10
    // v1 = {10, 10, 10, 10, 10}

    // 使用缺省值
    std::vector<int> v2(5);      // 5个元素,每个都是int()即0
    // v2 = {0, 0, 0, 0, 0}

    // 对于自定义类型
    struct Point 
    {
        int x, y;
        Point() : x(0), y(0) {}  // 默认构造函数
    };
    
    std::vector<Point> v3(3);    // 3个Point,每个都是Point()
    // v3 = {{0,0}, {0,0}, {0,0}}
}

为什么这样设计

这种设计提供了灵活性:

  • 当需要特定初始值时,可以显式指定

  • 当只需要默认值时,可以省略第二个参数

  • 保持与内置数组初始化行为的一致性(int arr[5] = {}

注意事项

  1. 如果 T 没有默认构造函数,则不能省略第二个参数

  2. 对于非基本类型,T() 可能会比想象中更耗时(涉及构造函数调用)

  3. C++11 后更推荐使用列表初始化 std::vector<int> v{1, 2, 3}

所以说这个T()的作用还是比较大的,这也是C++设计的比较好的地方!

5.问题分析

如果我们运行以下代码:

void test()
{
	td::vector<double> v1(10, 0.1);
	for (const auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	td::vector<int> v2(10, 1);
	for (const auto& e : v2)
	{
		cout << e << " ";
	}
}

那么结果是:

这个报错怎么在那个用一段迭代器初始化的那个函数那里?

我们通过调试发现在到double实例化个类的时候还是可以调用正确的构造函数,但是为什么我们用int实例化个类的时候就调用的构造函数不是正确的!我们在C++模板中讲过,C++编译器在进行类型传参时有个原则:匹配更匹配的!而且这是在有选择的情况下,所以这个情况就是那个用迭代器区间进行初始化的构造函数更匹配!所以出现了类型匹配的问题。因为用n个val进行初始化,那么第一个参数是size_t类型,而我们实例化是实例化为int类型,所以造成了类型匹配的问题。

我们可以把那个用n个val值进行初始化的构造函数的第一个参数改为int类型,但是最好的解决方案是:再写一个第一个参数为int类型的参数的重载函数。(这也是库函数对这种情况的解决办法)

解决方案

添加这个函数:

vector(int n, const T& val = T())
{
	resize(n, val);
}

那么最后结果为:

虽然n是int类型,但是在传给resize时会进行类型转换,所以不会报错!

6.模拟实现vector的代码总结

这都是最终版本的实现代码,其他版本的就没在里面了。

#include<iostream>
#include<vector>
#include<string>
#include<assert.h>
using namespace std;
namespace td
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		~vector()
		{
			delete[] _start;
			_start = _finish = _end_of_storage = nullptr;
		}
		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		bool empty() const
		{
			return begin() == end();
		}
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finish = x;
			++_finish;
		}
		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_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
		void pop_back()
		{
			assert(_finish > _start);
			--_finish;
		}
		void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (pos == _finish)
			{
				pop_back();
				return;
			}
			iterator it = pos + 1;
			while (it < _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
		}
		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				--it;
			}
			*pos = x;
			++_finish;
		}
		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
		vector(const vector<T>& v)
		{
			reserve(v.size());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		vector<T>& operator=(vector<T> x)
		{
			swap(x);
			return *this;
		}
		vector()
		{
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldSize = size();
				//开辟新空间
				T* tmp = new T[n];
				//拷贝数据
				for (size_t i = 0; i < oldSize; i++)
				{
					tmp[i] = _start[i];
				}
				//释放空间
				delete[] _start;
				_finish = tmp + oldSize;
				_start = tmp;
				_end_of_storage = _start + n;
			}
		}
		vector(initializer_list<T> il)
		{
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		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);
		}
		vector(int n, const T& val = T())
		{
			resize(n, val);
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

7.总结

vector的实现相对于string的实现简单一些,但是还是有很多东西需要掌握的,我实现的也只是一些比较重要的,其他的如:assign这些用的得不多的函数我就不实现了,感兴趣的可以自己去实现。好了,vector的所有部分都已经完结了。喜欢的可以一键三连哦,下讲将讲解:list。下讲再见!


网站公告

今日签到

点亮在社区的每一天
去签到