今天我要讲的是标准库里的一块“重地”:容器,它也是 C++ 泛型编程范式的基础。
不过在正式开讲之前,我先问你个问题:什么是容器?
你也许会说:容器,就是能够“容纳”“存放”元素的一些数据结构。
这个回答非常正确,而且说到了“点”上。
还记得计算机先驱的那句经典名言吗?“算法 + 数据结构 = 程序。”在 C++ 里,容器就是这个公式里面的“数据结构”。
所以,下面我就着重从数据结构的角度,来谈谈各种容器的区别、优缺点,还有如何选择最合适的容器。
认识容器
所谓的数据结构,就是数据在计算机里的存储和组织形式,比如堆、数组、链表、二叉树、B+ 树、哈希表,等等。
在计算机的发展历史上,众多“大牛”孜孜不倦地发明创造了这么多的数据结构,为什么呢?
因为没有一种数据结构是万能的、可以应用于任何场景。毕竟,不同的数据结构存储数据的形式不一样,效率也就不一样。有的是连续存放,有的是分散存放,有的存储效率高,有的查找效率高,我们必须要依据具体的应用场合来进行取舍。
我想,你肯定已经学过这些数据结构了,也知道它们的实现原理,自己写也不是什么太难的事情。
但是,对于最基本、最经典的那些数据结构,你完全没有必要去“自己造轮子”,因为 C++ 标准库里的容器就已经把它们给实现了。
容器,其实就是 C++ 对数据结构的抽象和封装。而且,因为标准库开发者的功力很深,对编译器的了解程度更是远超你我,所以,容器的性能和优化水平要比我们自己写的好上几十倍,这一点你绝对不用质疑。
我们要做的,就是仔细品鉴标准容器这盘大餐,从中找出最合适自己口味的那道菜。
由于容器相关的资料已经有很多了,无论是看图书还是网站,都可以找到非常详细的接口文档,所以今天,我就不去罗列每个容器的具体操作方法了,而是把重点放在特性介绍上。掌握了这些特性,今后你在面临选择的时候,不用太纠结,就可以选出最适合你的容器。
容器的通用特性
你必须要知道所有容器都具有的一个基本特性:它保存元素采用的是“值”(value)语义,也就是说,容器里存储的是元素的拷贝、副本,而不是引用。
从这个基本特性可以得出一个推论,容器操作元素的很大一块成本就是值的拷贝。所以,如果元素比较大,或者非常多,那么操作时的拷贝开销就会很高,性能也就不会太好。
一个解决办法是,尽量为元素实现转移构造和转移赋值函数,在加入容器的时候使用 std::move() 来“转移”,减少元素复制的成本:
Point p; // 一个拷贝成本很高的对象
v.push_back(p); // 存储对象,拷贝构造,成本很高
v.push_back(std::move(p)); // 定义转移构造后就可以转移存储,降低成本
你也可以使用 C++11 为容器新增加的 emplace 操作函数,它可以“就地”构造元素,免去了构造后再拷贝、转移的成本,不但高效,而且用起来也很方便:
v.emplace_back(...); // 直接在容器里构造元素,不需要拷贝或者转移
当然,你可能还会想到在容器里存放元素的指针,来间接保存元素,但我不建议采用这种方案。