|
基于接口和实现分离的原则。了解stl的接口和stl的实现。
1. 慎重选择容器类型
STL中有迭代器(iterator)、算法(algorithm)和函数对象(function object)。但是对于大多数C++程序员来说,最值得注意的还是容器。容器比数组功能更强大、更灵活。它们可以动态增长(和缩减),可以自己管理内存,可以记住自己包含了多少对象。它们限定了自己所支持的操作的复杂性。
序列容器:vector、string、deque、list、forward_list(C++11)、array(C++11)。
关联容器:set、multiset、map、multimap、
哈希容器:hash_set、hash_multiset、hash_map、hash_multimap、 unordered_set(C++11)、unordered_multiset(C++11)、unordered_map(C++11)、unordered_multimap(C++11)。
标准的非STL容器,包括:bitset(include <bitset>)、valarray(include <valarray>)。其它STL容器:stack(include <stack>)、queue(include <queue>)和priority_queue((include <queue>))。
vector<char>可以作为string的替代。vector作为标准关联容器的替代。有时vector在运行时间和空间上都要优于标准关联容器。
vector是默认应使用的序列类型,当需要频繁地在序列中间做插入和删除操作时,应使用list;当大部分插入和删除操作发生在序列的头部和尾部时, deque是应考虑的数据结构。 STL容器的一种分类方法:连续内存容器(contiguous memory container)和基于节点的容器(node-based container)。
?连续内存容器:vector、string、deque
基于节点的容器:list、关联容器、哈希容器
选择容器时最重要的一些问题:
(1). 你是否需要在容器的任意位置插入新元素?如果需要,就选择序列容器;关联容器是不行的。
(2). 你是否关心容器中的元素是如何排序的?如果不关心,则哈希容器是一个可行的选择方案;否则,你要避免哈希容器。
(3). 你选择的容器必须是标准C++的一部分吗?如果必须是,就排除了slist和rope。
(4). 你需要哪种类型的迭代器?如果它们必须是随机访问迭代器,则对容器的选择就被限定为vector、deque和string。或许你也可以考虑rope。如果要求使用双向迭代器,那么你必须避免slist以及哈希容器的一个常见实现。
(5). 当发生元素的插入或删除操作时,避免移动容器中原来的元素是否很重要?如果是,就要避免连续内存的容器。
(6). 容器中数据的布局是否需要和C兼容?如果需要兼容,就只能选择vector。
(7). 元素的查找速度是否是关键的考虑因素?如果是,就要考虑哈希容器、排序的vector和标准关联容器----或许这就是优先顺序。
(8). 如果容器内部使用了引用计数技术(reference counting),你是否介意?如果是,就要避免使用string,因为许多string的实现都使用了引用计数。rope也需要避免,因为权威的rope实现是基于引用计数的。当然,你需要某种表示字符串的方法,这时你可以考虑vector<char>。
(9). 对插入和删除操作,你需要事务语义(transactional semantics)吗?也就是说,在插入和删除操作失败时,你需要回滚的能力吗?如果需要,你就要使用基于节点的容器。如果对多个元素的插入操作(即针对一个区间的形式)需要事务语义,则你需要选择list,因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。对那些希望编写异常安全(exception-safe)代码的程序员,事务语义显得尤为重要。(使用连续内存的容器也可以获得事务语义,但是要付出性能上的代价,而且代码也显得不那么直截了当。)
(10). 你需要使迭代器、指针和引用变为无效的次数最少吗?如果是这样,就要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变得无效(除非它们指向了一个你正在删除的元素)。而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变得无效。
(11). 如果序列容器的迭代器是随机访问类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?这是非常特殊的情形,但如果你面对的情形正是如此,则deque是你所希望的容器。(当插入操作仅在容器末尾发生时,deque的迭代器有可能会变为无效。deque是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器。)
2.?不要试图编写独立于容器类型的代码
STL是以泛化(generalization)原则为基础的:数组被泛化为”以其包含的对象的类型为参数”的容器,函数被泛化为”以其使用的迭代器的类型为参数”的算法,指针被泛化为”以其指向的对象的类型为参数”的迭代器。
容器类型被泛化为序列和关联容器,类似的容器被赋予相似的功能。标准的连续内存容器提供了随机访问迭代器,而标准的基于节点的容器提供了双向迭代器。序列容器支持push_front和/或push _back操作,而关联容器则不然。关联容器提供了对数时间的lower_bound、upper_bound和equal_range成员函数,但序列容器却没有提供。
3.?确保容器中的对象拷贝正确而高效
当(通过如insert或push_back之类的操作)向容器中加入对象时,存入容器的是你所指定的对象的拷贝。当(通过如front或back之类的操作)从容器中取出一个对象时,你所得到的是容器中所保存的对象的拷贝。进去的是拷贝,出来的也是拷贝(copy in, copy out)。
在存在继承关系的情况下,拷贝动作会导致剥离(slicing)。也就是说,如果你创建了一个存放基类对象的容器,却向其中插入派生类的对象,那么在派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它所特有的部分(即派生类中的信息)将会丢失。”剥离”问题意味着向基类对象的容器中插入派生类对象几乎总是错误的。使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象。
class Widget {};
class SpecialWidget : public Widget {};
int test_item_3()
{
std::vector<Widget> vw;
SpecialWidget sw;
vw.push_back(sw); // sw作为基类对象被拷贝进vw中,它的派生类特有部分在拷贝时被丢掉了
return 0;
}
4.?调用empty而不是检查size()是否为0
empty对所有的标准容器都是常数时间操作,而对一些list实现,size耗费线性时间。


