C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ std::vector的6种初始化

C++中std::vector的6种初始化方式

作者:吃素的施子

这篇文章主要介绍了C++中std::vector的6种初始化方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

C++ std::vector的6种初始化

1.vector<int> list1; 默认初始化,最常用

此时,vector为空, size为0,表明容器中没有元素,而且 capacity 也返回 0,意味着还没有分配内存空间。 这种初始化方式适用于元素个数未知,需要在程序中动态添加的情况。

2.vector<int> list2(list); 或者 vector<int> ilist2 = ilist; //拷贝初始化 ,"="

两种方式等价 , list2 初始化为list 的拷贝, list必须与list2 类型相同, 也就是同为int的vector类型, ilist2将具有和ilist相同的容量和元素

3.vector<int> list = {1,2,3.0,4,5,6,7};

vector<int> list {1,2,3.0,4,5,6,7};//列表中元素的拷贝 ilist 初始化为列表中元素的拷贝,列表中元素必须与ilist的元素类型相容, 本例中必须是与整数类型相容的类型,整形会直接拷贝,其他类型会进行类型转换。

4.vector<int> list3(list.begin()+2, list.end()-1); //比较常用

将points数组转换成vector; 挺好用的; list3初始化为两个迭代器指定范围中元素的拷贝,范围中的元素类型必须与list3 的元素类型相容, 在本例中ilist3被初始化为{3,4,5,6}。

注意:由于只要求范围中的元素类型与待初始化的容器的元素类型相容,因此迭代器来自不同的容器是可能的,

例如,用一个double的list的范围来初始化ilist3是可行的。另外由于构造函数只是读取范围中的元素进行拷贝,因此使用普通迭代器还是const迭代器来指出范围并没有区别。

这种初始化方法特别适合于获取一个序列的子序列。

5.vector<int> ilist4(7); ilist4中将包含7个元素

默认值初始化,ilist4中将包含7个元素,每个元素进行缺省的值初始化, 对于int,也就是被赋值为0,因此ilist4被初始化为包含7个0。

当程序运行初期元素大致数量可预知,而元素的值需要动态获取的时候, 可采用这种初始化方式。  

6.vector<int> ilist5(7,3);

指定值初始化,ilist5被初始化为包含7个值为3的int; 这个也比较常用。

std::vector使用总结

Vector

Vector描述的是一个动态数组(dynamic array),并提供了相关操作和接口。

在这里插入图片描述

在使用Vector之前,需要引入头文件#include<vector>,在此头文件中,类型vector是一个定义于namespace std内的template

template<
    class T,
    class Allocator = std::allocator<T>
> class vector;

其中T可以是任意类型,Allocator用来定义内存模型,默认是C++标准库提供的allocator

Vector的能力

Vector将元素复制到dynamic array内部,是一种动态的顺序表结构。Vector支持随机访问,可以以常量时间访问元素,Vector支持随机访问迭代器,以及STL提供的任何算法(排序、查找等)。

但对于插入、删除和移动等操作,Vector效率较低(类似于数组的特性)。

大小(size)和容量(capacity)

vector区别于一般数组的特性之一就是能够动态的“扩容”,在容量能够容纳所有元素的前提下,提供基于pointer、reference、iterator的访问,以及元素的操作等,因此,大小和容量的概念至关重要。  

Vector的大小(size)是指当前元素所占用空间,而容量(capacity)则是指vector分配内存预留大小,当size超过capacity时,vector会自动进行扩容,重新分配内存。

举个例子——

vector<int> v;
	for (int i=0; i < 20; i++) {
		v.push_back(i);
		cout<<"i = "<<i
			<< " size = " << v.size()
			<< " capacity = " << v.capacity() << endl;
	}

在这里插入图片描述

可以看出,vector的大小是随着插入元素不断增加的,也就是说,size()的作用其实和sizeof的作用类似,但是由于vector都是reference语义的操作,sizeof一个vector对象,无法得到实际大小,所以vector提供了size成员函数。而capacity()则反映了vector的动态扩容机制。

vector容量的概念之所以重要,有两个原因——  

1.一旦内存重新分配,vector相关元素的所有reference、pointer、iterator都会失效。这里的失效是指,原来的值需要更新到重新分配后的内存地址。

2.内存的重新分配需要一定时间。

reserve函数

通过reserve函数可以显式的指定预留空间的大小,而避免vector反复的进行内存重新分配, 从而提高vector的使用效率。但reserve不能减小vector容量,比如,已存在vector.capacity = 100,使用reserve(80)不会有任何作用。

vector<int> v(100);
	cout<<v.size()<<" "<<v.capacity()<<endl;
	v.reserve(80);
	cout<<v.size()<<" "<<v.capacity()<<endl;

在这里插入图片描述

此外,还可以通过vector的构造函数显式的指定容量大小,比如:

vector<int> v(100);

使用这种方法的时候,vector会调用元素的default构造函数进行初始化,对于基础类型,vector会进行零值初始化(类似于Java的初始化机制),比如说——

