C++容器及选用总结

原创
小哥 3年前 (2023-05-24) 阅读数 6 #大杂烩

转自:http://www.cnblogs.com/answeryi/archive/2011/12/16/2289811.html

目录

====================================================

第一章 容器

第二章 Vector和string

第三章 关联容器

第四章 迭代器

第五章 算法

第六章 函数

第七章 在程序中使用STL

====================================================

第1章 容器

第1文章:仔细选择容器类型。

标准STL序列容器:vector、string、deque和list。

标准STL关联的容器:set、multiset、map和multimap。

非标准序列容器slist和rope。slist它是一个单链表,rope从本质上讲,它是一个“沉重的”string。

非标准关联容器hash_set、hase_multiset、hash_map和hash_multimap。

vector 作为string的替代。(见第13条)

vector作为标准关联容器的替代方案。(见第23条)

几种非标非STL容器,包括数组bitset、valarray、stack、queue和priority_queue。

你关心容器中的元素是如何排序的吗?如果您不在乎,请选择哈希容器.

容器中数据的布局是否需要与C相容?如果需要兼容性,则只能选择vector。(第16条)

元素的搜索速度是否是一个关键考虑因素?如果是这样,请考虑哈希容器,排序vector将容器与标准相关联 - 也许这是优先级顺序。

插入和删除操作是否需要事务语义?如果是这样,您只能选择list。因为在标准容器中,只有list多个元素的插入操作提供事务语义。