5.?区间成员函数优先于与之对应的单元素成员函数
区间成员函数是指这样的一类成员函数,它们像STL算法一样,使用两个迭代器参数来确定该成员操作所执行的区间。如果不使用区间成员函数就得写一个显示的循环。
优先选择区间成员函数而不是其对应的单元素成员函数有三条充分的理由:
1.区间成员函数写起来更容易,
2.更能清楚地表达你的意图,
3.而且它们表现出了更高的效率。具体表现为在减少拷贝构造函数的调用次数。因为每次insert、copy函数就对内存移动调用拷贝构造、赋值构造函数。


//区间成员函数优先于与之对应的单元素成员函数
class Widget5 {};
int test_item_5()
{
std::vector<Widget5> v1, v2;
v1.assign(v2.begin() + v2.size() / 2, v2.end()); // 推荐
v1.clear();
for (std::vector<Widget5>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end(); ++ci) // 不推荐
v1.push_back(*ci);
v1.clear();
std::copy(v2.begin() + v2.size() / 2, v2.end(), std::back_inserter(v1)); // 效率不如assign
v1.clear();
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end()); // 对copy的调用可以被替换为利用区间的insert版本
const int numValues = 100;
int data[numValues];
std::vector<int> v;
v.insert(v.begin(), data, data + numValues); // 推荐,使用区间成员函数insert
std::vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; ++i) {
insertLoc = v.insert(insertLoc, data[i]); // 不推荐,使用单元素成员函数
++insertLoc;
}
return 0;
}
? ? std 的 copy()函数
? ? copy(_InIt _First, _InIt _Last, _OutIt _Dest)? ? first、last表示源? ? dest表示目标
6.?当心C++编译器最烦人的分析机制

应该是这样 :
?

// 注意:围绕参数名的括号(比如对f2中d)与独立的括号的区别:围绕参数名的括号被忽略,而独立的括号则表明参数
// 列表的存在:它们说明存在一个函数指针参数
int f1(double d); // 声明了一个带double参数并返回int的函数
int f2(double(d)); // 同上,d两边的括号被忽略,可以给参数名加上圆括号
int f3(double); // 同上,参数名被忽略
int g1(double(*pf)()); // 参数是一个指向不带任何参数的函数的指针,该函数返回double值;g1以指向函数的指针为参数
int g2(double pf()); // 同上,pf为隐式指针
int g3(double()); // 同上,省去参数名
int test_item_6()
{
// 把一个存有整数(int)的文件ints.dat拷贝到一个list中
std::ifstream dataFile("ints.dat");
std::list<int> data1(std::istream_iterator<int>(dataFile), std::istream_iterator<int>()); // 小心,结果不会是你所想象的那样
std::list<int> data2((std::istream_iterator<int>(dataFile)), std::istream_iterator<int>()); // 正确,注意list构造函数的第一个参数两边的括号
std::istream_iterator<int> dataBegin(dataFile);
std::istream_iterator<int> dataEnd;
std::list<int> data3(dataBegin, dataEnd); // 正确
//输出 // 把一个list拷贝到一个存有整数(int)的文件outs.dat
std::ofstream outFile("outs.dat");
ostream_iterator<int> outite(outFile, " ");
copy(data3.begin(), data3.end(), outite);
return 0;
}
对于 迭代器配接器(iterator adapters)的介绍见:STL源码剖析之配接器。
STL源码剖析之配接器_小飞侠hello的博客-CSDN博客
7.?如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉

解决办法:使用智能指针,如shared_ptr 。具体介绍见:c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_小飞侠hello的博客-CSDN博客_智能指针取内容
class Widget7 {};
int test_item_7()
{
const int num = 5;
using SPW = std::shared_ptr<Widget7>; // SPW"指向Widget7的shared_ptr"
std::vector<SPW> vwp3;
for (int i = 0; i < num; ++i) {
vwp3.push_back(SPW(new Widget7)); // 从Widget7创建SPW,然后对它进行一次push_back使用vwp3,这里不会有Widget7泄露,即使有异常被抛出
}
return 0;
}
?8. 切勿创建包含auto_ptr的容器对象
auto_ptr的容器(简称COAP)是被禁止的。当你拷贝一个auto_ptr时,它所指向的对象的所有权被移交到拷入的auto_ptr上,而它自身被置为NULL。如果你的目标是包含智能指针的容器,这并不意味着你要倒霉,包含智能指针的容器是没有问题的。
9.?慎重选择删除元素的方法
(1).要删除容器中有特定值的所有对象:如果容器是vector, string或deque,则使用erase-remove习惯用法;如果容器是list,则使用list::remove;如果容器是一个标准关联容器,则使用它的erase成员函数。
int test_item_9()
{
// 删除c中所有值为1963的元素
std::vector<int> c1{1,1963,4,233,555,1963,2000};
c1.erase(std::remove(c1.begin(), c1.end(), 1963), c1.end()); // 当c1是vector, string或deque时,erase-remove习惯用法是删除特定值的元素的最好办法
std::list<int> c2{ 1,1963,4,233,555,1963,2000 };
c2.remove(1963); // 当c2是list时,remove成员函数是删除特定值的元素的最好办法
std::set<int> c3{ 1,1963,4,233,555,1963,2000 };
c3.erase(1963); // 当c3是标准关联容器时,erase成员函数是删除特定值元素的最好办法
return 0;
}
(2).要删除容器中满足特定判别式(条件)的所有对象:如果容器是vector, string或deque,则使用erase-remove_if习惯用法;如果容器是list,则使用list::remove_if;如果容器是一个标准关联容器,则使用remove_copy_if和swap,或者写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对它进行后缀递增。
bool badValue(int value) {
if (value < 100)
return true;
else
return false;
}
int test_item_9()
{
// 删除c中所有值为1963的元素
std::vector<int> c1{1,1963,4,233,555,1963,2000};
std::list<int> c2{ 1,1963,4,233,555,1963,2000 };
std::set<int> c3{ 1,1963,4,233,555,1963,2000 };
c1.erase(std::remove_if(c1.begin(), c1.end(), bind2nd(less<int>(), 100)), c1.end());
c2.remove_if(bind2nd(less<int>(), 100));
for (std::set<int>::iterator it = c3.begin();it != c3.end();)
{
if (badValue(*it))
{
c3.erase(it++);
}
else
{
++it;
}
}
return 0;
}
(3).要在循环内做某些(除了删除对象之外的)操作:如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次调用erase时,要用它的返回值更新迭代器;如果容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对迭代器做后缀递增.
// 每次元素被删除时,都向一个日志(log)文件中写一条信息
std::ofstream logFile;
for (std::set<int>::iterator i = c3.begin(); i != c3.end();) {
if (badValue(*i)) {
logFile << "Erasing " << *i << '\n'; // 写日志文件
c3.erase(i++); // 对坏值,把当前的i传给erase,递增i是副作用
}
else ++i; // 对好值,则简单第递增i
}
for (std::vector<int>::iterator i = c1.begin(); i != c1.end();) {
if (badValue(*i)) {
logFile << "Erasing " << *i << '\n';
i = c1.erase(i); // 把erase的返回值赋给i,使i的值保持有效
}
else ++i;
}
个人理解:在序列容器(特别是连续内存容器如vector、deque)不应该在循环遍历中,多次调用erase函数删除元素。因为删除元素后,迭代器会重新置值,不能再使用了。
10. 了解分配子(allocator)的约定和限制
编写自定义的分配子,需要注意:

void* mallocShared(size_t bytesNeeded)
{
return malloc(bytesNeeded);
}
void freeShared(void* ptr)
{
free(ptr);
}
template<typename T>
class SharedMemoryAllocator { // 把STL容器的内容放到共享内存(即由mallocShared生成的)中去
public:
typedef T* pointer; // pointer是个类型定义,它实际上总是T*
typedef size_t size_type; // 通常情况下,size_type是size_t的一个类型定义
typedef T value_type;
pointer allocate(size_type numObjects, const void* localityHint = 0)
{
return static_cast<pointer>(mallocShared(numObjects * sizeof(T)));
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
freeShared(ptrToMemory);
}
template<typename U>
struct rebind {
typedef std::allocator<U> other;
};
};
int test_item_11()
{
typedef std::vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;
// v所分配的用来容纳其元素的内存将来自共享内存
// 而v自己----包括它所有的数据成员----几乎肯定不会位于共享内存中,v只是普通的基于栈(stack)的对象,所以,像所
// 有基于栈的对象一样,它将会被运行时系统放在任意可能的位置上。这个位置几乎肯定不是共享内存
SharedDoubleVec v; // 创建一个vector,其元素位于共享内存中
// 为了把v的内容和v自身都放到共享内存中,需要这样做
int isize = sizeof(SharedDoubleVec);
void* pVectorMemory = mallocShared(sizeof(SharedDoubleVec)); // 为SharedDoubleVec对象分配足够的内存
SharedDoubleVec* pv = new (pVectorMemory)SharedDoubleVec; // 使用"placement new"在内存中创建一个SharedDoubleVec对象
// ... // 使用对象(通过pv)
pv->push_back(1.2);
pv->~SharedDoubleVec(); // 析构共享内存中的对象
freeShared(pVectorMemory); // 释放最初分配的那一块共享内存
return 0;
}
12.?切勿对STL容器的线程安全性有不切实际的依赖
当涉及到STL容器和线程安全性时,你可以指望一个STL库允许多个线程同时读一个容器,以及多个线程对不同的容器做写入操作。你不能指望STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。

?和普通的内存一样,可以在多线程下加上互斥量。
13. vector和string优先于动态分配的数组
因为不用考虑像new创建内存需要调用delete释放内存,不然内存泄漏。
许多string实现在背后使用了引用计数技术,这种策略可以消除不必要的内存分配和不必要的字符拷贝,从而可以提供很多应用程序的效率。如果你在多线程环境下使用了引用计数的string,那么注意一下因支持线程安全而导致的性能问题。vector的实现不允许使用引用计数,所以不会发生隐藏的多线程性能问题。所以在多线程下,可以考虑用vector<char> 替代string.
string类和vector<char>的区别
string类是一个保存字符的动态数组,由于其中有一个接口c_str,转化成c语言的字符串,要以\0结尾,所以string类最后会有一个\0.
vector<T>是一个保存T类型的动态数组,vector<char>也是保存字符的动态数组,但是,不会以\0结尾,不保存\0.
所以在vector类里面就是空的或string空类。vec[0]会崩溃,str[0] 不会崩溃。
string staa;
cout << staa[0] << endl;
vector<int> vecii;
cout << vecii[0] << endl;//崩溃
14.?使用reserve来避免不必要的重新分配
对于vector和string,增长过程是这样来实现的:每当需要更多空间时,就调用与realloc类似的操作。这一类似于realloc的操作分为四部分:(1).分配一块大小为当前容量的某个倍数的新内存。在大多数实现中,vector和string的容量每次以2的倍数增长,即,每当容器需要扩张时,它们的容量即加倍。(2).把容器的所有元素从旧的内存拷贝到新的内存中。(3).析构掉就内存中的对象。(4).释放旧内存。