class test{
	static int count;
public:
	test(){
		count++;
		cout<<"test constructor function:"<<count<<endl;
	}
};
int test::count = 0;
int main(){
	vector<test> v(10);// 调用10次test默认构造函数	
	return 0;
} 

在这里插入图片描述

这种方法可能会频繁调用构造函数,因此效率不如reverse的高。

shrink_to_fit函数

reverse函数无法缩减容量,但是很多时候,vector自动扩容机制分配的内存往往是多余的,如果频繁使用vector的话,这种内存浪费会相当可观。

C++11提供了缩减容量以符合当前需求的函数,shrink_to_fit函数。

该函数不具有强制力的要求,换言之,具体实现可能会因编译器实现而不同。

但大多数情况下,缩减容量是有效的,比如——

vector<int> v;
	cout<<"capacity = "<<v.capacity()<<endl;
	v.reserve(10);
	cout<<"capacity = "<<v.capacity()<<endl;
	v.shrink_to_fit();
	cout<<"capacity = "<<v.capacity()<<endl;

在这里插入图片描述

Vector操作

构造函数、析构函数

vector的构造函数和析构函数如下表所示——

序号操作效果
1vector<Elem> cDefault构造函数,产生一个vector,没有任何元素
2vector<Elem>c(c2)vector<Elem>c=c2Copy构造函数,建立c2同型vector并成为c2的一份副本,该复制是深度复制
3vector<Elem>c(rv)vector<Elem>c=rvrv是一个vector右值引用,那么这里的构造函数是一个Move构造函数,建立一个新的vector,取右值内容(C++11新特性)
4vector<Elem>c(n)利用元素的默认构造函数生成一个大小为n(容量也为n)的vector
5vector<Elem>c(n,elem)建立一个大小为n的vector,并初始化为elem
6vector<Elem>c(beg,end)建立一个vector,并以迭代器所指向的区间[beg,end)作为元素值
7vector<Elem>c(initlist)vector<Elem>c=initlist建立一个vector,以初值列initlist元素为初值(C++11新特性)
8c.~vector()销毁所有元素,释放内存

其中3和7是C++11新特性,6是基于迭代器的,第一次见可能会比较陌生,但使用起来很方便,比如——

vector<int> v{ 1,2,3,4,5,6,7,8,9 };// initlist初始化
	vector<int> v2(v.begin(), v.end());// 基于迭代器的初始化
	vector<int> v3 = move(v2); // Move构造函数
	for (const auto&elem : v3) // range-based for循环
	{
		cout << elem << " ";
	}

在这里插入图片描述

非更易型操作(Nonmodifying Operating)

STL中有非更易的概念,即不改变容器内元素.

序号操作效果
1c.empty()容器为空返回true,不为空返回false,相当于size()==0
2c.size()返回当前元素的个数
3c.max_size()返回元素个数之最大可能量
4c.capacity()返回容器当前最大容量
5c.reserve(n)如果容器不足,显示扩容,该操作会引起迭代器、指针、引用的失效,但并未改变元素的值,因此仍旧视为非更易型操作
6c.shrink_to_fit()降低容量,使得size()==capacity(),(C++11新特性)
7c1==c2对每个元素调用c1==c2,全部相等返回true
8c1!=c2只要有一个元素相等,返回true,相当于!(c1==c2)
9c1>c2,c1>=c2,c1<c2,c1<=c2同上,依次类推

赋值操作(Assignment Operating)

赋值操作可能会引起元素的默认构造函数、复制构造函数、赋值操作符等,详细如下表——

