vector 是什么

std::vector 是C++标准库中的一个容器类,用于存储和操作一系列相同类型的元素。它是一个动态数组,可以根据需要自动调整大小。std::vector 提供了许多成员函数,可以方便地进行元素的插入、删除、访问和遍历操作,使用时候需要#include <vector>
其定义为:

namespace std {
    template <typename T, // 任意类型T
        typename Allocator = allocator<T>> //  内存分配器,一般都使用c++ 标准库默认的分配器
    class vector;
}

vector 构造

std::vector<Elem> c; // 默认构造,产生一个空的vector, 没有任何元素
std::vector<Elem> c(c2); // copy构造, 产生一个c2同类型的vector, 并且拷贝一份c2的数据。(每个元素都需要被复制)
std::vector<Elem> c = c2; // copy构造, 产生一个c2同类型的vector, 并且拷贝一份c2的数据。(每个元素都需要被复制)

std::vector<Elem> c(rv); // move 构造函数, 建立一个新的cector, 取 rv的内容。 可以理解为原始vector 的数据指针赋值给新的vector数据指针,并且将原来的值数据指针设置为空。
std::vector<Elem> c = rv; // move 构造函数, 建立一个新的cector, 取 rv的内容。

std::vector<Elem> c(n); // 调用元素的默认构造函数生成一个大小为n的vector, 如果Elem 是基础类型,那么每个元素会初始化为随机值,如果是对象,那么调用默认构造函数,(会对每个元素都调用默认函数)
std::vector<Elem> c(n, elem); // 构造一个大小为n的vector, 每个元素都是elem
std::vector<Elem> c(begin, end); // 构造一个vector, 用区间值[beg, end) 作为元素的值,注意是左闭又开区间。相当于end不算
std::vector<Elem> c(initlist); // 列表初始化,列 std::vector<int> c({1, 2, ,3})
std::vector<Elem> c = initlist; // 列表初始化,  std::vector<int> c = {1, 2, ,3};
  1. 拷贝构造就是对每个元素都进行拷贝,内置类型是赋值操作,类类型是调用类的copy构造函数
  2. vector的移动构造它实际上是将原始对象的资源所有权转移到了新创建的对象上,而不是进行深拷贝。这个过程包括以下几个步骤:
    • 首先,移动构造函数接收一个右值引用作为参数,这个右值引用指向需要移动的对象。右值引用表示的是临时对象或者即将被销毁的对象。
    • 然后,移动构造函数将原始对象的成员变量的指针或者其他资源指针设置为nullptr或者其他默认值,以确保原始对象不再拥有这些资源。
    • 接着,移动构造函数将原始对象的资源指针赋值给新创建的对象,使得新对象拥有这些资源。这样做的好处是避免了不必要的资源复制和内存分配,提高了程序的性能。

vector 赋值

c = c2; // 将c2赋值给c, c的旧值被替换
c = rv; // 将右值移动给c,c的旧值被替换
c = initlist; // 列表初始化 {1, 2, 3}
c.assign(n, elem); // 函数赋值,赋值n个elem给c,
c.assign(beg, end); // 注意, [beg, end), 左闭又开和构造一致
c.swap(c2); // 交换两个容器的数据
std::swap(c, c2); // 交换两个容器的数据。

所有的赋值操作,都有肯恩个调用元素的默认构造函数,拷贝构造函数,或者等于操作符, 析构函数。 如果自定义对象不想被频繁的销毁和构造,建议使用智能指针。

vector 元素容量和比较操作

c.empty(); // 判断容器是否为空,相当于c.size() == 0, 为空返回true
c.size(); // 返回当前容器中的元素个数
c.max_size(); // 返回当前容器元素最大可能存储的个数
c.capacity(); // 返回 【不重新进行空间分配】情况下的元素最大容量。
c.reserve(num); // 预留空间。如果容量不够,那么将会扩大
c.reserve(num, elem); // 预分配空间,如果size变大,那么多出的元素都是elem的拷贝
c.shrink_to_fit(); // 缩减容量,来符合当前实际存在元素的个数。比如预留2000个容量,但是实际只有两个,就可以调用该函数减小内存占用

