这是《深入实践C++模板编程》第五章“容器、迭代器和算法”的读书笔记。
容器、迭代器和算法
通过C++模板可以将类型以及其他编译期常数作为参数抽离出来,使代码拜托对类型依赖,从而设计容纳不同类型的容器成为可能。
- 容器是指专门用于存储某种形式组织及存储的类。
容器的实现
常见的容器有数组、链表、集合、关联组等。通常语言本身会提供几种基本容器,通过这些基本容器可以组织更复杂的容器。这些容器的共同点是可以容纳多种类型的数据。
弱类型的动态语言,对于数据类型不敏感,其提供的类型天然支持多种数据类型。例如Python中的list
,dict
,set
等,可以在同一个容器实例中保存不同类型的数据,实现异质容器。
强类型语言,变量以及函数惨了类型固定变,这是设计支持多种数据类型容器的最大障碍。C++通过模板,使得容器和算法结构不再依赖具体类型;但是这并没有实现真正支持多种类型的容器类。支持不同类型,只是生成了不同类型的容器类。
除了C++模板,其他强语言类型也有各自容器实现方法。这里介绍Java实现容器方法。
Java的实现方法
与C++相比,Java是更高级的面相对象语言。Java中所有类都必须同一颗继承树上的某个节点;这颗继承树的根节点是类Object
。除了几种有限的“原始(primitive)”类型外,所有类型都是标准根节点Object的派生类。即使是“原始”类,在继承树上也有对应的封装类。
Java的任何实例,可以**上转义(upcast)**成为基类实例,所以所有类都可以转义为Object实例。且Java中的变量是一个类实例的引用,从C++角度看,变量都是指针;所以变量值传递只是地址的传递,不会有任何数据损失。
正因为上面这两个特点,Java中的单一容器可以容纳多种数据类型。容器只需设计为保存Object类型数据,任何实例都可以上转义为Object类型实例后放入容器,从容器取数据时,必须**下转义(downcast)**为所需类型实例引用。
Java这种实现存在风险,例如将类型A实例上转义为Object类型保存到容器,取出时可以下转义为B类型。在语法上没有错误,编译时只会给出警告,如果有错误,只有在运行时Java解释器才会发现这个非法转义操作。
从Java SE(Java Standard Environment)1.5版本开始,引入了**泛型(Generic)**概念,可以约束容器只接纳一种数据类型,这也是新版Java推荐的做法。
Java中的泛型和C++中模板看似类似,但是本质不同。模板是根据类型在编译期生成不同类型容器,泛型只是增加约束,在编译期做了额外检查以及取出时自动做了类型转换,Java中容器实现并没有改变。
C++的实现方法
C++没有Java的那两个特点
- 没有官方继承树。
- 变量不是引用,即变量不是地址。
定义继承树,就会限制C++开发者的自由,且C++必须显式集成。变量不是引用,做变量类型转换上转义时,会有数据丢失,即对象切割(object slicing)。
C++中是否可以在容器中保存地址,即指针?可以,但是比较危险。C++的内存需开发者自己管理,保存在容器中,何时释放、由谁释放都是问题,稍有不慎,就会出错。
C++中用模板实现容器,是其语言本身特性决定的。在当下,使用模板实现容器是明智的选择;但也不排除后续C++引入新的特性,就会有新的容器实现方式。
容器与迭代器
容器可以存储数据,从容器中取数据,也需要统一的标准,即迭代器(iterator)。
迭代器设计类似于指针思想。通过遍历数组,可以得到启发。例如有一个数组array[]
,遍历它可以通过两种方式:
// 遍历法一
for (unsigned i = 0; i < sz; ++i)
do_something(array[i]);
// 遍历法二
int* begin = array;
int* end = begin + sz;
for(int* it = begin; it != end; ++it)
do_somethin(*it);
第一种遍历方法比较直观,它依赖于数组内存连续。迭代器设计类似于第二种遍历方法,通过++it
来找到下一个元素位置,可以通过容器内在元素之间的联系实现++it
。
容器中的元素,总可以通过某种规则排列成一个虚拟序列。例如链表天然就是一个序列;二叉树可以按照前序、中序、后序遍历成一个序列;图可以按照广度优先或深度优先排序成一个序列。
容器可以通过封装代理类实现迭代器。例如重载自加操作符++
指向序列中下一个元素,重载*
取数据。
下面实现两个简单的容器及其迭代器。
链表容器与迭代器
/ list_iterator.hpp
#pragma once
#include <stdexcept>
template<typename T> class list; // forward declare
template<typename N>
class list_iterator {
N* pos;
template<typename T> friend class list;
public:
typedef typename N::value_type value_type;
typedef typename N::reference_type reference_type;
typedef typename N::const_reference_type const_reference_type;
typedef list_iterator<N> self_type;
list_iterator() : pos(0) {}
list_iterator(N* pos) : pos(pos) {}
bool operator != (self_type const& right) const {
return pos != right.pos;
}
bool operator == (self_type const& right) const {
return pos != right.pos;
}
self_type& operator ++ () {
if (pos) pos = pos->next;
return *this;
}
reference_type operator * () throw (std::runtime_error) {
if (pos) return pos->value;
else throw (std::runtime_error("deferenct null iterator!"));
}
};
// list.hpp
#pragma once
#include <stdexcept>
#include "list_iterator.hpp"
template<typename T>
struct list_node {
typedef T value_type;
typedef T& reference_type;
typedef const T& const_reference_type;
T value;
list_node* prev;
list_node* next;
list_node(T const& value, list_node* prev, list_node* next) :
value(value), prev(prev), next(next) {}
};
template<typename T>
class list {
typedef list_node<T> node_type;
node_type *head;
public:
typedef T value_type;
typedef list_iterator<node_type> iterator;
list(): head(nullptr) {}
~list() {
while (head) {
node_type* n = head;
head = head->next;
delete n;
}
}
void push_front(const T& v) {
head = new node_type(v, 0, head);
if (head->next) head->next->prev = head;
}
void pop_front() {
if (head) {
node_type* n = head;
head = head->next;
head->prev = nullptr;
delete n;
}
}
void insert(iterator it, const T& v) {
node_type* n = it.pos;
if (n) {
node_type* new_node = new node_type(v, n, n->next);
new_node->next->prev = new_node;
n->next = new_node;
}
}
void erase(iterator& it) {
node_type *n = it.pos;
++it;
if (n) {
if (n->next) n->next->prev = n->prev;
if (n->prev) n->prev->next = n->next;
if (head == n) head = head->next;
delete n;
}
}
bool is_empty() const {return head == 0;}
iterator begin() {return iterator(head);}
iterator end() {return iterator();};
};
// test.cpp
#include <iostream>
#include "list.hpp"
int main(int argc, char* argv[]) {
list<int> l;
for (int i = 0; i < 10; ++i) l.push_front(i);
for(auto it = l.begin(); it != l.end(); ++it) {
std::cout << *it << std::endl;
}
return 0;
}
集合与迭代器
集合通常采用红黑树实现,这个为了简单,采用二叉搜索树实现。假设树节点有公共成员变量value
,公共成员变量parent
、left
、right
是分别指向其父节点、左右节点的指针。二叉树中序遍历得到的序列是从小到大,所以遍历时采用中序。
// tree_iteartor.h
#pragma once
#include <stdexcept>
template<typename N>
class tree_iterator {
const N* pos; // 当前位置
public:
typedef typename N::value_type value_type;
typedef typename N::const_referenct_type const_referenct_type;
typedef tree_iterator<N> self_type;
tree_iterator() : pos(nullptr) {}
tree_iterator(const N* pos) : pos(pos) {}
bool operator == (self_type const& right) const {
return pos == right.pos;
}
self_type& operator++ () {
if (pos) {
if (pos->right) {
pos = pos->right;
while (pos->left) pos = pos->left;
}
else {
while ((pos->parent) && (pos->parent->right == pos)) {
pos = pos->parent;
}
pos = pos->parent;
}
}
return *this;
}
const_referenct_type operator* () const throw (std::runtime_error) {
if (pos) return pos->value;
else throw std::runtime_error("dereferencing null iterator!");
}
};
template<typename N>
bool operator != (tree_iterator<N> const& left, tree_iterator<N> const& right) {
return !(left == right);
}
// set.h
#pragma once
#include "tree_iterator.h"
#include <iostream>
template<typename T>
struct tree_node {
typedef T value_type;
typedef T& reference_type;
typedef const T& const_referenct_type;
T value;
tree_node* parent;
tree_node* left;
tree_node* right;
tree_node(T const& value, tree_node* parent, tree_node* left, tree_node* right) :
value(value), parent(parent), left(left), right(right) {}
~tree_node() {
if (left) delete left;
if (right) delete right;
}
};
template<typename T>
class set {
typedef tree_node<T> node_type;
node_type* root;
public:
typedef T value_type;
typedef tree_iterator<node_type> const_iterator;
set(): root(nullptr) {}
~set() { if(root) delete root;}
bool insert(const T& v) {
node_type** n = &root;
node_type* p = nullptr;
while (*n) {
if (v == (*n)->value) // 集合中已经有该值
return false;
else {
p = *n;
n = v < (*n)->value ? &((*n)->left) : &((*n)->right);
}
}
*n = new node_type(v, p, nullptr, nullptr);
// std::cout<< "insert ["<< v << "] , parent [" << (p ? p->value: 0 )<< "]" << std::endl;
return true;
}
bool has(const T& v) {
node_type *n = root;
while (n) {
if (v == n->value)
return true;
n = v < n->value ? n->left : n->right;
}
return false;
}
bool is_empty() const {return root == nullptr;}
const_iterator begin() const {
// 定位到最左节点,即中序遍历的第一个节点
node_type *n = root;
while (n->left)
n = n->left;
return const_iterator(n);
}
const_iterator end() const {return const_iterator();}
};
// test_set.cpp
#include "set.h"
#include <stdlib.h>
#include <iostream>
int main(int argc, char* argv[]) {
set<int> my_set;
for (int i = 0; i < 10; ++i) {
int num = rand() % 20;
std::cout << num << "--";
my_set.insert(num);
}
std::cout << std::endl;
int count = 0;
for(auto it = my_set.begin(); it != my_set.end(); ++it) {
std::cout << *it << "--";
++count;
if (count > 10) break;
}
std::cout << std::endl;
}
上面两个容器不同,但是迭代器有着相同的接口:可以通过++
找到下一个元素,可以通过*
解引用。
迭代器与算法
有了迭代器之后,遍历容器或者访问容器某个区间可以和具体容器解耦和,使得通用性算法实现变得可能。
求容器中元素纸盒
最直观的实现是在函数内调用begin()
得到其实元素,之后累加
template<typename C>
typename C::value_type
sum(C& c) {
typedef typename C::value_type value_type;
typedef typename C::iterator iterator;
value_type sum(0);
for (iterator i = c.begin(); i != c.end(); ++i) sum += *i;
return sum;
}
上面的函数可以计算大部分容器,但是还有2点不足:1、无法求解部分元素之和;2、不兼容数组(C++11已经兼容了),因为数组内没有嵌套定义value_type。下面实现一个求解区间元素之和的函数
template<typename I>
typename I::value_type
sum(I begin, I end) {
typedef typename I::value_type value_type;
value_type sum(0);
for(; begin != end; ++begin) sum += *begin;
return sum;
}
上面代码已经可以求解区间元素的和了。但是还是不支持数组;数组内没有嵌套定义value_type
,无法知道数组内元素类型。
要统一数组和容器,还需要借助另外一个类模板及其模板特例来统一描述迭代器和指针。这个模板只是用来嵌套定义元素值类型,如果模板参数是迭代器,则重新定义值类型,如果是指针,则在模板中为指针定义元素类型。
template<typename I>
struct iterator_traits {
typedef typename::I::value_type value_type;
};
template<typename P>
struct iterator_traits<P*> {
typedef P value_type;
};
template<typename I>
typename iterator_traits<I>::value_type
sum(I begin, I end) {
typedef typename iterator_traits<I>::value_type value_type;
value_type sum(0);
for (; begin != end; ++begin) sum += *begin;
return sum;
}
微型算法库
这个库由一个头文件组成,可以对容器元素求和,还可以定位元素、打印元素移位等。
#include <iostream>
template<typename I>
struct iterator_traits {
typedef typename::I::value_type value_type;
};
template<typename P>
struct iterator_traits<P*> {
typedef P value_type;
};
// 打印
template<typename I>
void print(I begin, I end) {
if (begin != end) {
std::cout << *begin;
for (++begin; begin != end; ++begin)
std::cout << " " << *begin;
std::cout << std::endl;
}
}
// 求和
template<typename I>
typename iterator_traits<I>::value_type
sum(I begin, I end) {
typedef typename iterator_traits<I>::value_type value_type;
value_type sum(0);
for (; begin != end; ++begin) sum += *begin;
return sum;
}
// 指定范围内数据循环移位
template<typename I>
void shift(I begin, I end) {
typedef typename iteartor_traits<T>::value_type value_type;
I it = begin;
if (it != end) {
value_type v = *begin;
value_type tmp;
for (++it; it != end; ++it) {
tmp = *it;
*it = v;
v = tmp;
}
*begin = v;
}
return;
}
// 容器内查找并定位指定数据
template<typename I>
I find(I begin, I end, typename I::value_type const& v) {
for(; begin != end; ++begin)
if (*begin == v) break;
return begin;
}
容器和迭代器的分类
上面实现的链表和集合代表了两类容器:序列型容器(sequence container)和关系型容器(associative container)。
- 序列型容器:数据存储位置和内容无关,通常按照一定的序列组织,例如数组、链表、向量等。
- 关系型容器:数据存储位置和内容有关,例如集合、字典、散列表等。
序列型容器提供的迭代器可以修改数据,而关系型容器则不可以(是指不可以修改key)。
按照迭代器在容器上的移动能力,可以分为三类:
- 前向迭代器(forward iterator):只能移动到下一个元素,例如单向链表的迭代器。这也是最基本的迭代器。
- 双向迭代器(bidirectional iterator):可以移动到下一个或上一个元素的迭代器。
- 随机迭代器(random iterator):可以前向或后向跳若干个元素的迭代器。
容器的陷阱
迭代器引入了一致性的陷阱,写代码时需要小心。有以下几点需要注意:
- 容器中,以迭代器为参数对容器进行操作,该迭代器必须来自此容器,否则会有undefined behavior。
- 一次性使用多个迭代器,要防止彼此之间冲突。
- 要防止迭代器失效。如果使用迭代器在容器中删除了元素,那么
operator()++
很可能失效,因为删除操作可能改变了容器结构。