?reserve要求参数n必须大于当前的大小(size). resize的参数n无任何要求。
reserve成员函数能使你把重新分配的次数减少到最低限度,从而避免了重新分配和指针/迭代器/引用失效带来的开销。避免重新分配的关键在于,尽早地使用reserve,把容器的容量设为足够大的值,最好是在容器刚被构造出来之后就使用reserve。
通常有两种方式来使用reserve以避免不必要的重新分配。第一种方式是,若能确切知道或大致预计容器中最终会有多少元素,则此时可以使用reserve。第二种方式是,先预留足够大的空间(根据你的需要而定),然后,当把所有数据都加入以后,再去除多余的容量。
另外:连续内存的容器如string、vector插入元素的操作会是迭代器失效,所以插入元素后,就不应该再使用原来的迭代器了。
int test_item_14()
{
std::vector<int> v;
v.reserve(1000); // 如果不使用reserve,下面的循环在进行过程中将导致2到10次重新分配;加上reserve,则在循环过程中,将不会再发生重新分配
for (int i = 1; i <= 1000; ++i) v.push_back(i);
return 0;
}
15.注意string实现的多样性
一般情况下,string 的大小是char *指针(大小为7)的7倍。所以string的大小sizeof(string)为28字节。
int isize0 = sizeof(char*); //4
int isize1 = sizeof(string); //28
string straaa = "122";
int isize11 = sizeof(straaa); //28
char *psq =const_cast<char*>(straaa.c_str() );
每个string包括以下信息:
1.大小(size)? 1字节
2.容量(capaacity)? 1字节
3.字符串的值,通过指向指针获取字符串。 1字节。可通过c_str()函数取到字符串地址。如
4.它的分配子(allocator)的一份拷贝。默认4字节。
5.对值得引用计数。? ?是在指向字符串的指针上。
总结:
(1).string的值可能会被引用计数,也可能不会。很多实现在默认情况下会使用引用计数,但它们通常提供了关闭默认选择的方法,往往是通过预处理宏来做到这一点。
(2).string对象大小的范围可以是一个char*指针大小的1倍到7倍。
(3).创建一个新的字符串值可能需要零次、一次或两次动态分配内存。
(4).string对象可能共享,也可能不共享其大小和容量信息。
(5).string可能支持,也可能不支持针对单个对象的分配子。
(6).不同的实现对字符内存的最小分配单位有不同的策略。
16.?了解如何把vector和string数据传给旧的API
C++标准要求vector中的元素存储在连续的内存中,就像数组一样。
string中的数据不一定存储在连续的内存中,而且string的内部表示不一定是以空字符结尾的。
c_str()函数返回一个指向字符串的值的指针。
string straaa = "122";
char *psq =const_cast<char*>(straaa.c_str() );
17.?使用”swap技巧”除去多余的容量
对vector或string进行shrink-to-fit(压缩到合适大小)操作时,考虑”swap”技巧。C++11中增加了shrink_to_fit成员函数。
?std::vector<int>(contestants).swap(contestants);
// vector<int>(contestants)创建一个临时矢量,vector的拷贝构造函数只为所拷贝的元素分配所需要的内存。在做swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。在swap发生后,原先指向某容器中元素的迭代器、指针和引用依然有效,并指向同样的元素----但是,这些元素已经在另一个容器中了。
swap技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最下值。
int test_item_17()
{
// 从contestants矢量中除去多余的容量
std::vector<int> contestants{1,3,5,6,7,8,9,0,10,4,3,10};
contestants.erase(std::remove_if(contestants.begin(), contestants.end(), bind2nd(less<int>(), 8)), contestants.end());
// ... // 让contestants变大,然后删除它的大部分元素
// vector<int>(contestants)创建一个临时矢量,vector的拷贝构造函数只为所拷贝的元素分配所需要的内存
std::vector<int>(contestants).swap(contestants);
contestants.shrink_to_fit(); // C++11
std::string s("12222223fdsddddddddddddddd");
s.erase(s.begin() + 6,s.end());
// ... // 让s变大,然后删除它的大部分字符
std::string(s).swap(s); //
s.shrink_to_fit(); // C++11
std::vector<int>().swap(contestants); // 清除contestants并把它的容量变为最小
std::string().swap(s); // 清除s并把它的容量变为最小
return 0;
}
18.?避免使用vector<bool>
作为一个STL容器,vector<bool>只有两点不对。
首先,它不是一个STL容器;其次,它并不存储bool。除此以外,一切正常。
储存在”vector”中的每个”bool”仅占一个二进制位,一个8位的字节可容纳8个”bool”。在内部,vector<bool>使用了与位域(bit field)一样的思想,来表示它所存储的那些bool,实际上它只是假装存储了这些bool。
int test_item_18()
{
std::vector<bool> v;
// error: cannot convert 'std::vector<bool>::reference* {aka std::_Bit_reference}' to 'bool*' in initialization
bool* pb = &v[0]; // 不能被编译,原因:vector<bool>是一个假的容器,它并不真的储存bool,相反,为了节省空间,它储存的是bool的紧凑表示
return 0;
}
当你需要vector<bool>时,标准库提供了两种选择,可以满足绝大多数情况下的需求。第一种是deque<bool>。deque几乎提供了vector所提供的一切(没有reserve和capacity),但deque<bool>是一个STL容器,而且它确实存储bool。当然deque中元素的内存不是连续的,所以你不能把deque<bool>中的数据传递给一个期望bool数组的C API。
deque<bool> dqbool{true,false};
bool *pb = &(dqbool[0]);
第二种可以替代vector<bool>的选择是bitset。bitset不是STL容器,但它是标准C++库的一部分。与STL容器不同的是,它的大小(即元素的个数)在编译时就确定了,所以它不支持插入和删除元素。
#include <bitset>
bitset<32> bit(0);
19.?理解相等(equality)和等价(equivalence)的区别
相等的概念是基于operator==的。等价关系是以”在已排序的区间中对象值的相对顺序”为基础的。标准关联容器是基于等价而不是相等。
默认情况下,该比较函数应该是equal_to,但equal_to从来没有被用作STL的默认比较函数。当STL中需要相等判断时,一般的惯例是直接调用operator==)。
??
|