c1 == c2; // 对每一个元素都调用 等于操作符,只有所有元素相等才为true
c1 != c2; // c1 是否不等于c2 相当于 !(c1==c2)
c1 > c2;
c1 < c2;
c1 <= c2;
c1 >= c2;
  1. vector 分配的容量总是多余当前容纳元素更多内存
  2. 内存重新分配是很耗时的。为了内存空间连续,是需要将原来的内存拷贝到新空间的。
  3. 防止内存分配可以使用reserve(size_t n) 预留n 个元素空间。
  4. vector 的内存一但分配是不会缩减的。即使我们删除元素也不行。可以使用shrink_to_fit() 或者 swap 缩小内存,交换两个vector,容量也是会交换的。
  5. vector 是一种有序集合,可以理解为一种动态数组,可以存储任意一种类型的元素,并可以随机访问每个元素。由于是一块完整的内存,在头和尾插入元素效率是很客观的,如果要在中间插入,那将会涉及到大块内存的移动,效率就很不理想。
  6. vector容器的比较操作是对每一个元素调用相同的比较操作符。

vector 元素访问

下标访问

c[index]; // 与c风格数组类似,返回index位置的元素,【但是不检查范围,超出范围发生未定义行为,大概率是崩溃】
c.at(index); // 函数访问,对下标进行检查,如果越界,则会抛出range-error 异常,经常配合try catch 使用,否则也基本是崩溃
c.front(); // 返回第一个元素, (不检查是否存在第一个元素) 对一个空元素调用该方法,大概率也是崩溃
c.back(); // 返回最后一个元素,(不检查是否存在最后一个元素)对一个空元素调用该方法,大概率也是崩溃

除了at()函数外,访问都应该加上判空,否则基本上是崩溃的。或者是未定义行为。

迭代器访问

c.begin(); // 返回随机访问iterator, 指向第一个元素
c.end(); // 返回随机访问iterator, 指向最后一个元素的下一个位置。 
c.cbegin(); // 返回 不可变的随机迭代器,不更改使用该迭代器,或者在加了const 的函数中使用该函数 指向第一个元素
c.cend(); // 返回 不可变的随机迭代器,不更改使用该迭代器,或者在加了const 指向最后一个元素的下一个位置。 

c.rbegin(); //  r 单词代表 =reverse iterator 逆向迭代器,指向末尾一个元素,back
c.rend(); // 返回逆向迭代器,指向第一个元素的上一个位置。
c.crbegin(); //  const 版本rbegin
c.crend(); //  const 版本rend

vector 数据的插入和删除

void c.push_back(elem); // 在容器的末尾添加一个elem的拷贝。
void c.pop_back(); // 弹出末尾元素,直接弹出不返回
iterator c.insert(pos, elem); //  在pos的前方插入一个elem的拷贝。并返回新元素的位置。 注意是pos 之前
inerator c.insert(pos, n, elem); // 在pos的前方插入n个elem的拷贝,并返回第一个新元素的位置。
iterator c.insert(pos, beg, end); //  在pos的前方插入一个区间[beg, end)内的所有元素的一份拷贝,
                                  // 并返回新元素的iterator, 如果没有新元素,返回pos 【注意区间,和拷贝,并且在pos前】
iterator c.insert(pos, initlist); // 在pos 前方插入一个初始化列表的拷贝,并返回第一个新元素的位置,没有新元素,返回pos

iterator c.emplace(pos, args...); // 在pos前方插入一个以可变参数args为初值构造的元素,并返回新元素的位置。
void c.emplace_back(pos, args...); //  用可变参数agrs 构造一个elem, 和push_back的区别就是push_back 调用拷贝构造,这里直接构造。

iterator c.erase(pos); // erase 橡皮擦,删除的意思,删除pos位置上的元素,并返回下一个元素的位置。
iterator c.erase(beg, end); //  删除[beg, end) 区间的所有元素,返会删除区间的下一个元素位置,注意区间是左闭又开。
void c.clear(); //  清空所有的元素,容器将会被清空,但不会释放所分配的内存
  1. vector 除了emplace* 类函数,其他函数插入都是值语义,都会拷贝一份。emplace 也是根据传入的参数确定了,传了对象,那么也是调用拷贝构造,
  2. 插入时候一定要注意位置,是在pos前插入,并返回插入的第一个元素的位置。
  3. 插入一个区间,一定是左闭又开区间,是不包含end元素的。
  4. 删除元素也是要注意区间,左闭又开区间。
  5. 如果遍历vector中,要删除容器的某个元素,应该更新迭代器指针,防止迭代器失效crash

多线程问题

  1. vector std版本不是原子操作的,涉及多线程应该自己加锁保护。
  2. 之只要改变vector内容都应该加锁。比如删除元素,添加元素,弹出末尾元素,这些操作涉及到了vector 内容的变更。