deque它是唯一的,迭代器可能会变得无效(插入操作仅在容器末尾发生,deque迭代器可能变得无效,而指针和数据引用仍然是有效的标准STL容器。

第2文章:不要尝试编写独立于容器类型的代码。

如果要编写适用于大多数容器的代码,则只能使用其函数的交集。不同的容器是不同的,它们有非常明显的优缺点。它们不是为可互换使用而设计的。

第3栏:确保正确有效地复制容器中的对象。

copy in,copy out,是STL工作方法的整体设计理念是避免不必要的复制。

第4条:调用empty而不是检查size()是否为0。

原因很简单:empty对于所有标准容器,它是恒定时间操作,而对于某些容器list的实现,size消耗线性时间。

第5文章:间隔成员函数优先于其相应的单元素成员函数。

区间成员函数更容易编写和更清楚地表达您的意图,并且它们表现出更高的效率。

第6条:当心C++编译器最烦人的分析机制。

在括号中包含形状是合法的,将整个参数(包括数据类型和参数名称)括在括号中是非法的。

第7条形图:如果容器包含new操作创建的指针,记得在销毁容器对象之前设置指针delete。

STL它非常智能,但还不至于智能到知道是否删除它所包含的指针所指向的对象。为避免资源泄漏,必须在销毁容器中的每个指针之前手动删除容器中的每个指针,或使用 引用计数形式的智能指针 (比如Boost的sharedprt)替换指针。

第8条形图:不创建包含auto_ptr的容器对象。

拷贝一个auto_ptr这意味着改变其值。例如,对于auto_ptr的vector调用sort排序,结果是vector的几个元素设置为NULL并且相应的元素已被删除。

第9文章:仔细选择删除元素的方法。

删除容器中具有指定值的所有对象:

如果容器是vector、string或deque,则使用erase-remove惯用法。

SeqContainer c;

c.erase(remove(c.begin(),c.end(),1963),c.end());

如果容器是list,则使用list::remove。

如果容器是标准关联容器,请使用它的erase成员函数。

要删除容器中满足特定条件的所有对象,请执行以下操作:

如果容器是vector、string或deque,则使用erase-remove_if惯用法。

如果容器是list,则使用list::remove_if。

如果容器是标准关联容器,请使用remove_copy_if和swap或者通过容器的元素编写一个循环,记得将迭代器传递给erase当时,它需要用后缀递增。

AssocCOntainer c;

...

AssocContainer goodValues;

remove_copy_if(c.begin(), c.end(),

inserter(goodValues, goodValues.end()), badValue);

c.swap(goodValues);

for(AssocContainer::iterator i = c.begin();i !=c.end();/ do nothing /){

if(badValue(*i)) c.erase(i++);

else ++i;

}

要在循环中执行某些操作(删除对象除外):

如果容器是标准序列容器,写一个循环遍历容器中的元素,记得每次都用erase何时,使用迭代器的返回值更新迭代器。

如果容器是标准关联的容器,请编写一个循环来遍历容器中的元素,记得将迭代器传递给erase当迭代器需要用后缀递增时。

第12文章:不要STL容器的线程安全具有不切实际的依赖关系。

对一个STL最多只能期望多线程读取是安全的;多个线程写入不同的容器是安全的。

你不能指望STL该库将使您免于手动同步控制,并且您不能依赖任何线程支持。

第2章 vector和string

第13条:vector和string优先于动态分配的阵列。

如果用new意味着您需要确保稍后完成delete。

如果您正在使用string它是通过引用计数实现的,并且您在多线程环境中运行并相信string引用计数的实施会影响效率,因此您至少有三个可行的选项,并且没有一个是可以放弃的STL。首先,检查库实现,看看是否可以禁用引用计数,通常是通过更改预处理变量的值。其次,找到或开发一种不使用引用计数的方法string实现。第三,考虑使用vector而不是string。vector的实现不允许使用引用计数,因此不会发生隐藏的多线程性能问题。

第14条:使用reserve避免不必要的重新分发。

通常有两种使用方法reserve避免不必要的重新分发。第一种方法是使用reserve。第二种方法是首先保留足够的空间,然后在添加所有数据后,删除产能过剩。

第15条:注意string实施的多样性。

如果你想有效地使用它STL所以你需要知道string实现的多样性,尤其是当您编写的代码必须位于不同的代码时STL在平台上运行时,面临严格的性能要求。

第16文章:了解如何vector和string将数据传输到旧API。

如果您有vector v你需要得到一个v指向数据的指针,这样数据就可以作为一个数组对齐,那么只有和v[0没关系,你也可以使用&*v.begin(),但这并不容易理解。大约string s相应的表格是s.c_str()。

如果要使用C API初始化一个vector所以你可以使用vector兼容数组的内存布局,先写入数据vector然后将数据复制到预期的最终写入STL容器中。

第17酒吧:使用”swap技巧” 除去 产能过剩。

vector(contestants).swap(contestants);

表达式vector(contestants)创建一个临时向量contestants副本:这是由 vector复制构造函数用于完成它。然而vector的拷贝构造函数只为所拷贝的元素分配所需要的的内存,所以这个临时矢量没有产能过剩。然后我们把临时矢量中的数据和contestants数据在swap操作,之后,contestants它被移除后的容量,即原始临时变量的容量,临时变量的容量成为原始容量contestants容量庞大。此时,临时向量被解构,从而释放前一个contestants占用的内存量。

同样的技术适用于string也实用:

string s;

...

string(s).swap(s);

第3章 关联容器

第19文章:了解平等(equality)和等价性(equivalence)的区别。

标准关联容器始终保持顺序,因此每个容器必须具有比较功能(默认值为less)。等价的定义是通过此比较函数确定的。平等总它是等价的,等价并不总是相等的。

第20条形:指定包含指针的关联容器的比较类型。

每当您创建包含指针的关联容器时,将根据指针的值(即内存地址)对容器进行排序,这在大多数情况下不是您想要的。

第21柱线:在等价的情况下始终使比较函数返回false。

现在我将向你们演示一个很酷的现象。创建一个set,用less_equal作为其比较类型,则10插入到此集合中:

set<int, less_equal > s; //s 用"<=" 来排序

s.insert(10);

s.insert(10);

对于第二个insert集合将检查以下表达式是否为真:

!(10a <= 10b) && !(10b <= 10a); //检查10a和10b等效吗?结果是false

结果集中有两个10!

从技术上讲,用于对关联容器进行排序的比较函数必须为它们比较的对象定义“严格的弱排序”(strict weak ordering)。

第22文章:不要直接修改set或multiset中的键。

如果您不关心可移植性并且想要更改set或multiset元素的值和您的STL实施(是STL在实施中,例如set:: iterator 的operator*总是返回const T&如果允许您这样做,请继续该过程。请注意不要更改元素中的关键部分,这可能会影响容器的顺序。

如果您重视可移植性,则需要确保set和multiset中的元素无法修改。至少它不能在不强制转换的情况下强制转换为引用类型const_cast<T&>)只需修改它。

如果您想以始终可行和安全的方式做出承诺set、multiset、map和multimap中的元素可分为5采取简单的步骤:

1. 找到要修改的容器元素。如果您不确定最佳方法45本文介绍了如何执行适当的搜索以查找特定元素。

2. 复制要修改的元素。留map和multimap在这种情况下,请记住不要将副本的第一部分声明为const。毕竟,你想改变它。

3. 修改副本以具有所需的值。

4. 从容器中删除此元素,通常通过以下方式erase将要执行(见第10段)9条)。

5. 将副本插入容器。如果根据容器的排列顺序,新元素的位置可能与已删除元素的位置相同或相邻,请使用“prompt”(hint)形式的insert为了提高从对数时间到恒定时间的插入效率。带你从1分步迭代器作为提示信息。

第23项目:考虑使用排序vector更换关联的容器。

标准关联容器通常实现为平衡的二叉搜索树。也就是说,它适合的应用程序首先执行一些插入操作,然后执行查找,然后可能插入一些元素,也许删除一些,然后执行查找,等等。这一系列时间的主要特点是混合了插入、删除和超搜索。总的来说,不可能预测这棵树的下一个操作是什么。

许多应用程序使用其数据结构的方式并不那么混乱。使用其数据结构的过程可以明确分为三个阶段,总结如下:

1. 设置舞台。创建新的数据结构并插入大量元素。在此阶段,几乎所有操作都是插入和删除操作。查找操作很少或几乎没有。

2. 查找操作。查询数据结构以查找特定信息。在此阶段,几乎所有操作都是查找操作,很少或没有插入和删除操作。

3. 重组阶段。更改数据结构的内容可能涉及删除所有当前数据并插入新数据。在行为方面,这个阶段与1阶段相似。此阶段结束后,应用程序将返回到2阶段。

第24当效率至关重要时,请输入map::operator[]与map::insert在它们之间仔细选择。

如果要更新现有映射表元素,请选择operator[];如果要添加新元素,请选择insert。

第25文章:熟悉非标准哈希容器。

标准C++库没有任何哈希容器,大家都觉得很可惜,但是C++标准委员会认为,将它们纳入标准会延迟标准的完成。已决定在下一版本的标准中包含哈希容器。

第4章 迭代器

第26条:iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator。

减少混合不同类型的迭代器的机会,并尝试尽可能多地使用它们iterator代替const_iterator。从const从正确性的角度来看,只是为了避免一些可能出现的问题 STL由于实施缺陷而放弃使用const_iteraor显得不公平。但考虑到容器类中的某些成员函数指定使用iterator根据目前的情况,可以得出结论, iterator较之const_iterator得出更实际的结论并不奇怪。此外,从实际的角度来看,并不总是值得参与 const_iterator在麻烦之中。

第27条:使用distance和advance将容器的const_iterator转换成iterator。

以下代码尝试转换const_iterator强制转换为iterator:

typedef deque IntDeque; //类型定义,简化代码

typedef IntDeque::iterator Iter;

typedeef IntDeque:;const_iterator ConstIter;

ConstIter ci; //ci 是一个const_iterator

...

Iter i(ci); //编译错误!从const_iterator 到 iterator无隐式转换路径

Iter i(const_cast(ci)); //还是编译错误!无法const_iterator强制转换为iterator

无法编译包含显式类型转换的代码的原因是,对于这些容器类型,iterator和const_iterator这是一个完全不同的阶级,他们的关系甚至比string和complex他们之间的关系更加疏远。

这是此解决方案的本质。

typedef deque IntDeque; //类型定义,简化代码

typedef IntDeque::iterator Iter;

typedeef IntDeque:;const_iterator ConstIter;

IntDeque d;

ConstIter ci; //ci 是一个const_iterator

... //使ci指向d

Iter i(d.begin());//使i指向d起始位置

advance(i,distance(i,ci));//移动i让它指向ci指示位置

这种方法看起来非常简单明了,也很令人惊讶。为了获得与const_iterator指向同一位置iterator首先,创建一个新的 iterator将其指向容器起始位置,然后获得const_iterator与容器起始位置的偏移量和iterator向前移动相同的偏移量。此技术的效率取决于您使用的迭代,对于随机迭代器,它是一个恒定时间操作;对于双向迭代器以及一些哈希容器,它是线性时间运算。

第28正确理解reverse_iterator的base()由成员函数生成iterator的用法。

如果你愿意reverse_iterator ri在指定位置插入元素,然后只需ri.base()在位置插入元素。对于插入操作,ri和ri.base()它是等价的,ri.base()是真正与ri对应的iterator。

如果你愿意reverse_iterator ri要删除指定位置的元素,必须ri.base()对上一个位置执行删除操作。对于删除操作,ri和ri.base()它不等同。

我们还是有必要看一下执行这种删除操作的实际代码,其中包含一些惊喜:

vector v;

... //同上,插入1到5

vector::reverse_iterator ri = find(v.rbegin(),v.rend(),3);//使ri指向3

v.erase(--ri.base()); //试图删除ri.base()上一个元素,用于vector通常,编译会失败

对于vector和string此代码可能有效,但对于vector和string它的许多实现无法编译。这是因为在这样的实现中, iterator(和vconst_iterator)它是通过内置指针实现的,因此ri.base()结果是一个指针。C和C+和+都规定从函数返回的指针不应修改,因此编译无法通过。

既然我们不能base()要对结果执行递减操作,只需先递增reverse_iterator然后打电话base()功能就够了!

...

v.erase((++ri).base()); //删除ri提到的元素,现在编译没有问题!

第29对于逐个字符的输入,请考虑使用istreambuf_iterator。

如果要将文本文件的内容复制到string在该对象中,以下代码似乎是合理的解决方案:

ifstream inputFile("interestingData.txt");

inputFIle.unsetf(ios::skipws);//istream_iterator使用operator>>函数完成实际读取操作,默认情况下operator>>该函数将跳过空格字符

string fileData((istream_iterator (inputFIle)),istream_iterator());

但是,您可能会发现整个复制过程并不像您想要的那么快。istream_iterator内部使用operator>>实际上,执行了格式化输入,但是如果您只想从输入流中读取下一个字符,则它变得有点多余。

有一种更有效的使用方法STL中国最神秘的法宝之一:istreambuf_iterator。 istreambuf_iterator对象使用方法和istream_iterator大致相同,但是istreambuf_iterator直接从流的缓冲区读取下一个字符。更具体地说, istreambuf_iterator输入流中的对象istream s读取下一个字符的操作由s.rdbuf()->sgetc()要完成

ifstream inputFile("interestingData.txt");

string fileData((istreambuf_iterator(inputFile)),istreambuf_iterator());

这次我们不需要清楚输入流skipws徽标,因为istreambuf_iterator不会跳过任何字符。

同样,对于未格式化的逐字符输出过程,您还应考虑使用ostreambuf_iterator。

第5章 算法

第30文章:确保目标范围足够大。

当程序员想要向容器添加新对象时,下面是一个示例:

int transmogrify(int x); //此函数基于x生成新值

vector values;

vector results;

transform(values.begin(),values.end(),back_inserter(results),transmogrify);

back_inserter返回迭代将导致push_back被称为,所以back_inserter适用于所有可用产品push_back方法的容器。同样地front_inserter仅适用于提供以下人员push_front成员函数的容器(例如deque和list)。

当是使用reserver提高序列插入操作的效率时,请记住reserve它只增加了集装箱的容量,但集装箱的尺寸没有改变。当算法需要 vector或者string添加新元素,即使它们已被调用reserve还必须使用插件迭代器。以下代码提供了一种不正确的方法:

vector values;

vector results;

...

results.reserve(results.size() + values.size());

transform(values.begin(), values.end(), results.end(), transmogrify);//转换的结果将写入未初始化的内存,结果将是不确定的

在上面的代码中transform欣然接受results最后在未初始化的内存中执行复制操作的任务。由于强调在两个对象之间而不是在对象和未初始化的内存块之间分配值,因此此代码通常在运行时失败。

假设希望transform覆盖results如果容器中已经有元素,则必须确保results中的现有元素至少匹配values里面的元素也一样多。否则,必须使用它resize为了确保这一点。

vector values;

vector results;

...

if(results.size() < values.size()){

results.resize(values.size());

}

transform(values.begin(),values.end(),results.begin(),transmogrify);

或者,可以先清除它results然后以通常的方式使用插入类型迭代:

...

results.clear();

results.reserve(values.size());

transform(values.begin(),values.end(),back_inserter(results),transmogrify);

第31文章:了解与排序相关的各种选项。

sort(stable_sort)、partial_sort和nth_element算法需要立即访问迭代器,因此这些算法只能应用于 vector、string、deque和数组。partion(stable_partion)只需要一个双向迭代器即可完成工作。

对标准关联容器中的元素进行排序没有实际意义,因为它们始终使用比较函数来维护内部元素的有效性。

list是唯一需要排序但不能使用这些排序算法的容器。因此list特别提供sort成员函数(有趣的是,list::sort执行稳定排序。如果你想要一个list要执行完整的排序,您可以使用sort成员功能;但是,如果有必要list使用partial_sort或者nth_element如果使用算法,则只能间接实现。一种间接方法是list将元素复制到提供随机访问迭代器的容器中,然后在该容器上执行所需的算法;另一种介绍方法是首先创建一个list::iterator在容器上执行相应的算法,然后通过其迭代器访问它list元素;第三种方法是通过重复调用来利用包含迭代器的有序容器的信息splice杆件函数,将 list将元素调整到所需的目标位置。如您所见,您将有很多选择。

第32项目:如果确实需要删除元素,则需要在remove这种算法后来称为erase。

1 2 3 99 5 99 7 8 9 99

调用remove(v.begin(),v.end(),99);后变成

1 2 3 5 7 8 9 8 9 99

remove无法从迭代器推断出对应的容器类型,因此无法调用容器的成员函数erase因此,不可能真正删除该元素。另外两种算法remove_if和 unique类似。然而list::remove和list::unique实际上会删除元素(例如使用erase-remove和erase-unique 更高效),即STL中间不一致。

第33条形:用于包含指针的容器remove使用这种类型的算法时要特别小心。

无论您如何处理包含动态分配指针的容器,您始终可以执行以下操作: 或调用remove在类算法之前,手动删除指针并将其设置为 null,或通过引用对智能指针进行计数( 如boost::shared_ptr)或者你自己发明的其他一些技术。

以下代码使用第一种方法:

void delAndNullifyUncertified(Widget*& pWidget)

{

if(!pWidget->isCertified())

{

delete pWidget;

pWidget = 0;

}

}

for_each(v.begin(),v.end(),delAndNullifyUndertified);

v.erase(vemove(v.begin(),v.end(),static_cast<Widget*>(0)),v.end());

下面的代码使用第二种方法:

template<typename T> //RSCP = "Reference Counting Smart Pointer"

class RCSP{...};

tpedef RCSP RCSPW;

vector v;

...

v.push_back(RCSPW(new Widget));

...

v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());

第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实现忽略大小写的简单字符串比较。

用mistatch实现:

//此函数确定两个字母是否相同,忽略它们的大写

int ciCharCompare(char c1, char c2)

{

int lc1 = tolower(static_cast<unsigned_char>(c1));

int lc2 = tolower(static_cast<unsigned_char>(c2));

if(lc1 < lc2) return -1;

if(lc1 > lc2) return 1;

return 0;

}

/ 此函数确保将其传递给ciStringCompareImpl的s1比s2短,如果s1和s2相同,返回0;如果s1比s2短,返回-1;如果s1比s2长,返回1。/

int ciStringCompare(const string& s1, const string& s2)

{

if(s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);

else return – ciStringCompareImpl(s2, s1);

}

//如果s1和s2相同,返回0;如果s1比s2短,返回-1;如果s1和s2所有不匹配都发生在非结束位置,由最初不匹配的字符决定。

int ciStringCompareImpl(const string &s1, const string &c2)

{

typedef pair<string::const_iterator,string::const_iterator> PSCI;

PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));

