从c++11标准以来,c++中std定义的几种容器的效率非常高,优化的非常好,完全没有必要自己去定义类似的数据结构。了解使用它们,可以满足90%的日常编程需要。该篇文章基于c++11标准,从用户角度来介绍常用的顺序容器与并联容器(如果想从内部了解它们是怎么实现的,推荐看看《std源码剖析》这本书)。它们包括:
顺序容器:
- vector
- string (它不是类模板)
- list
- forward_list
- deque
- queue
- priority_queue
- stack
有序关联容器:
- map
- multimap
- set
- multiset
无序关联容器:
unordered_map
unordered_multimap
unordered_set
unordered_multiset
力推网站: https://en.cppreference.com/w/cpp/container, 里面介绍的绝对很全的,绝对比本篇文章好太多太多。
顺序容器
1. vector容器
a. vector的定义与初始化
// T 表示实例化类模板时使用的类型 vector<T> v1 // 默认初始化, 此时v1为空。 vector<T> v1(v2) // 执行的copy初始化,此时v1与v2的内容相同 vector<T> v1 = v2 // 与上面相同,都会执行copy构造函数 vector<T> v1(n) // 此时v1的size大小为n ,它里面的值是根据T的类型进行默认初始化的 vector<T> v1(n, a) // v1的初始化为n个值为a的元素 vector<T> v1{a, b, c} // 列表初始化,v1内现在的元素就是a, b, c (这是c++11标准新入的) vector<T> v1 = {a, b, c} // 与上面相同
列表初始化是什么?
对于上面的几种初始化方法,最常用的有三种, 1. 默认初始化,这里vector为空;2.copy初始化,这时用另一个vector初始化该vector 3. 列表初始化,为vector 初始化一些初始值。 几乎或很少在初始化vector的时候去设定它的size大小,因为vector的push_bask是非常高效的,甚至比提前设置它的大小更高效(见c++primer 页)
b. vecotr常使用的操作
1. 属性操作
v1.size() //v1内已经存放的元素的数目
v1.capacity() // v1现有的在存储容量(不再一次进行扩张内存空间的前提下)
v1.empty() // 判断v1是否为空
v1.max_size() // 返回vector可以存放的最大元素个数,一般这个数很大,因为vector可以不断调整容量大小。
v1.shrink_to_fit() // 该函数会把v1的capacity()的大小压缩到size()大小,即释放多余的内存空间。
2. 访问操作:访问操作都会返回引用,通过它,我们可以修改vector中的值。
v1[n] // 通过下标进行访问vector中的元素的引用 (下标一定要存在 ,否则未定义,软件直接崩了)
v1.at(n) // 与上面类似,返回下标为n的元素的引用,不同的是,如果下标不存在,它会抛出out_of_range的异常。它是安全的,建议使用它。
v1.front() // 返回vector中头部的元素的引用(使用时,一定要进行非空判断)
v1.back() // 返回vector中尾部的元素 引用(使用时,一定要进行非空判断)
3. 添加操作:
v1.push_back(a) //在迭代器的尾部添加一个元素
v1.push_front(a) // vector不支持这个操作
v1.insert(iter, a) // 将元素a 插入到迭代器指定的位置的前面,返回新插入元素的迭代器(在c++11标准之前的版本,返回void)
v1.insert(iter, iter1, iter2) //把迭代器[iterator1, iterator2]对应的元素插入到迭代器iterator之前的位置,返回新插入的第一个元素的迭代器(在c++11标准之前的版本, 返回空)。
在c++11标准中,引入了emplac_front()、 emplace()、emplace_back(), 它们分别与push_front()、insert()、 push_back()相对应,用法与完成的动作作完全相同,但是实现不一样。 push_front()、insert()各push_back()是对元素使用copy操作来完成的,而emplac_front()、 emplace()和emplace_back()是对元素使用构造来完成的,后者的效率更高,避免了不必要的操作。因此,在以后更后推荐使用它们。
4. 删除操作:
v1.erase(iterator) // 删除人人迭代器指定的元素,返回被删除元素之后的元素的迭代器。(效率很低,最好别用)
v1.pop_front() //vector不支持这个操作
v1.pop_back() //删除vector尾部的元素 , 返回void类型 (使用前,一定要记得非空判断)
v1.clear() //清空所有元素
5. 替换操作:
v1.assign({初始化列表}) // 它相当于赋值操作,
v1.assign(n, T) // 此操作与初始化时的操作类似,用个n T类型的元素对v1进行赋值
v1.assign(iter1, iter2) // 使用迭代器[iter1, iter2]区间内的元素进行赋值(该迭代器别指向自身就可以),另外,只要迭代器指的元素类型相同即可(存放元素的容器不同,例如:可以用list容器内的值对vector容器进行assign操作,而用 "=" 绝对做不到的。
v1.swap(v2) // 交换v1与v2中的元素。 swap操作速度很快,因为它是通过改变v1与v2两个容器内的数据结构(可能是类似指针之类的与v1和v2的绑定)完成的,不会对容器内的每一个元素进行交换。 这样做,不仅速度快,并且指向原容器的迭代器、引用以及指针等仍然有效,因为原始的数据没有变。在c++ primer 中建议大家使用非成员版本的swap()函数,它在范型编程中很重要。
c. 小结:
1. vector容器最重要的特性是: 它在一段连续的内存空间中存储元素, 可以在常量时间内对vector容器进行随机访问,并且可以很高效的在vector的尾部进行添加与删除操作,在vector中间或头部添加与删除元素的效率很低。
2. 只要对vector进行增加与删除元素的操作,都会使迭代器、指针、引用失效(可能有时候它们仍然有效,不过是随机的,绝对不能作这样假设)。所以当使用vector的迭代器、引用和指针时,一定要杜绝对他们进行增加与删除元素的操作
3. 对于vector的迭代器,它除了可以进行 ++iter 与 --iter 的操作之外 ,还可以进行算术运算,例如: iter + n 、 ::difference_type a = iter1 - iter2 //它的返回类型为 ::difference_type,例如vector<int>::difference_type (另一个也支持迭代器算术运算的容器为string)
4. 待补充!
2. string容器
string与vector类似,但是string不是一种类模板,而就是一种类型,因为它专门用于存放字符的(存放的元素类型已经明确),所以没有设计为类模板。它的所有特性与vector相同,包括存储在连续的空间/快速随机访问/高效在尾部插入与删除/低效在中间插入与删除等, string的迭代器也支持算术运算。 实际上,就可以把string类型看作为vector<char>类型, vector的所有特性都适合与string类型。当然,因为string类型比vector模板更特例化一些,因此它肯定具有一些自己特有而vector没有的特性,下面总结一下。
在陈述之前,首先说明:
1. 在string中(有一些也适用于C风格的字符串),我们可以使用一组迭代器/单个迭代器(从此迭代器开始到字符串末)/位置+长度表示范围/单个位置(从此位置到字符串末)来表示字符串中的范围, 这样的参数记作range.
2. 可以使用列表初始化的字符串/使用字符串+range的组合形式表示的子字符串 / 字面值常量(如“china”)来表示字符串。 这里的字符串包括string类型的字符串和C风格的char* 字符串。 字符串使用字符args 表示。
正因为pos和args的样式可以随意组合,所以string的操作函数的参数是多种的,因此它的重载函数数目很多,由于对于insert(pos, args)/append(args)/erase(pos,args)/replace(pos, args)等操作。
a. string的初始化
相对于vector类型来说, string 增加一个使用字面值类型进行初始化,即:
1 string a("xiaoming") 2 string a = "xiaoming"
b. string中包含的专有的操作(相对于vector来说)
1. string的添加与替换
在string中,增加了append()与 replace()函数
str.append(args) // 在尾部添加一个字符或一个字符
str.replace(pos, args) // 在尾部添加一个字符或一个字符 ,它的重载函数很多,共16个。
2. string的访问子字符串:
str.substr(_pos, n) //该函数可以获得原字符串中的部分字符, 从pos开始的n个字符,当_pos超过范围时,会抛出out_of_range的异常。
3. str的搜索操作:
str.find(args) //查找args 第一次出现的位置
str.rfind(args) //查找args最后一次出现的位置
str.find_first_of(args) //搜索的是字符, 第一个是args里的字符的位置
str.find_last_of(args) // 搜索的是字符, 最后一个是args里的字符的位置
str.find_first_not_of() // 搜索的是字符,第一个不是args里的字符的位置
str.find_last_not_of() // 搜索的是字符, 最后一个不是args里的字符的位置
4. str的大小操作:
str.length() // 该函数与str.size()函数完成一样,只是名字不同而已罢了。只所以这样搞的原因,可能开发人员感觉length更适合字串符,size更适合容器吧。
c字符串的转换函数
1. 由数值转换为字符串:
to_string(val):
2. 由字符串转换为数值:(要转换的string的第一个非空白符必须是数值中可能出现的字符,处理直到不可能转换为数值的字符为止,以下内容来自:c++primer)
stoi(str, pos, base) // 字符串转换为整型,其中str表示字符串, pos用于表示第一个非数值字符的下标(意思就是我给函数传入一个地址,它会对它进行赋第一个非数值字符的位置), base表数值的基数,默认为10,即10进制数。
stol(str, pos, base) // 转换为long
stoul(str, pos, base) // 转换为 unsigned long
stoll(str, pos, base) // 转换为 long long
stoull(str, pos, base) // 转换为unsigned long long
stof(str, pos) // 转换为float
stod(str, pos,) // 转换为double
stold(str, pos,) // 转换为long double
d 对字符的操作(在cctype头文件中,并不属于string头文件的范围,但是关系很紧密的)
以下内容来自:c++ primer 第五版p82, 只写出部分常用来的(字母:alpha, 数字:number或digit)
isalnum(c) // 当为字母或数字时为真
isalpha(c) // 当为字母时为真
isdigit(c) // 当为数字时真
islower(c) // 当为小写字母时为真
issupper(c) // 当为大写字母时为真
isspace(c) // 当为空格时为真
tolower(c) // 转换为小写字母, 当本身为小写字母时,原样输出
toupper(c) // 转换为大写字母, 当本身为大写字母时,原样输出
3. list 容器
与vector和string相比,list内部的实现为一个双向链表,它的元素不是存储在连续的内存空间中,而是非连续的,这就决定了它不能在常量时间内完成对元素的随机访问,只能从头到尾的遍历一遍。 因为它是用双向链表实现的,所以,它的一大特性就是它的迭代器永远不会变为无效(除非这段空间不存在了),即无论增加、删除操作,都不会破坏迭代器。
大多数对vector的操作也适合于list,由于底层实现不同,有也差异:
list与vector的差别:
1. list支持push_front()、pop_front()操作
2. list不支持vector中的随机访问操作,即使用v1.at( )和v1[ ] 操作。
3. list的删除与增加元素的操作不会破坏迭代器,而 vector与string 会使迭代器失效。
4. list 内部增加了一个sort()的方法,用于实现排序,不过呢,反正我感觉基本不用它,直接用<algorithm>里的范型sort()更好啊啊。
5. list增加了一个类似insert()的函数,为splice( ) :该函数可以实现在常数时间内把一个list 插入到另一个list内,与insert()的区别在于insert是进行copy, 而splice()直接操作的链表的指针指向。它有好几个重载函数。
6. list的去重复函数: unique(); 该函数的作用是去除连续重复的元素,参数即可以为空,也可以传入一个二元谓词,用于确定相等的比较算法。 因为unique()函数可能去除连续重复的元素,因此,很依赖配合上sort()函数使用啊。
7. list的合并函数merge(): 该函数就是合并两个list, 它在合并过程中会在两个链表之间进行来回的比较,如果原来的两个list是有顺序的,合并之后的结果也是有序的,如果合并之前是无序的,合并之后也是无序的。反正吧,这个比较就这样。
4. forward_list容器
forward_list的实现是使用单向链表(list为双向链表), 在操作单向链表的时候,为了对一个元素进行删除与添加,都需要访问到该元素的前趋节点,因此呢,forward_list的会有insert.after()emplase.after()/erase.after()等操作, 另外forward_list也没有size()操作,原因在于为了尽可能让forward_list与手写的单向链表的效率相同。说实话呢,forward_list操作起来有点反人类,用起来有点不方便,我个人比较买习惯使用list,但是list相对forward_list的内存空间花费更多。
以后什么时候用它的时候,再来介绍。
5. deque容器(double-end queue, 双端队列)
6.
有序关联容器
关联容器与顺序容器最大的区别在于关联容器没有下标,都过键值或 值本身进行索引。有序关联容器内部通过红黑树实现的,当搜索一个元素时,具有O(logn)的平均复杂度,而无序的关联容器在底层是通过散列表(哈希函数映射)实现的,当搜索一个元素时,通常O(1)的平均复杂度,最坏为O(logn), 下面介绍它们。
1. map 容器
在介绍map之前,必须先介绍pair 类型。
pair类型:
1. pair类型定义在头文件utility中。
2. pair类型为一个结构体类型的模板,(在c++中结构体与类,除了默认的访问符不同,没有其它任何区别)
3. pair 有两个public的数据成员,分别为first与second.
4. pair的初始化与大多数结构体或类的初始化相同:
- pair<int, string> sb //初始化一个默认值的pair对象sb, 它的first是默认初始化的(0,内置类型默认初始化大多数应该是未定义的啊,它这是为0), second也是采用默认初始化(空字符串)
- pair<int, string> sb(1, "japan"); //很常见的初始化方法
- pair<int, string> sb = (1, "japan");
- pair<int, string> sb{1,"japan"} //c++11中的列表初始化方法
- pair<int, string> sb = {1, "japan"}
- 可以调用make_pair()模板函数,返回一个pair对象:
1. map是用于存放键-值对的容器,它使用pair的first数据成员表示键(key),second数据成员表示对应的值(value),所以呢,map是存放pair类型对象的容器。在map中,key都是固定的,一旦使用就不可以改变,而value是可以改变的, 因此会把pair类型的first数据成员的类型声明为const。
2. map的特性之一是:按value的大小进行有序存放(unordered_map是无序的), 因此,构造mqp容器时,要求它的key类型必须能够比较大小,当使用自定义的类类型时,
应该把重载的 operator< 运算符传递给map, 例如:
1 // 添加相关代码 2 3 4 5 ..
3.在map中:
- ::value_type表示"键-值 对"类型
- ::key_type表示键类型,vlue类型
- ::mapped_type 表示值的类型
例如: map<int, string>, 则 map<int, string>::value_type 与pair<int, string>等价, map<int, string>::key_type与int等价, map<int, string>::mapped_type与string等价;
4. map的访问操作:
- map同样支持使用迭代器,它会返回指向 pair类型的对象 的迭代器
- map 使用[]运算符 通过key来访问对应的 value ,如果访问的key不存在,则会自动添加一个对应的pair 对象,其中它的value采用默认值。因此,当通过key来访问map时,
- map不能是const类型。
- map 使用at()成员函数 通过key来访问对应的value, 如果访问的key不存在,则会抛出一个out_of_range的异常;
5. map的添加与删除操作:
- insert()或emplace()操作: 当向map中插入不存在的元素(指key值不同)时,可以插入成功,当插入一个已经存在key值的pair对象时,ma不会作任何改变。因此,当对map进行插入操作时,需要知道有没有插入成功。insert()与emplace()函数的 返回值也是一个pair类型,first为一个迭代器,指向插入时的键值对应的pair对象(可能是新插入的,也可能是已经存在的), second是一个bool类型,它表示是否插入成功(例如:当map中已经存在待插入的值时,为false)
- erase()操作:它有三个版本,前两个版本与顺序容器相同,使用迭代器指定一个位置或一对迭代器指定一个范围,这时返回值为一个迭代器,指向删除之后的下一个元素;第三个版本的erase()很不错,我很喜欢,它的参数为key值,删除对应key值的pair()对象, 返回值为成功删除的个数(可能为0或1,在multimap中可能为n)
6.查找操作
- find(key): 查找一个特定key值的pair对象,如果找到就返回对应的迭代器,如果找不到,就返回.end()迭代器。
- count(key):统计在map容器中特征key值的pair对象的个数.(在multimap与multiset中很有用的)
- equal_range(key) // 返回一个pair类型,first表示low_bound, second表示upper_bound;
- lower_bound(key) //返回迭代器,对应第一个大于等于key的元素
- upper_bound(key) //返回迭代器,对应第一个大于key的元素 (说明:其实,最后这四个函数,在multimap与multiset中是非常有用的)
2. multimap容器:
与map容器相比,区别在于multimap允许键值重复,即一个键值可能对应多个value。所以呢,相应的操作会有一些变化,例如:multimap不可以像map中使用key 作为索引(使用operator[]和at()成员函数)进行访问元素(因为对应的value可能是多个),multimap的插入操作一定会成功等,除此之外,它们的性相同, 不多介绍。
3. set容器:
set容器与map容器的唯一区别在于:存放的元素类型不同: map存储的是键-值对,即pair类型,而set中只存放键值。正因为如此,所以:
- 1. set只有::value_type与key_type类型,没有::mapped_type类型;
- 2. set不需要索引访问操作(通过operator[]和at()函数)
除此之外, set与map也没有什么其它区别了。
4. multiset容器:
multiset容器相对于set容器,允许它容器内部的元素重复。没有其它区别了。
无序关联容器:
1. unordered_map容器:
d
2. unordered_multimap容器:
d
3. unordered_set容器:
d
4. unordered_multiset容器:
d
在