序号操作效果
1c1=c2把c2的全部元素赋值给c
2c=rv将rvalue 右值引用以 move assignment的方式赋值给c(C++11新特性
3c = initlist将初值列initlist的所有元素赋值给c(C++11新特性)
4c.assign(n,elem)复制n个elem,赋值给c
5c.assign(beg,end)复制迭代器指向区间[beg,end)内容,赋值给c
6c.assign(initlist)将初值列initlist的所有元素赋值给c
7c.swap(c2)置换c和c2的数据
8swap(c1,c2)置换c1和c2的数据

元素访问(Element Access)

下表列出了所有访问vector元素的方法,对于所有non-const元素,返回的都是元素的reference,这些操作中,只有at()会检查边界,如果越界,抛出out_of_range异常。——

序号操作效果
1c[index]返回索引index所指向的元素
2c.at(index)返回index所指向的元素(会进行边界检查)
3c.front()返回第一个元素(不会检查第一元素是否存在)
4c.back()返回最后一个元素(不会检查最后一个元素是否存在)

由于vector只提供了at函数的检查元素,所以对于越界或者访问空元素的情况,其结果是未定义的,视编译器而异。

Visual Studio比较严格,将此行为定义为运行时异常,而GCC则较为宽松,允许这一非法操作。

vector<int> v{ 1,2,3,4,5,6,7,8,9 };
	v.clear();
	cout << v.front() << endl;// Expression:front() called on empty vector.
	cout<< v[10]<<endl;// Expression:vector subscript out of range.

在这里插入图片描述

迭代器函数(Iterator Function)

vector支持随机访问(random access)迭代器,理论上STL提供的所有迭代器都能为其所用。

迭代器是STL提供操作容器的重要工具,熟练使用迭代器能够将STL各大容器的性能发挥到极致。

序号操作效果
1c.begin()返回一个randrom access iterator指向第一个元素
2c.end()返回一个random access iterator指向的之后一个元素
3c.cbegin()返回一个const random access iterator指向的第一个元素(C++11新特性
4c.cend()返回一个const random access iterator指向的最后一个元素(C++11新特性
5c.rbegin()返回一个反向迭代器(reverse iterator)指向的第一个元素
6c.rend()返回一个reverse iterator指向的最后一个元素
7c.crbegin()返回一个const reverse iterator指向的第一个元素(C++新特性
8c.crend()返回一个const reverse iterator指向的最后一个元素(C++11新特性

迭代器结合auto以及range-based for循环,能够将元素的访问以一种清新脱俗的方式展现出来——

vector<int> v{ 1,2,3,4,5,6,7,8,9 };
	for (auto it = v.cbegin(); it != v.cend();++it) {
		cout << *it << " ";
	}

在这里插入图片描述

再比如说容器元素的逆序输出——

vector<int> v{ 1,2,3,4,5,6,7,8,9 };
	for (auto it = v.crbegin(); it != v.crend();++it) {
		cout << *it << " ";
	}

在这里插入图片描述

插入和移除(Inserting and Removing)

插入和移除无疑是最常用的操作,掌握了这些,基本上就可以使容器在自己的代码中产生战斗力——

序号操作效果
1c.push_back(elem)在vector末尾插入元素elem
2c.pop_back()移除最后一个元素,但是不返回该元素
3c.insert(pos,elem)在iterator指向的pos位置的前方插入一个元素elem的副本,并返回新元素的位置(此时返回的是整型,而非iterator)
4c.insert(pos,n,elem)在iterator指向的pos位置的前方插入n个元素的副本,并返回第一个新元素的位置
5c.insert(pos,beg,end)在iterator指向的pos位置的前方插入区间[beg,end)内所有元素的副本,并返回第一个新元素的位置
6c.insert(pos,initlist)在iterator指向的pos位置的前方插入初始化列表所有元素的副本,并返回第一个元素的位置(C++11新特性
7c.emplace(pos,args…)在iterator指向的pos位置的前方插入一个以args为初值的元素,并返回新元素的位置(C++11新特性
8c.emplace_back(args…)在vector末尾附加一个args为初值的元素,不返回任何东西
9c.erase(pos)移除iterator位置pos上的元素,返回下一个元素的位置
10c.erase(beg,end)移除区间[beg,end)所指向的元素所有内容,返回下一个元素的位置
11c.resize(num)将vector大小调整为num,若大小增大,新元素以默认构造函数或者零值进行初始化
12c.resize(num,elem)将vector大小调整为num,若大小增大,新元素以elem进行初始化
13c.clear()移除所有元素,容器清空

我们演示一个插入到删除的过程——

vector<int>v;
	vector<int>v2{ -1,-2,-3,-4 };
	cout << "Source data:";
	for (int i = 0; i < 10; i++) {
		v.push_back(i);
	}
	for (const auto&elem : v) {
		cout << elem << " ";
	}
	cout << endl << "insert vector 2:";
	v.insert(v.end(), v2.begin(), v2.end());
	for (const auto&elem : v) {
		cout << elem << " ";
	}
	cout << endl << "remove vector 2:";
	auto begin_it = v.begin();
	while (*begin_it != *v2.begin()) {
		begin_it++;
	}
	v.erase(begin_it, v.end());
	for (const auto&elem : v) {
		cout << elem << " ";
	}

在这里插入图片描述

异常处理

vector仅支持最低限度的逻辑差错检查,Subscript操作符的安全版本at()是唯一一个被C++ standard认可得以抛出异常的函数,此外C++ standard 同时规定,只有一般标准异常或者被用户自定义的异常才可能发生,也就是说,vector的一些非法操作,在运行时都不会抛出异常,但程序员需要对自己的非法操作负责。

1. 如果push_back安插元素时发生异常,函数不产生效用;

2. 如果元素remove/copy操作不抛出异常, 那么insert/emplace等要么成功,要么不抛出异常;

3. pop_back绝对不会抛出任何异常;

4. 如果元素remove/copy操作不抛出异常,erase也不会抛出异常;

5. swap和clear不会抛出异常;

6. 如果元素remove/copy操作不抛出异常,那么所有的操作不是成功,就是不产生任何效果,包括不抛出异常。

以上所有都基于“析构函数不得抛出任何异常”的前提。但实际上,编译器会做不同程度的优化,比如热心的VS,几乎在所有vector可能出现的异常检测上都做了处理,在C++ Standard未定义的部分做了诸多工作。

这些仅为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文