任何时候都适用的20个C++技巧 <14-20> STL and Generic Programming
2010-12-09 13:54 凌云健笔 阅读(2672) 评论(1) 编辑 收藏 举报标准模板库和通用编程
标准模板库(STL)给C++程序员编写代码的方式带来了革命性的影响。这样的代码重用将生产力水平提升到了更高的水平,节省了大量的时间,避免了重复性的劳动。然而,STL是一个具有特殊术语和复杂规则的、比较全面的框架,如果你想更好的去应用它,那么你只能去掌握它,“知己知彼方能百战不殆”吗。为了更深入地了解STL某些方面的情况,这大类中将包含6个小技巧。
第一个技巧将介绍一下STL的基本组成和一些关键术语。接下来的小技巧则集中于模板定义这一方面。正如你所知,模板是STL容器和算法最基础的“建筑材料”。接下来的三个小技巧将依次描述如何使用标准库中应用最为广泛的容器 - vector,学习如何在vector中存储对象指针,避免常见陷阱,以及如何将vector当做一个内置的数组来使用。第五个提示将会告诉你如何使用vector来模仿多维数组。最后的提示将介绍一个非常重要的问题:auto_ptr和STL容器之间的一些问题。
技巧14:非常有用的STL术语
接下来所讲述的是STL中一些非常关键的条款。也许在你阅读标准模板库(STL)文献或文档的时候,您遇到过它们。
Container 容器
容器是一个对象,它将对象作为元素来存储。通常情况下,它是作为类模板来实现,其成员函数包括遍历元素,存储元素和删除元素。std::list和std::vector就是两种典型的容器类。
Genericity 泛型
泛型就是通用,或者说是类型独立。上面对于容器类的定义是非常宽松的,因为它适用于字符串,数组,结构体,或者是对象。一个真正的容器是不局限于某一种或着某些特定的数据类型的。相反,它可以存储任何内置类型或者用户自定义类型。这样的容器就被认为是通用的。请注意,string只能包含字符。泛型也许是STL的最重要的特征。第三个技巧将给出函数对象的标准基类。因为函数对象是通用编程重的一个重要部分。在设计实现过程中,坚持标准规范将会省去你的很多的困难。
Algorithm 算法
算法就是对一个对象序列所采取的某些操作。例如std::sort()排序,std::copy()复制,和std::remove()删除。STL中的算法都是将其作为函数模板来实现的,这些函数的参数都是对象迭代器。
Adaptor 适配器
适配器是一个非常特殊的对象,它可以插入到一个现有的类或函数中来改变它的行为。例如,将一个特殊的适配器插入到std::sort()算法中,你就可以控制排序是降序还是升序。 STL中定义了多种类型的序列适配器,它可以将一个容器类变换成一个具有更严格接口的不同容器。例如,堆栈(stack)就可以由queue<>和适配器来组成,适配器提供了必要的push()和pop()操作。
O(h) Big Oh Notation
O(h)是一个表示算法性能的特殊符号,在STL规范当中用于表示标准库算法和容器操作的最低性能极限。任何其他的实现可能会提供更好的性能,但绝不是更坏的。O(h)可以帮助您去评估一个算法或者某个特定类型容器的某个操作的效率。std::find()算法遍历序列中的元素,在最坏的情况下,其性能可以表示为:
T(n) = O(n). /* 线性复杂度 */
Iterator 迭代器
迭代器是一种可以当做通用指针来使用的对象。迭代器可以用于元素遍历,元素添加和元素删除。 STL定义了五个种主要的迭代器:
输入迭代器和输出迭代器 input iterators and output iterators
前向迭代器 forward iterators
双向迭代器 bidirectional iterators
随机访问迭代器 random access iterators
请注意,上述迭代器列表并不具有继承关系,它只是描述了迭代器种类和接口。下面的迭代器类是上面类的超集。例如,双向迭代器不仅提供了前向迭代器的所有功能,还包括一些附加功能。这里将对这些类别做简要介绍:
输入迭代器允许迭代器前行,并提供只读访问。
输出迭代器允许迭代器前行,并提供只写访问。
前向迭代器支持读取和写入权限,但只允许一个方向上的遍历。
双向迭代器允许用户在两个方向遍历序列。
随机访问迭代器支持迭代器的随机跳跃,以及“指针算术”操作,例如:
string::iterator it = s.begin();
char c = *(it+5); /* assign sixth char to c*/
技巧15:模板定义的位置在哪里?是.cpp文件吗?
通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。
另外一个方法就是使用关键字“export”!你可以在.h文件中,声明模板类和模板函数;在.cpp文件中,使用关键字export来定义具体的模板类对象和模板函数;然后在其他用户代码文件中,包含声明头文件后,就可以使用该这些对象和函数了。例如:
// output.h - 声明头文件
template<class T> void output (const T& t);
// out.cpp - 定义代码文件
#include <****>
export template<class T> void output (const T& t) {std::cerr << t;}
//main.cpp:用户代码文件
#include "output.h"
void main() // 使用output()
{
output(4);
output("Hello");
}
某种程度上,这有点类似于为了访问其他编译单元(如另一代码文件)中普通类型的变量或对象而采用的关键字extern。
但是,这里还有一个不得不说的问题:并非所有的编译器都支持export关键字(我们最熟悉、最常用的两款编译器VS 和 GCC就是不支持export的典型代表)。对于这种不确定,最好的方法就是采用解决方案一:声明定义放在一起,虽然这在某种程度上破坏了C++编程的优雅
性。
---------------版权分割线--------以下引自 飞诺网(www.firnow.com)-------------
分离编译模式(Separate Compilation Model)允许在一处翻译单元(Translation Unit)中定义(define)函数、类型、类对象等,在另一处翻译单元引用它们。编译器(Compiler)处理完所有翻译单元后,链接器(Linker)接下来处理所有指向 extern 符号的引用,从而生成单一可执行文件。该模式使得 C++ 代码编写得称心而优雅。
然而该模式却驯不服模板(Template)。标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。
以优雅著称的 C++ 是不能容忍有此“败家玩意儿”好好活着的。标准 C++ 为此制定了“模板分离编译模式(Separation Model)”及 export 关键字。然而由于 template 语义本身的特殊性使得 export 在表现的时候性能很次。编译器不得不像 .net 和 java 所做的那样,为模板实体生成一个“中间伪代码(IPC,intermediate pseudo - code)”,使得其它翻译单元在实例化时可找到定义体;而在遇到实例化时,根据指定的 typename 实参再将此 IPC 重新编译一遍,从而达到“分离编译”的目的。因此,该标准受到了几乎所有知名编译器供应商的强烈抵制。
谁支持 export 呢?Comeau C/C++ 和 Intel 7.x 编译器支持。而以“百分百支持 ISO ”著称的 VS 和 GCC 却对此视而不见。真不知道这两大编译器“百分百支持”的是哪个版本的 ISO。在 VS 2008 中,export 关键字在 IDE 中被标蓝,表示 VS IDE 认识它,而编译时,会用警告友情提示你“不支持该关键字”,而配套的 MSDN 9 中的 C++ keywords 页则根本查不到该关键字;而在 VS 2010 中,就没那么客气了,尽管 IDE 中仍然会将之标蓝,但却会直截了当地报错。
---------------版权分割线--------以上引自 飞诺网(www.firnow.com)-------------
技巧16:函数对象的标准基类
为了简化编写函数对象的过程,标准库提供了两个类模板,作为用户自定义函数对象的基类:std::unary_function和std::binary_function。两者都声明在头文件<functional>中。正如名字所显示的那样,unary_function被用作是接受一个参数的函数的对象的基类,而binary_function是接受两个参数的函数的对象的基类。这些基类定义如下:
template < class Arg, class Res > struct
unary_function
{
typedef Arg argument_type;
typedef Res result_type;
};
template < class Arg, class Arg2, class Res >
struct binary_function
{
typedef Arg first_argument_type;
typedef Arg2 second_argument_type;
typedef Res result_type;
};
这些模板并不提供任何实质性的功能。他们只是确保其派生函数对象的参数和返回值有统一的类型名称。在下面的例子中,is_vowel继承自unary_function,接受一个参数:
template < class T >
class is_vowel: public unary_function< T, bool >
{
public:
bool operator ()(T t) const
{
if ((t=='a')||(t=='e')||(t=='i')||(t=='o')||(t=='u'))
return true;
return false;
}
};
技巧17:如何在STL容器中存储动态分配的对象?
假设你需要在同一容器中存储不同类型的对象。通常情况下,您可以通过存储储动态分配对象的指针来达到这一点。然而,除了使用指针外,也可以按照下面的方式将元素插入到容器中:
class Base {};
class Derived : public Base{};
std::vector <Base *> v;
v.push_back(new Derived);
v.push_back(new Base);
如果按照这种方式,那么存储的对象只能通过其容器来访问。请记住,应按照下面的方式删除分配的对象:
delete v[0];
delete v[1];
技巧18:将向量当作数组使用
假设你有一个整型向量vector<int> v,和一个以int*为参数的函数。为了获得向量v内部数组的地址,并将它传给函数,你必须使用表达式&v[0]或者是&*v.front()。举个例子:
void func(const int arr[], size_t length );
int main()
{
vector <int> vi;
//.. fill vi
func(&vi[0], vi.size());
}
只要你遵守线面的几条规则,你用&vi[0]和&*v.front()作为其内部数组地址就会很安全放心:
(1)fun()不应访问超出数组范围的元素。
(2)向量中的元素必须是连续的。虽然C++标准中并没有做这样的规定,但是据我所知,没有一个vector的实现不是使用连续内存的。
技巧19:动态多维数组和向量的恩恩怨怨
你可以按照以下方式手动分配多维数组:
int (*ppi)[5] = new int[4][5]; /*parentheses required*/
/*fill array..*/
ppi[0][0] = 65;
ppi[0][1] = 66;
ppi[0][2] = 67;
//..
delete [] ppi;
然而,这种编码风格是非常枯燥的,而且容易出错。你必须用圆括号括起ppi,以确保这个声明能被编译器正确解析;同时你也必须手动地删除你所分配的内存。更糟糕的是,你会在不经意间碰上令人头疼的缓冲区溢出。而使用向量的向量来模拟多维数组则是一个更好的选择:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <vector <int> > v; /*two dimensions*/
v.push_back(vector <int>()); /*create v[0]*/
v.push_back(vector <int>()); /*create v[1]*/
v[0].push_back(15); /*assign v[0][0]*/
v[1].push_back(16); /*assign v[1][0]*/
}
因为vector重载了操作符[],你可以向使用内置的二维数组一样使用[][]:
cout << v[0][0];
cout << v[1][0];
用向量的向量模拟多维数组主要有两个优点:向量会自动的按照需要来分配内存。其次,它自己负责释放分配的内存,而你则不必担心潜在的内存泄漏。
技巧20:为什么你不应该在STL容器中存储auto_ptr对象?
在C++标准中,一个STL元素必须是可以“拷贝构造”和“赋值”。这个条款意味着,对于一个给定类,“拷贝”和“赋值”是其非常便利顺手的操作。特别是,当你将它复制到目标对象时,原始对象的状态是不会改变的。
但是,这是不适用auto_ptr的。因为auto_ptr的从一个拷贝到另一个或赋值到另一个对象时会使得原始对象产生预期变动之外的变化。具体说来就是,会将原来的对象的指针转移到目标对象上,从而使原来的指针变为空指针,试想一下,如果照下面代码所示来做,会出现什么结果呢:
std::vector <auto_ptr <Foo> > vf;/*a vector of auto_ptr's*/
// ..fill vf
int g()
{
std::auto_ptr <Foo> temp=vf[0]; /*vf[0] becomes null*/
}
当temp被初始化时,vf[0]的指针变成空。任何对该元素的调用都将导致程序的运行崩溃。在您从容器中复制元素时,这种情况极有可能会发生。注意,即使您的代码中没有执行任何显式的拷贝操作或赋值操作,许多算法(例如std::swap()和std::random_shuffle()等)都会创建一个或多个元素的临时副本。此外,该容器的某些成员函数也会创建一个或多个元素的临时副本,这就会抵消原有的元素指针。以致任何对容器中元素的后续操作都将带来的不确定的后果。
可能Visual C++用户会说,我在STL容器中使用auto_ptr时,从来没有遇到过任何问题。这是因为在Visual C++中auto_ptr的实现已经过时,而且依赖在一个过时规范上。如果厂商决定赶上当前的最新ANSI/ISO C++标准,并相应地改变它的标准库,在STL容器中使用auto_ptr将会导致严重故障。
总结来说,你是不应该在STL容器中使用auto_ptr的。你可以使用普通指针或其他智能指针类,但绝不是auto_ptr指针。
//------------------------------------------
<<任何时候都适用的20个C++技巧>>这个小系列到此就结束了,虽然关于C++的技巧还很多,还值得我们去总结。如果遇到,我会第一时间拿出来与大家分享! 谢谢各位博友!!
作者: 凌云健笔
出处:http://www.cnblogs.com/lijian2010/
版权:本文版权归作者和博客园共有
转载:欢迎转载,为了保存作者的创作热情,请按要求【转载】
要求:未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任