vector 作为c风格数组使用。

  1. c.data() 返回起始指针,如std::vector::data(); 就可以使用char* 类型接收。 并且可以对返回值使用strcpy, memset, memcpy等函数,只要保证数组不越界即可。
  2. 可以使用下标访问再取地址的方式获取某个位置的地址,和数组操作一样,因为他内部实现保证了内存连续。可以当做数组使用,&c[index]; 去的index处的地址,并对地址进行加减操作,copy操作
  3. 如果用来存在字符串,那么需要多一个位置存储’\0’, 其他不和指针操作差不多
    eg.
    std::vector<char> strHello{ 'h','e','l','l','o','\0' };
    int len = strlen(&strHello[0]); // 取地址求字符串
    printf("str1:%s len: %d\n", &strHello[0], len);
    len = strlen(strHello.data()); // data返回数据
    printf("str2:%s len: %d\n", strHello.data(), len);

    strHello.reserve(100);
    //copy
    memcpy(&strHello[0], "123456789", strlen("123456789"));
    // 别忘了加'\0'
    strHello.push_back('\0');
    len = strlen(strHello.data()); // data返回数据
    printf("str3:%s len: %d\n", strHello.data(), len);
    strHello[3] = 'A';
    printf("str4:%s len: %d\n", strHello.data(), len);

    // 输出:
    str1:hello len: 5
    str2:hello len: 5
    str3:123456 len: 6
    str4:123A56 len: 6

vector 一些示列

构造

    void printVector(const char *name, const std::vector<int> &c)
    {
        printf("%s\tsize:%d\t [", name, c.size());
        for (auto &item : c)
        {
            printf("%d,", item);
        }
        printf("]\n");
    }
    std::vector<int> c0;
	printVector("c0", c0);
	std::vector<int> c1{ 1, 2, 3 }; // 容器拥有3个元素 1, 2, 3
	printVector("c1", c1);
	std::vector<int> c2 = c1;		// 容器拥有3个元素 1, 2, 3
	printVector("c2", c2);
	std::vector<int> c3(5, 1);		 // 容器拥有 5个 元素 1 1 1 1 1
	printVector("c3", c3);
	std::vector<int> c4(c1.begin(), c1.begin() + 2);	// 不包含end,所以只有1, 2
	printVector("c4", c4);
	std::vector<int> c5 = { 1, 2, 3};				// 列表初始化为3个元素,1,2,3
	printVector("c5", c5);
// 输出
c0      size:0   []
c1      size:3   [1,2,3,]
c2      size:3   [1,2,3,]
c3      size:5   [1,1,1,1,1,]
c4      size:2   [1,2,]
c5      size:3   [1,2,3,]

遍历

    #include <vector>
    #include <algorithm>
    std::vector<int> c{ 1, 2, 3, 4, 5 };
	// 范围for
	printf("for:\t[");
	for (auto item : c)
	{
		printf("%d, ", item);
	}
	printf("]\n");

	// 下标
	printf("index:\t[");
	for (int i = 0, size = c.size(); i < size; i++)
	{
		printf("%d, ", c[i]); // 确保不能越界
	}
	printf("]\n");


	// 迭代器
	printf("iterator:\t[");
	for (auto it = c.begin(), end = c.end(); it != end; it++)
	{
		printf("%d, ", *it); // 确保it != end
	}
	printf("]\n");

	// 逆向迭代器
	printf("reverseit:\t[");
	for (auto it = c.rbegin(), end = c.rend(); it != end; it++)
	{
		printf("%d, ", *it); // 确保it != end 从后往前读
	}
	printf("]\n");

	// std::for_each

	printf("for_each:\t[");
	std::for_each(c.begin(), c.end(), [](int item)
	{
		printf("%d, ", item);
	});
	printf("]\n");
// 输出
for:    [1, 2, 3, 4, 5, ]
index:  [1, 2, 3, 4, 5, ]
iterator:       [1, 2, 3, 4, 5, ]
reverseit:      [5, 4, 3, 2, 1, ]
for_each:       [1, 2, 3, 4, 5, ]

插入

    void printVector(const char *desc, const std::vector<int> &c)
    {
        printf("%s\tsize:%d\t [", desc, c.size());
        for (auto &item : c)
        {
            printf("%d,", item);
        }
        printf("]\n");
    }
	std::vector<int> c{ 1, 3};
	// 变为123 在第二个位置前,插入都是pos之前 
	// 一定注意,begin 代表第一个元素的位置。
	c.insert(c.begin() + 1, 2);
	printVector("insert2", c);

	// 变为123 456
	c.insert(c.end(), { 4, 5, 6 });
	printVector("insert4", c);

	// 删除 456 从pos开始,到end前一个 删除 插入都是左闭又开
	c.erase(c.begin() + 3, c.end());
	printVector("erase4", c);

	// 在1 后面(2前面,一定是pos 前)插入3个3
	c.insert(c.begin() + 1, 3, 3);
	printVector("insert3", c);

	// 删除3个三 位置在1后面,第二个位置, 删除3个
	c.erase(c.begin() + 1, c.begin() + 1 + 3);
	printVector("erase3", c);

	// 清空所有元素
	c.clear();
	printVector("clear", c);