if(p.first == s1.end()){

if(p.second == s2.end()) return 0;

else return -1;

}

return ciCharCompair(p.first, p.second);

}

用lexicographical_compare实现:

bool ciCharLess(char c1, char c2)

{

return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));

}

bool ciStringCompare(const string &s1,const string &s2)

{

return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);

}

第36条:理解copy_if算法的正确实现。

STL中没有copy_if以下是算法的实现,但还不够完美:

template<typename INputIterator,typename OUtputIterator,tpename Predicate>

OutputIterator copy_if(INputIterator begin,INputIterator end,OutputIterator destBegin,Predicate p)

{

return remove_copy_if(begin,end,destBegin, not1(0));

}

copy_if(widgets.begin(), widgets.end(), ostream_iterator(cerr, "\n"),isDefective);//编译错误

因为not1它不能直接应用于函数指针(请参阅41文章),函数指针必须首先使用ptr_fun执行转换。要调用copy_if这种实现,您不仅应该传入一个函数对象,还应该传入一个适配器(adaptable)的函数对象。虽然这很容易实现,但为了成为STL算法,它不能给客户这样的负担。

下面是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执行间隔统计。

确保accumulate的返回类与初始值类型相同。for_each返回一个函数对象。accumulate不允许副作用for_each允许。这是一个根深蒂固的问题,也涉及STL核心问题,有待解决)

