写在前面
毕业论文交了,毕设还需要修改,抽空继续复习C++
本文也是《Effective STL》的阅读笔记
正文
一、容器
1. 慎重选择容器类型
C++ 提供的几种容器:
- 标准STL序列容器:
vector
、string
、deque
和list
。 - 标准STL关联容器:
set
、multiset
、map
和multimap
。 - 非标准序列容器:
slist
和rope
。 - 非标准的关联容器:
hash_set
、hash_multiset
、hash_map
和hash_multimap
。 vector<char>
作为string
的替代。vector
作为标准关联容器的替代。- 几种非标准STL容器:数组、
bitset
、valarray
、stack
、queue
和priority_queue
.
C++ 标准就“如何在 vector
、deque
和 list
中做出选择” 提供了如下建议:
- vector 是默认应使用的序列类型;
- 当需要频繁地在序列中间插入和删除操作时,应使用list;
- 当大多数插入和删除操作发生在序列头部和尾部是,deque是应考虑的数据结构。
对于连续内存容器 和 基于节点的容器的区分:
- 连续内存容器,也称为基于数组的容器。把元素存放在一块或多块内存中,每块内存中存有多个元素。当有新元素插入或已有元素被删除时,同一内存中其他元素要向前或后移动。标准连续内存容器有
vector
、string
和deque
;非标准:rope
。 - 基于节点的容器,在每一个内存块中只存放一个元素。容器中元素的插入或删除只影响到指向节点的指针,不影响节点本身的内容。基于点的容器有:链表容器如
list
和slist
;所有标准关联容器;非标准的哈希容器使用不同的基于节点的实现。
书中对读者的忠告,我删减了一些我觉得不必要的:
- 是否需要在容器的任意位置插入新元素? 如果需要,选择序列容器;关联容器是不行的
- 是否关系容器中元素如何排序?如果关心容器中元素的排序,千万不要使用哈希容器
- 需要哪种类型的迭代器?如果是随机访问迭代器,容器选择限定为
vector
、deque
和string
。也可以考虑rope
。如果要求使用双向迭代器,必须避免使用slist
以及哈希容器。 - 插入或删除操作时,避免移动容器中原来的元素是否很重要?如果是,就要避免连续内存的容器
- 容器中数据的布局是否需要和C兼容? 如果需要,只能选择
vector
- 元素查找速度需求大,优先考虑哈希容器,排序的
vector
和 标准内联容器 - 是否介意容器内部使用了引用计数技术,如果介意,就要避免使用
string
和rope
。比如string
可以用vector<char>
替代 - 对于插入和删除操作,需要事务语义吗?比如插入失败时,需要滚回能力吗?如果需要,就要使用基于节点的容器。如果对多个元素的插入操作需要事务语义,则需要选择
list
。 - 需要使迭代器、指针和引用变为无效的次数最少吗?如果是,就要选择基于节点的容器,因为这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效。
2. 不要试图编写独立于容器类型的代码
有时不可避免地要从一种容器类型转换到另一种,可以使用常规的方式来实现这种转变:使用封装技术。最简单的方式是通过对容器类型和其迭代器类型使用类型定义。
请不要这样写:
class Widget{...};
vector<Widget> vw;
Widget bestWidget;
... //此处为 bestWidget 和 vw 赋值
vector<Widget>::iterator i = find(vw.begin(), vw.end(), bestWidget); //试图在vw中找与bestWidget相同值的Widget
而要这样写:
class Widget{...};
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
...
WCIterator i = find(cw.begin(), cw.end(), bestWidget);
这样就使得改变容器类型容易的多,尤其当这种改变仅仅是增加一个自定义分配子时,就显得更为方便。
class Widget{...};
template<typename T>
SpecialAllocator{...};
typedef vector<Widget, SpecialAllocator<Widget>> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw; //仍可工作
Widget bestWidget;
...
WCIterator i = find(cw.begin(), cw.end(), bestWidget); //仍可工作
类型定义这个玩意儿确实有用,他现在解决了我一个一直以来感觉有点麻烦但也不在乎的一个问题
假设有如下类型的对象
map<string, vector<Widget>::iterator, CIStringCompare> //ISStringCompare 是不区分大小写比较
如果想用const_iterator
来遍历此 map,可能需要把这行敲入很多遍,比如
map<string, vector<Widget>::iterator, CIStringCompare> :: const_iterator
虽然类型定义只不过是其其它类型的别名,所以它带来的封装纯粹是词法上的。所以:
- 不想把自己选择的容器暴露给客户,就得用
class
class CustomerList { private: typedef list<Customer> CustomerContainer; typedef CustumorContainer::iterator CCIterator; CustomerContainer customers; public: ... //把想暴露的部分写在这里 }
- 只是想要减少替换类型所修改的代码,可以用
typedef
3. 确保容器中的对象拷贝正确而高效
从容器中取出一个对象时,所取出的不是容器中所保存的那份。得到的是容器中所保存对象的拷贝。
进去的是拷贝,出来的也是拷贝(copy in,copy out),这就是STL的工作方式。
一个对象被保存到容器中,它经常会进一步拷贝。比如 vector、string或 deque 中进行元素插入或删除操作时,现有元素会被移动(拷贝),像排序算法,next_permutation
或 previous_permutation
,remove
,unique
或 类似的操作,rotate
或 reverse
等等,那么对象将会被移动(拷贝)。
拷贝构造函数复杂度高的话,会严重影响效率。
使拷贝动作高效、正确,防止剥离问题发生的一个简单办法是使容器包含指针而不是对象。
4. 调用 empty 而不是检查 size() 是否为 0
虽然
if(c.size() == 0)...
本质上与
if(c.empty()) ...
是等价的。
但是,empty
通常被实现为内联函数,而且对于一些list
实现,size
耗费线性时间。
5. 区间成员函数优先于与之对应的单元素成员函数
有两个 vector
,v1 和 v2,是v1的内容和v2的后半部分相同的最简单操作是什么?
v1.assign(v2.begin() + v2.size() / 2, v2.end());
最坏的方法是使用循环,避免循环的一种方法是使用copy
算法
v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
而 copy 可以替换为
v1.clear();
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
通过利用插入迭代器的方式来限定目标区域的copy
调用,几乎都应该被替代为对区域成员函数的调用。 因为使用区域成员函数,通常可以少写一下代码,意图清晰和更加直接,最重要的是他们有时有更高的效率。
支持区间的成员函数
- 区间创建
标准容器都具有如下形式的构造函数container::container(InputIterator begin, InputInterator end);
- 区间插入
标准序列容器都提供了如下形式的insert
关联容器利用比较函数决定了元素该插入何处,省去了positionvoid container::insert(iterator position, InputIterator begin, InputIterator end);
void container::insert(InputIterator begin, InputIterator end);
- 区间删除
所有标准容器都提供了区间形式的删除操作,但对于序列和关联容器,返回值不同
序列容器提供了这样的形式
关联容器提供了如下形式iterator container::erase(iterator begin, iterator end); //返回了被删除元素之后的元素的迭代器
对于vector和string 内存会自动增长以容纳新元素,但减少时内存不会自动减少void container::erase(iterator begin, iterator end);
- 区间赋值
void container::assign(InputIterator begin, InputIterator end);
6. 当心 C++ 编译器最烦人的分析机制
震惊
这是一个构造,还是一个函数的声明?
List<int> data(istream_iterator<int>(dataFile), isteram_iterator<int>());
这是一个函数声明,省去参数名那个版本
如何解决?
把形式参数的声明用括号括起来是非法的,但给函数参数加上括号却是合法的,可以通过加一对括号,强迫编译器按照我们的方式来工作
List<int> data((istream_iterator<int>(dataFile)), isteram_iterator<int>());
但是,并不是所有编译器都这样
更好的解决办法是避免匿名
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
7. 如果容器中包含了通过 new 操作创建的指针,切记在容器对象析构前将指针 delete 掉
当容器包含的是通过new
的方式而分配的指针时,指针容器在自己被析构时会析构所有包含的元素,但指针的“析构函数”不做任何事情
比较好的办法是用智能指针
void doSomething()
{
typedef boost::shared_ptr<Widget> SPW;
vector<SPW> vwp;
for(int i = 0; i < SOME_MAGIC_NUMBER; ++i)
{
vwp.push_back(SPW(new Widget));
...
}
}
STL 容器很智能,但没有智能到知道是否删除自己所包含的指针的程度。
8. 切勿创建包含 auto_ptr 的容器对象
根据第3条,STL的核心是拷贝。当拷贝一个auto_ptr
时,它所指向的对象的所有权被移交到拷入的auto_ptr
上,而它自身被置为NULL
。
9. 慎重选择删除元素的方法
设定有一个标准的STL容器c,它包含int类型整数
Container<int> c;
现在想要删除 c 中所有值为 1963 的元素。
-
对于连续内存容器(
vector
、deque
或string
),最好的办法是使用erase-remove
习惯用法c.erase(remove(c.begin(),c.end(),1963), c.end());
-
对于
list
,remove
更加有效c.remove(1963);
-
当c是标准内联容器(
set
、multiset
、map
或multimap
)时,应调用erase
:c.erase(1963);
现在,我们删除所有使用下面判别式返回 true 的每一个对象
bool badValue(int);
- 对于序列容器(
vector
、string
、deque
和list
),把remove
调换成remove_if
就可以了c.erase(remove_if(c.begin(),c.end(),badValue), c.end()); c.remove_if(badValue);
- 对于标准内联容器
- 使用
remove_copy_if
把需要的值拷贝到一个新容器中,然后把原来容器的内容和新容器的内容相互交换AssocContainer<int> c; ... AssocContainer<int> goodValues; remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValue.end()), badValue); //把不被删除的值从 c 拷贝到 goodValues中 c.swap(goodValues);
- 写一个循环,手动判断。但是直观的直接删除,当容器中一个元素被删除时,指向该元素的所有迭代器都将变得无效,我们要在调用erase之前,使一个迭代器指向c中的下一个元素
AssocContainer<int> c; ... for(AssocContainer<int>::iterator i = c.begin(); i != c.end(); /* 什么也不做 */) { if(badValue(*i)) { c.erase(i++); } else { ++i; } }
- 使用
现在,问题再次改变如果想要删除的同时 log 一下,对于连续内存容器(vector
、string
、deque
)删除会是被删除之后的迭代器都无效,解决办法是:
for(SeqContainer<int>::iterator i = c.begin(); i != c.end(); )
{
if(badValue(*i))
{
logFile << "Erasing" << *i << '\n';
i = c.erase(i); //利用c.erase的返回值
}
else
{
++i;
}
}
10. 了解分配子(allocator)的约定和限制
allocator 封装了STL容器在内存管理上的低层细节
如果需要编写自定义分配子需要记住:
- 分配子是一个模板,模板参数T代表你为它分配内存的对象的类型
- 提供类型定义 pointer 和 reference,但是始终让 pointer 为 T*,reference 为 T&
- 千万别让你的分配子拥有随对象而不同的状态(per-object state)。通常,分配子不应该有非静态的数据成员
- 记住,传给分配子的 allocate 成员函数的是那些要求内存的对象的个数,而不是所需的字节数。同时要记住,这些函数返回 T* 指针(通过pointer类型定义),即尚未有 T 对象被构造出来。
- 一定要提供嵌套的 rebind 模板,因为标准容器依赖该模板
不得不说,这一节我好多没看懂,可能有用到要回来重读几遍。
11. 理解自定义分配子的合理用法
提供了两个使用自定义分配子的例子
例1:
假定有一些特殊过程,采用 malloc
和 free
内存模型来管理一个位于共享内存的堆:
void* mallocShared(size_t bytesNeeded);
void* freeShared(void* ptr);
而你想把STL容器放到这块儿共享内存中
template<typename T>
class SharedMemoryAllocator
{
public:
...
pointer allocate<size_type numObjects, const void *localityHint = 0)
{
return static_cast<pointer>(malloShared(numObjects *sizeof(T)));
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
freeShared(ptrToMemory);
}
...
};
然后可以这样使用
typedef vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;
...
{
...
SharedDoubleVec v;//创建一个vector,元素位于共享内存当中
...
}
为了把 v 的内容和 v 自身都放到共享内存中,需要这样做
void *pVectorMemory = mallocShared(sizeof(SharedDoubleVec)); //为 SharedDoubleVec 对象分配足够的内存
SharedDoubleVec *pv = new (pVectorMemory) SharedDoubleVec; //在内存中创建一个SharedDoubleVec
...
pv->~SharedDouleVec(); //析构内存中的对象
freeShared(pVectorMemory); //释放最初分配的共享内存
例2:
假设有两个堆,Heap1类 和 Heap2类,分别有相应的静态成员函数来执行内存分配和释放操作:
class Heap1
{
public:
...
static void *alloc(size_t numBytes, const void *memoryBlockToBeNear);
static void dealloc(void *ptr);
...
};
class Heap2{...}; //同样的 alloc/dealloc 接口
如果想把STL容器的内容放在不同的堆里,首先编写一个分配子
template<typename T, typename Heap>
class SpecificHeapAllocator
{
public:
...
pointer allocate(size_type numObjects, const void *localityHint = 0)
{
return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T), localityHint));
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
Heap::dealloc(ptrToMemory);
}
...
};
然后使用 SpecificHeapAllocator 把容器的元素聚集到一起来:
vector<int, SpecificHeapAllocator<int,Heap1>> v; //v和s的元素都放在Heap1中
set<int, SpecificHeapAllocator<int, Heap1>> s;
list<Widget, SpecificHeapAllocator<Widget, Heap2>> L; //L和m的元素都放在Heap2中
map<int, string, less<int>, SpecificHeapAllocator<pair<const int, string>, Heap2>> m;
12. 切勿对 STL 容器的线程安全性有不切实际的依赖
对于STL的期望
- 多个线程读是安全的
- 多个线程对不同的容器写入操作是安全的
考虑当一个库试图实现完全的容器线程安全时可能采用的方式
- 对容器成员函数的每次调用,都锁住容器直到调用结束
- 在容器所返回的每个迭代器的生存期结束前,都锁住容器
- 对于作用于容器的每个算法,都锁住该容器,直到算法结束
二、 vector 和 string
13. vector 和 string 优先于动态分配的数组
因为用 new
动态分配数组还要惦记着正确回收,所以直接用 vector
和 string
比较省事
14. 使用 reserve 来避免不必要的重新分配
vector
和 string
可自动增长空间,增长过程类似 C# 的list
- 分配一块大小为当前容量的某个倍数的新内存。在大多数实现中,
vector
和string
的容器每次以 2 倍数增长,即,每当容器需要扩张时,他们的容器即加倍。 - 把容量的所有元素从旧的内存拷贝到新内存中
- 析构掉旧内存中的对象
- 释放旧内存
使用 reserve
成员函数能使你把重新分配的次数减少到最低限度,从而避免重新分配和指针/迭代器/引用失效带来的开销
在标准容器中,只有 vector
和 string
提供了所有这 4 个函数
size()
,容器中有多少个元素capacity()
,已经分配的内存可以容纳多少个元素resize(Container::size_type n)
,强迫容器改变到包含 n 个元素的状态reserve(Container::size_type n)
,强迫容器把它的容量变为至少是 n, 前提是 n 不小于当前的大小
总之,秘诀就是,尽早的使用 reserve 把容器的容量设的足够大,就可以减少后续因为容量不够而导致的重新分配
这就像用C# 中的 list
在初始化时指定大小的操作
vector<int> v;
v.reserve(1000);
15. 注意 string 实现的多样性
string有多重实现
几乎每个 string
实现都包含如下信息:
- 字符的大小(
size
),即,他所包含的字符的个数 - 用于存储该字符串中字符的内存的容器
- 字符串的值(
value
),即构成该字符串的字符
还可能包含:
- 分配子的一份拷贝
- 对值的引用计数
注意:
string
的值可能会被引用计数,也可能不会string
对象大小的范围可以是一个char*
指针的大小的 1 倍到 7 倍- 创建一个新的字符串值可能需要零次、一次或两次动态分配内存
string
对象可能共享,也可能不共享其大小和容器信息string
可能支持,也可能不支持对单个对象的分配子- 不同的实现对字符内存的最小分配单位有不同的策略
16. 了解如何把 vector 和 string 数据传给旧的 API
对于vector v
,\&v[0]
是指向第一个元素的指针,绝不可以用 v.begin()
替代
vector<int> v;
void doSomething(const int* pInts, size_t numInts);
...
doSomething(&v[0], v.size());
对于 string
,可以使用成员函数 c_str
返回一个指向字符串的值的指针
string s;
void doSomething(const char* pString);
...
doSomething(s.c_str());
17. 使用 “swap 技巧” 除去多余的容量
如果希望有一种方法能把vector
或string
的容量从以前的最大值减到当前需要的数量,可以用 swap
vector<Contestant> v;
string s;
...
vector<Contestant>().swap(contestants);
string().swap(s);
18. 避免使用 vector<bool>
vector<bool>
不完全满足 STL 容器的要求;最好不要使用它;可以用 deque<bool>
和 bitset
来替代它。
shocking
vector<bool> v;
bool *pb = &v[0];
不能编译,因为 vector<bool>
是一个假容器,并不是真的存储 bool
, 为了节省空间,它存储的是 bool
的紧凑表示。
一个经典的实现中, 储存 “vector” 中的每个 “bool” 仅占用一个二进制位,一个8位的字节可容纳8个“bool”。
三、关联容器
19. 理解相等(equality)和等价(equivalence)的区别
find
对“相同”的定义是相等是以operator==
为基础的set::insert
对“相同”的定义是等价,是以operator<
为基础的
我们在有些情况不得不自定义比较器
20. 为包含指针的关联容器指定比较类型
set<string*> ssp;
//等于
set<string*, less<string*>> ssp;
//等于
set<string*, less<string*>, allocator<string*>> ssp;
所以,如果想让string* 指针在集合中按字符串的值排序,那么你不能使用默认的比较函数子类,必须使用自己编写的比较函数子类
struct StringPtrLess : public binary_function<const string*, const string*, bool>
{
bool operator () (const string *ps1, const string *ps2) const
{
return *ps1 < *ps1;
}
};
然后可以用 StringPtrLess 作为 ssp 的比较类型:
typedf set<string*, StringPtrLess> StringPtrSet;
StringPtrSet ssp;
...
for(StringPtrSet::const_iterator i = ssp.begin(); i != ssp.end(); ++i)
{
cout << **i << endl; //输出
}
21. 总是让比较函数在等值的情况下返回 false
对于 set
和map
, multiset
和 multimap
皆是如此
22. 切勿直接修改 set 或 multiset 中的键
修改 set、multiset、map 和 multimap 中的元素,可以先拷贝一份,修改要修改的内容,删除原来的元素,最后出入进去。(删除通常通过调用 erase 来进行)
例子:
EmpIDSet se;
Employee selectedID;
...
EmpIDSet::iterator i = se.find(selectedID);
if(i != se.end())
{
Employee e(*i); //拷贝一份
e.setTitle("Corporate Deity"); //修改内容
se.erase(i++); //删除该元素
se.insert(i, e); //插入该元素
}
23. 考虑用排序的 vector 替代关联容器
标准关联容器通常被实现为平衡二叉树。
vector
占用空间小,如果数据量非常大,被分割到多个内存页面,那vector
需要更少的页面。
根据情况,加入很少有插入或删除操作,而且插入设置重组阶段划分明显,那么就考虑用排序的 vector
替代
24. 当效率至关重要时,请在 map::operator[] 与 map::insert 之间慎重做出选择
map::operator[]
与 vector
、deque
、string
的 operator[] 函数无关,与用于数组的内置 operator[]
也没有关系。
map::operator[]
的设计目的是为了提供 “添加和更新” 的功能。
对于
map<K, V> m;
表达式
m[k] = v;
检查键 k 是否已经在 map 种了,如果没有,它就被加入,并以 v 作为相应的值。如果 k 已经在映射表中了,则与之关联的值被更新为 v。
- 执行添加操作时
insert
比operator[]
效率高,因为operator[]
相当于构造一个内容类,然后赋值。 - 更新时使用
operator[]
, 效率高,因为不需要构造和析构 pair 对象
25. 熟悉非标准的哈希容器
SGI 版本:
template<typename T,
typename HashFunction = hash<T>,
typename CompareFunction = equal_to<T>,
typename Allocator = allocator<T>>
class hash_set;
标准关联容器的默认比较函数是 less,而 SGI 的设计使用了 equal_to。
Dinkumware 版本:
template<typename T, typename CompareFunction>
class hash_compare;
template<typename T,
typename HashingInfo = hash_compare<T, less<T>>,
typename allocator = allocator<T>>
class hash_set;
其中,HashingInfo 类型中存储了容器的哈希函数和比较函数,同时还有一些枚举函数,用于控制哈希表中桶的最小数目,容器中元素与桶个数的最大允许比率。当超过这个比率时,哈希表的桶的数目将自动增加,表中的某些元素要被重新做哈希计算。(SGI版本中也提供了一些类似的控制功能)
HashingInfo 看起来有点像这样:
typename<typename T, typename CompareFunction = less<T>>
class hash_compare
{
public:
enum
{
bucket_size = 4; //元素个数和桶个数的最大比率
min_buckets = 8; //最小的桶数目
};
size_t operator()(const T&) const; //哈希函数
bool operator()(const T&, const T&) const; //比较函数
... //其他细节,包括CompareFunction的使用
}
SGI 把实现表放在一个单向链表中,而 Dinkumware的实现使用了双向链表。内存角度来说 SGI 的设计更省一些。
四、 迭代器
26. iterator 优先于 const_iterator、reverse_iterator 以及 const_reverse_iterator
对于容器类 container<T>, iterator
类型相当于 T*,而 const_iterator
则相当于 const T*
而 reverse_iterator
与 const_reverse_iterator
与上面对应相同,不同之处在于从容器尾部反向遍历到容器头部
- 表中容器中的
insert
和erase
函数总是支持iterator
的,其他的不一定 - 关系,如下图所示,想要隐式的将一个
const_iterator
转换成iterator
是不可能的
27. 使用 distance 和 advance 将容器的 const_iterator 转换成 iterator
distance
用以取得两个迭代器之间的距离advance
用于将一个迭代器移动指定的距离
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
...
Iter i(d.begin()); //使i指向d的起始位置
advance(i, distance<ConstIter>(i, ci)); //移动i,使它指向ci所指的位置
28. 正确理解由 reverse_iterator 的 base() 成员函数所产生的 iterator 的用法
- 调用
reverse_iterator
的base()
成员函数可以得到 “与之对应的”iterator
- 如果在
reverse_iterator
指定位置上插入一个元素,则只需要通过base()
得到iterator
处插入即可 - 如果在
reverse_iterator
指定位置上删除一个元素,则需要在base()
前面的位置上执行删除操作
注意:
在有些实现中terator
(和 const_iterator
)是以内置指针的方式来实现的,所以,ri.base()
返回的结果是一个指针,C 和 C++ 都规定了函数返回的指针不应该被修改,所以
vector<int> v;
...
vector<int>::reverse_iterator ri = find(v.rbegin(), v.rend(),3);
v.erase(--ri.base));
无法通过编译,应该先递增 reverse_iterator
,然后再调用 base()
函数即可
...
v.erase((++ri).base());
29. 对于逐个字符的输入请考虑使用 istreambuf_iterator
istream_iterator
使用operator>>
函数来完成读操作,默认情况下会跳过空白字符istream_iterator
内部使用operator>>
函数实际上执行了格式化输入,每调用一次operator>>
都要执行许多附加操作istreambuf_iterator
与istream_iterator
用法大致相同,但istream_iterator<char>
对象使用operator>>
从输入流中读取单个字符,而istreambuf_iterator<char>
则直接从流的缓冲区中读取下一个字符,效率很高
ifstream inputFile("interestingData.txt");
string fileData((istreamBuf_iterator<char>(inputFile)), istreambuf_iterator<char>());
如果坚持使用 istream_iterator
读取,则需要清除输入流的 skipws
标志
ifstream inputFile("interestingData.txt");
inputFile.unsetf(ios::skipws); //禁止忽略 iputFile 中的空格
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());
五、 算法
30. 确保目标区间足够大
- 无论何时,如果所使用的的算法需要制定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大
- 要在算法执行过程中增大目标区间,需要使用插入型迭代器,比如
ostream_iterator
或者由back_inserter
、front_inserter
和inserter
返回的迭代器。
例:
int transmogrify(int x); //该函数根据x生成一个新的值
vector<int> values;
...
vector<int> results;
results.reserve(values.size() + results.size()); //提高插入性能,避免重新分配内存
transform(values.begin(),values.end(),back_inserter(results),transmogrify); //对每个values调用transmogrify,将结果插入到results尾部
31. 了解各种与排序有关的选择
- 如果需要对
vector
、string
、deque
或者数组中的元素执行一次完全排序,那么可以使用sort
或者stable_sort
。 - 如果有一个
vector
、string
、deque
或数组,并且只需要对等价性最前面的 n 个元素进行排序,那么可以使用partial_sort
。 - 如果有一个
vector
、string
、deque
或数组,并且需要找到第 n 个位置上的元素,或者,需要好到等价性最前面的 n 个元素,但又不必对这 n 个元素进行排序,那么,nth_element
正是你所需要的函数。 - 如果需要将一个标准序列容器中的元素按照是否满足某个特定的条件区分开来,那么,
partition
和stable_partition
可能正是你所需要的。 - 如果你的数据在一个
list
中,那么你仍然可以直接调用partition
和stable_partition
算法;你可以用list:sort
来替代sort
和stable_sort
算法。但是,如果你需要获得partial_sort
或者nth_element
算法的效果,那么需要使用以下间接途径:- 将
list
中的元素拷贝到一个提供随机访问迭代器的容器中,然后对该容器执行你所期望的算法; - 先创建一个
list:iterator
的容器,在对该容器执行相应的算法,然后通过迭代器访问list
的元素; - 利用一个包含迭代器的有序容器中的信息,通过反复地调用
splice
成员函数,将list
中的元素调整到期望的位置目标。
- 将
- 如果希望容器中的元素始终保持特定的顺序,可以考虑标准的非STL容器
priority_queue
,它总是保持其元素的顺序关系。
资源消耗升序:
partition
stable_partition
nth_element
partial_sort
sort
stable_sort
32. 如果确实需要删除元素,则需要在 remove 这一类算法之后调用 erase
remove
不是真正意义上的删除元素,因为它做不到,remove
只是将“不用被删除”的元素移动到了区间的前部(保持原顺序),返回的一个迭代器指向最后一个“不用被删除”元素之后的元素。- 只有容器的成员函数才能删除容器中的元素。
vector<int> v; ... v.erase(remove(v.begin(),v.end(),99,v.end()); //删除所有值等于99的元素
- 只有
list
的成员函数remove
确实删除了容器中的元素list<int> li; ... li.remove(99);
33. 对包含指针的容器使用 remove 这一类算法时要持特别小心
- 智能指针可以直接使用
erase
-remove
习惯用法 - 如果是旧指针,需要在
remove
算法之前手工删除指针并将它们置为空
本条主要是要避免资源泄露,参考 9
34. 了解哪些算法要求使用排序的区间作为参数
- 用于查找的算法
binary_search
、lower_bound
、upper_bound
和equal_range
要求排序区间,因为他们用二分法查找数据 - 线性时间效率的结合操作
set_union
、set_intersection
、set_difference
和set_symmetric_difference
,如果是排序区间,那么它将会有更好的性能 merge
和inplace_merge
实际上实现了合并和排序的联合操作:它们读入两个排序区间,然后合并成一个新的排序区间,其中包含了原来两个区间中的所有元素。如果是排序区间,它们才能在线性时间内完成includes
算法用来判断一个区间中的所有对象是否都在另一个区间中,如果源区间是排序的,那么它承诺线性时间的效率unique
和unique_copy
与上述讨论的算法有所不同,对于未排序的区间也有很好的行为,但他们的功能是删除每一组连续相等的元素,仅保留其中的第一个。所以,有的情况需要先排序。
35. 通过 mismatch 或 lexicographical_compare实现简单的忽略大小写的字符串比较
lexicographical_compare
是strcmp
的泛化版本,可以与任何类型的值的区间一起工作,可以接受一个判别式,由该判别式来决定两个值是否满足一个用户自定义的准则。bool ciCharLess(char c1,char c2); bool ciStringCompare(const string &s1, const string &s2) { return lexicographical_compare(s1.begin(),s1.end(),s2.begin(),s2.end(),ciCharLess); }
mismatch
用于标识出两个区间中第一个对应值不相同的位置int ciStringCompare(const string &s1, const string &s2); PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));
36. 理解 copy_if 算法的正确实现
template<typename InputIterator, typename OutputIterator, typename Predicate>
OutPutIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p)
{
while(begin != end)
{
if(p(*begin))
{
*destBegin++ = *begin;
++begin;
}
}
return destBegin;
}
37. 使用 accumulate 或 for_each 进行区间统计
accumlate
可以按照某种自定义的方式对区间进行统计处理
list<double> ld;
...
double sum = accumlate(ld.begin(), ld.end(), 0.0);//累加求和,第三个元素为初始值,其类型会影响accumulate累加的变量
比如使用accumlate
计算容器类字符串的长度总和
string::size_type stringLengthSum(string::size_type sumSoFar, const string& s)
{
return sumSoFar + s.size();
}
...
set<string> ss;
...
string::size_type lengthSum = accumulate(ss.begin(), ss.end(), static_cast<string::size_type>(0),stringLenghSum);
但有时for_each
更加方便,比如我们要区分开累加和某样计算的时候,比如求均值
六、 函数子、函数子类、函数及其他
38. 遵循按值传递的原则来设计函数子类
- 无论是C还是C++,都不允许将一个函数作为参数传递给另一个函数,必须传递函数指针。
void qsort(void* base, size_t nmemb, size_t size, int(*cmpfcn)(const void*, const void*));
- 函数对象往往按值传递和返回,所以:
- 函数对象必须尽可能的小,否则导致拷贝开销昂贵
- 函数对象必须是单态的,他们不得使用虚函数,参考第3条
- 如果需要可以,创建一个小巧的、单态的类,包含一个指针,指向另一个实现类,将数据和虚函数都放在实现类中
39. 确保判别式是“纯函数”
- 纯函数是指返回值仅仅依赖于其他参数的函数。例如,假设f是一个纯函数,x和y是两个对象,那么只有当x或者y的值发生变化的时候,f(x,y)的返回值才可能发生变化;
- 一个判别式是一个返回值为bool类型的函数。
40. 若一个类的函数子,则应使它可配接
- STL的四个标准函数配接器(
not1
、not2
、bind1st
和bind2nd
)要求一些特殊的定义,非标准的、与STL兼容的配接器也是如此 - 可配接的函数对象能够与其他STL组件更为默契地协同工作,能够应用于更多的上下文环境中,因此应当尽可能地使你编写的函数对象可以配接
- 提供这种定义最简单的方法是从特定的基类继承:
- 如果子函数类的
operator()
只有一个实参,那么它应该从std::unary_function
继承; - 如果函数子类的
operator()
有两个实参,那么它应该从std::binary_function
继承 ;
- 如果子函数类的
template<typename T>
class MeetsThreshold: public std::unary_function<Widget,bool>
{
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator()(const Widget&) const;
...
};
struct WidgetNameCompare: public std::binary_function<Widget,Widget,bool>
{
bool operator()(const Widget& lhs, const Widget& rhs)const;
};
这样,我们才可以:
list<Widght> widgets;
...
//找到最后一个不符合阈值10的Widget
list<Widget>::reverse_iterator il = find_if(widgets.rbegin(), widgets.rend(), not1(MeetsThreshold<int>(10)));
Widget w(...);
//找到按 WidgetNameCompare 定义的规则排序时,在w之前的第一个Widget对象
list<Widget>::iterator i2 = find_if(widgets.begin(), widgets.end(), bind2nd(WidgetNameCompare(),w));
41. 理解 ptr_fun、mem_fun 和 mem_fun_ref 的来由
void test(Widget& w);
class Widget
{
public:
...
void test();
...
};
vector<Widget> vw;
...
for_each(vw.begin(), vw.end(), test); //#1 可通过
...
for_each(vw.begin(), vw.end(), &Widget::test); //#2 不能通过
...
list<Widget*> lpw;
for_each(vw.begin(), vw.end(), &Widget::test); //#3 不能通过
mem_fun
将 #3 调整为 #1mem_fun_ref
将 #2 调整为 #1
for_each(vw.begin(), vw.end(), mem_fun(Widget::test)); //#3改,可通过
- #1 可以通过是因为传入了一个真正的函数,没有必要调整语法形式,但加上
ptr_fun
也不会有性能或其它什么影响
for_each(vw.begin(),vw.end(),ptr_fun(test));
- 他们三个都是为了做可配接调整的
42. 确保 less<T> 与 operator< 具有相同的语义
应该尽量避免修改 less
的行为,这样做很可能误导其它程序员,为了达到某些目的,你可以创建一个特殊的函数子类,但名字不是less
七、 在程序中使用STL
因为这一章内容经过前面的铺垫变得浅显易懂,所以标题就可以总结一切
43. 算法调用优先于手写循环
44. 容器的成员函数优先于同名算法
45. 正确区分 count、find、binary_search、lower_bound、upper_bound 和 equal_range
46. 考虑使用函数对象而不是函数作为STL算法的参数
class A{
public:
int operator() (int x)
{
return x;
}
};
A a;
a(1);
a就是函数对象;
a(1) 相当于利用重载符()
47. 避免产生“直写型”(write-only)的代码
也就是优雅、美观的写代码