// 输出
insert2 size:3   [1,2,3,]
insert4 size:6   [1,2,3,4,5,6,]
erase4  size:3   [1,2,3,]
insert3 size:6   [1,3,3,3,2,3,]
erase3  size:3   [1,2,3,]
clear   size:0   []

push_back 与 emplace_back 的区别

先定义一个带有构造、copy构造 、移动构造函数的类

struct Human
{
	std::string m_name;
	int m_age;
	Human(const std::string& name, int age)
		: m_name(name),
		m_age(age)

	{
		std::cout << "human structure" << std::endl;
	}
	Human(const Human &other)
		: m_name(other.m_name),
		m_age(other.m_age)
	{
		std::cout << "human copy structure" << std::endl;
	}
	Human(Human &&other)
		: m_name(std::move(other.m_name)),
		m_age(other.m_age)
	{
		other.m_age = 0;
		std::cout << "human move structure" << std::endl;
	}
	~Human()
	{
		std::cout << "humna destruction" << std::endl;
	}
};

调用push_back、emplac_back的不同函数版本

  1. push_back 调用copy 构造版本
	std::vector<Human> c;
	Human knox("knox", 23);
	c.push_back(knox);
    // 输出
    knox human structure // 是调用 Human knox("knox", 23); 调用了构造函数
    knox human copy structure //  push_back 调用了copy 构造函数,
    knox humna destruction // 对象knox 销毁,调用析构函数
    knox humna destruction // c 销毁,调用每个元素的析构
  1. push_back 调用 move 构造版本
	std::vector<Human> c;
	Human knox("knox", 23);
	c.push_back(std::move(knox));
    // 输出
    knox human structure  // 构造knox 对象调用构造函数
    knox human move structure // push_back 中std::move 调用了移动构造函数 
     humna destruction // knox 对象析构,knox 没打印的原因是m_name 变量被移动到容器的knox对象中了
    knox humna destruction // 容器销毁,调用每个元素的析构
  1. emplace_back调用拷贝构造版本
	std::vector<Human> c;
	Human knox("knox", 23);
	c.emplace_back(knox);

    // 输出
    knox human structure // 构造对象调用构造函数
    knox human copy structure // 调用拷贝构造函数
    knox humna destruction //knox 对象销毁调用默认析构
    knox humna destruction // 容器销毁,调用每个元素的析构

  1. emplace_back 调用构造函数版本
    std::vector<Human> c;
	c.emplace_back("knox", 23);
    // 输出
    knox human structure // 只有一次构造 一次析构,大大提高效率
    knox humna destruction // 容器销毁,调用每个元素的析构
  1. emplace_back 调用移动构造
	std::vector<Human> c;
	Human knox("knox", 23);
	c.emplace_back(std::move(knox));
    // 输出
    knox human structure
    knox human move structure
    humna destruction
    knox humna destruction

通过上面5种情况的对比,他们的区别emplace_back 是可以直接调用构造函数,push_back是不能直接调用拷贝构造函数的,其他的copy构造,移动构造都基本差不多。

再回顾一下vector

  1. 动态大小:std::vector 可以根据需要动态调整大小,可以在运行时添加或删除元素。

  2. 连续存储:std::vector 内部使用连续的内存存储元素,因此可以通过索引快速访问元素,支持随机访问。

  3. 功能丰富:std::vector 提供了丰富的成员函数和操作符,可以方便地对元素进行访问、插入、删除和修改等操作。

  4. 自动扩容:当元素数量超过当前容量时,std::vector 会自动分配更大的内存空间,并将原有元素复制到新的内存中。

  5. 内存管理:std::vector 负责管理内存,当容器不再需要时,会自动释放内存。

  6. 迭代器支持:std::vector 提供了迭代器,可以使用迭代器遍历容器中的元素。

  7. 性能高效:由于 std::vector 内部使用连续的内存,因此在访问元素时具有良好的缓存局部性,提供了较高的访问性能。

  8. 插入、删除、构造使用了区间,那么一定是左闭又开区间。

  9. 插入一定是在pos位置之前,删除一定是在pos位置上。一定要做好数组 越界检查,不然删除、插入不存在的位置,那么数组越界,基本上崩溃

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