第6章 函数子、函数子类、函数等

第38规则:遵循按值传递的原则来设计函数子类。

在STL在函数中,当函数对象在函数之间来回传递时,它们也会像函数指针一样通过值传递。因此,您的函数对象必须尽可能小,否则复制成本会很大;其次,函数对象必须是单态的,即不能使用虚函数。这是因为如果参数的类型是基类类型,而实际参数是派生类对象,则在传递过程中会出现剥离问题(slicing problem)在对象复制过程中,可能会删除派生部分,只保留基类部分(请参阅部分3条)。

试图禁止多态函数运算符也是不切实际的。因此,我们必须找到一种方法来两全其美,它允许函数对象很大和/或保留多态性,并且还可以与STL使用基于值的传递函数的习惯保持一致。这种方法是将所需的数据和 Virtual 函数从函数子中分离出来,将它们放入一个新类中,然后在函数 sub 中设置一个指针指向新类。

第39文章:确保判别器是“纯函数”。

判别式(predicate)它是返回值bool类型的函数。纯函数(pure function)它指的是一个函数,其返回值仅取决于其参数。

因为接受函数子STL该算法可能首先创建函数子对象的副本,然后使用此副本。因此,这一特征的直接反映是判别函数必须是纯函数。

template<typename FwdIterator,typename Predicata>

FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)

{

begin = find_if(begin, end, p);//可能是p的拷贝

if(begin == end return begin;

else{

FwdIterator next = begin;

return remove_copy_if(++next, end, begin, p);//可能是p另一个副本

}

}

第40文章:如果类是函子,则应使其可连接。

4标准功能适配器(not1、not2、bind1st和bind2nd)都需要一些特殊的类型定义。提供这些必要的类型定义(argument_type、first_argument_type、second_argument_type以及result_type)的函数对象称为可匹配(adaptable函数对象,相反,如果函数对象缺少这些类型定义,则称其为不匹配。可匹配的函数对象可以与其他对象匹配STL组件协同工作更加一致。但是,不同类型的函数子类需要不同的类型定义,除非编写自定义适配器,否则不需要知道这些类型定义的详细信息。这是因为提供这些类型定义的最简单方法是让函数子类继承自特定的基类,或者更准确地说,如果函数子类的operator()只有一个形式参数,因此应从 std::unary_function继承模板的实例;如果函数的子类operator()有两个形式参数,因此应从std:: binary_function继承。

对于unary_function必须指定函数子类operator()携带的参数类型和返回类型;大约binary_function必须指定三种类型:operator()的第一个和第二个参数的类型,以及operator()的返回类型。下面是两个示例:

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;

};

你可能已经注意到了MeetsThreshold是一个类,并且WidgetNameCompare它是一个结构。那是因为MeetsThreshold包含状态信息(数据成员threshold)类是封装状态信息的逻辑方式;相反,WidgetNameCompare它不包含状态信息,因此不需要私有成员。如果函数的所有成员都是公共的,则通常将其声明为结构而不是类。究竟是选择结构还是类来定义函数子级纯属个人编码风格,但如果正在改进自己的编码风格,希望它更专业,就应该注意了,STL(例如less、 plus通常定义为结构。

一起来看看吧WidgetNameCompare:

struct WidgetNameCompare:

public std::binary_function<Widget, Widget, bool> {

bool operator() (const Widget& lhs, const Widget& rhs) const;

};

虽然operator()所有参数类型均为const Widget&但我们把它传递给了binary_function类型为Widget。通常,它被传递给unary_function或 binary_function需要删除的非指针类型const和引用(&)偏(不问原因,有兴趣可以参观 boost.org卡片可能会看到它们正在呼叫功能(traits)和函数对象适配器)。

如果operator()使用指针参数时,规则再次不同。以下是WidgetNameCOmpare该函数的另一个版本与此版本不同,使用Widget*指针作为参数:

struct PtrWidgetNameCompare:

public std::binary_function<const Widget, const Widget, bool> {

bool operator() (const Widget lhs, const Widget rhs) const;

};

第41条:理解ptr_fun、mem_fun和mem_fun_ref的来由。

如果有函数f和一个对象x现在我希望x上调用f而我们是x为了执行此调用,C++提供三种不同的语法:

f(x); //语法#1:f它是一个非成员函数

x.f(); //语法#2:f是一个杆件函数,并且x是对象或对象引用

p->f(); //语法#3:f是杆件函数,并且p它是一个指向对象x的指针

现在让我们假设有一个可用于测试Widget对象的功能:

void test(Widget& w);

另一个存储Widget对象的容器:

vector vw;

为了测试vw每一个在Widget自然可以通过以下方式调用对象for_each:

for_each(vw.begin(), vw.end(), test); //调用#1 (可以编译)

但是,加入test是Widget的杆件函数,即Widget支持自检:

class Widget{

public:

...

void test();

....

};

所以在理想情况下,也应该可以使用for_each在vw在 中的每个对象上调用Widget::test成员功能:

for_each(vw.begin(), vw.end(), &Widget::test);//调用#2(无法编译)

事实上,如果它真的很理想,那么对于存储Widget* 指针的容器也应该能够通过以下方式访问for_each来调用Widget::test:

list<Widget*> lpw;

for_each(lpw.begin(), lpw.end(), &Widget::test);//调用#3(也无法编译)

这是因为STL一个常见的约定是,当调用函数或函数对象时,它始终使用非成员函数的语法形式(即#1)。

现在mem_fun和mem_fun_ref它们必须存在的原因已经很清楚了——它们用于调整(通常#2和#3)杆件函数,使它们能够传递语法#1被调用。 mem_fun、mem_fun_ref方法其实很简单,只要看一下任何一个函数的声明就清楚了。它们是真正的函数模板,根据它们连接到的成员函数的圆形形状,具有多种变体。让我们看一下其中一个语句来了解它是如何工作的:

template<typename R, typename C> //该mem_fun声明不带参数的非参数const杆件函数,C是类,R成员函数的返回类型是否指向

mem_fun_t<R,C>

mem_fun(R(C::*pmf) ());

mem_fun使用指向成员函数的指针参数pmf并返回一个mem_fun_t类型的对象。mem_fun_t是函数的子类,它保存指向成员函数的指针并提供operator()函数,在operator()通过参数传入的对象上的成员函数被调用。例如,请看下面的代码:

list<Widget*> lpw;

...

for_each(lpw.begin(),lpw.end(),mem_fun(&Widget::test));//现在可以编译了

for_each收到一种类型mem_fun_t保存指向的指针的对象Widget::test指向的指针。大约lpw每一个在 Widget指针,for_each将使用语法#1来调用mem_fun_t对象,则对象立即使用语法#3调用Widget指针的 Widget::test()。

(ptr_fun是多余的吗mem_fun它是成员函数的适配器,mem_fun_ref它是对象容器的适配器。

第42条:确保less与operator<具有相同的含义。

operator<不仅仅是less默认的实现方法也是程序员所期望的less做什么。让less不调用operator<而去做别的事情会不公正地违背程序员的意愿,这与让人惊讶的“少”原则是一致的(the principle of least astonishment)完全相反。这是非常糟糕的,您应该尽可能避免这样做。

如果要以特殊方式对对象进行排序,最好创建一个特殊函数子类,其名称不能less。

第7章 在程序中使用STL

第43算法调用优于手写循环。

原因有三:

效率:算法通常比程序员自己编写的循环更有效。STL实现者可以针对特定容器优化算法;几乎所有STL这些算法都使用复杂的计算机科学算法,其中一些非常复杂且不是平均水平C++程序员可以达到它。

正确性:自己编写的循环比使用算法更容易出错。例如,迭代器在插入元素后可能会失败。

可维护性:使用算法的代码通常比使用手写循环的代码更简洁明了。算法的名称指示其功能,以及for、while和do但不是每个专业人士C++程序员应该知道每个算法的作用,看到算法可以揭示代码的功能。对于循环,只能继续查看特定的代码以了解其目的。

第44文章:容器的成员函数优先于同名算法。

首先,成员函数通常速度很快;其次,成员函数通常与容器(尤其是关联容器)结合得更紧密(相等和等价的差异,如关联容器),count只能使用相等性检验。

第45条形:正确的区分count、find、binary_search、lower_bound、upper_bound和equal_range。

你想知道什么

使用算法 使用成员函数 未排序 排序 set或map multiset或multimap 是否存在特定值 find binary_search count find 是否存在特定值?如果是这样,第一个在哪里 find equal_range find find或lower_bound 其中 是第一个不超过特定值的对象 find_if lower_bound lower_bound lower_bound 其中,第一个超过特定值的对象 find_if upper_bound upper_bound upper_bound 有多少对象具有特定值 count equal_range (然后distance) count count 具有特定值的对象在哪里 find(反复调用) equal_range equal_range equal_range

第46文章:考虑使用函数对象而不是函数指针作为STL算法的参数。

函数指针抑制内联机制,编译器可以针对函数对象进行优化。

另一个原因是这样做有助于避免语言本身的一些细微缺陷。在偶尔的情况下,编译器可能会出于合法但不明确的原因拒绝一些看似合理的代码。例如,当函数模板的实例化名称与函数名称不完全等效时,可能会出现此类问题。下面是一个示例:

template<typename FPType>

FPType average(FPType val1, FPType val2)//返回两个浮点值的平均值

{

return (val1 + val2) / 2;

}

template<typename InputIter1,typename InputIter2>

void writeAverages(InputIter1 begin1, INputIter1 end1, InputIter2 begin2,ostream& s) //按顺序平均两个序列的值并将它们写入流中

{

transform(begin1, end1, begin2,

ostream_iterator<typename iterator_trais::value_type(s,"\n")>,

average<typename iterator_traits::value_type> //错误?

);

}

许多编译器接受此代码,但C++标准不识别此类代码。原因是理论上存在另一个名字,叫做average函数模板也只采用一个类型参数。如果是这样,表达式average<typename iterator_traits::value_type>由于编译器无法区分应实例化哪个模板,因此会出现歧义。可以用函数对象替换它。

第47酒吧:避免创建“直接写作”(write-only)的代码。

读取代码的次数远远超过写入代码的次数。

第48条形图:始终包含(#include)正确的头文件。

几乎所有标准STL容器在具有相同名称的头文件中声明。

除了4个STL除算法外,所有其他算法均在中,这4个算法是accumulate、 inner_product、adjacent_difference和partial_sum它们都在头文件中声明 中。

特殊类型的迭代器,包括istream_iterator和istreambuf_iterator(见第29文章),声明于中。

标准函子(例如less)和功能子适配器(如not1、bind2nd)在头文件中声明中。

第49文章:学习分析和STL相关的编译器诊断信息。

替换为文本(例如string替换掉basic_string<char,struct std::char_traits,class std::allocator >)。

第50条:熟悉STL相关的Web站点。

SGI STL

站点: http://www.sig.com/tech/stl/

STLport

站点: http://stlport.org

BOost

站点: http://boost.org

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除