STL与泛形程序设计

来源:互联网 发布:二手家具市场淘宝 编辑:程序博客网 时间:2024/06/06 01:09
ANSI/ISO C++ 程序员手册: STL和泛型程序设计 输出PDF 打印 E-mail 作者: [转载]Danny Kalev    2007-09-27 面向对象设计对代码重用提供了有限的形式继承和多态。泛型程序设计风格可以有更高层次的可重用性。与数据隐藏不同,它依赖数据独立。C++有两个特性来支持数据独立:模板和运算符重载。两种特性合起来可以写出对实际对象类型要求很少的泛型算法,不管对象是基本类型还是用户定义类型。因此,这种算法不限于具体数据类型,比依赖类型的算法有更高的可重用性 by Danny Kalev
  • 简介
  • 泛型程序设计
  • STL头文件的组织

    • 容器
    • 算法
    • 迭代器
    • 数字库
    • 工具
  • 容器

    • 顺序容器
    • STL容器包含元素的要求
    • 容器类vector
    • 容器重分配
    • capacity()和size()
    • 通过构造器来指定容器的容量
    • 访问单一元素
    • Front和Back操作
    • 容器赋值
    • Vectors的邻接区域
    • 一个vector<Base>不能存储Derived对象
    • FIFO对象模型
  • 迭代器

    • begin()和end()
    • 迭代器的底层实现
    • 迭代器的“const Correctness”
    • 通过内建数组来初始化Vector的内容
    • 迭代器的失效
  • 算法

    • 不改变序列的操作
    • 改变序列的操作
    • 排序操作
  • 函数对象

    • 函数对象的实现
    • 函数对象的用途
    • 断言对象
  • 适配器

    • 序列适配器
    • 迭代器适配器
    • 函数适配器
  • 分配器
  • 专用容器
  • 联合容器
  • 类auto_ptr

    • STL容器不能存储auto_ptr元素
  • 类容器
  • 类string

    • 构造器
    • 转换成C字符串
    • 访问单一元素
    • 清除string的内容
    • 比较
    • 附加重载运算符
    • 性能问题
  • 总结



简介


面向对象设计对代码重用提供了有限的形式继承和多态。泛型程序设计风格可以有更高层次的可重用性。与数据隐藏不同,它依赖数据独立。C++有两个特性来支持数据独立:模板和运算符重载。两种特性合起来可以写出对实际对象类型要求很少的泛型算法,不管对象是基本类型还是用户定义类型。因此,这种算法不限于具体数据类型,比依赖类型的算法有更高的可重用性。


标准模板库(Standard Template Library)(STL) 是建立在泛型程序设计基础上的规范性framework。STL是通过迭代器相联系泛型算法和容器的集合。本章探索泛型程序设计的原则,主要在STL。 STL的所有容器和算法可以另写一本书,所以本章只讨论泛型程序设计的基本概念。从STL的头文件开始,其后是STL组件:容器、迭代器、算法、函数对象、适配器和分配器。这些讨论展示了STL算法和容器的广泛用途。最后讨论,class string的细节。


泛型程序设计


泛型软件是主要的可重用软件。可重用性有两个重要特性:适应性和高效率。一些适应性很高的软件组件却因效率太低而得不到广泛应用(这些通常是通过复杂的继承体系、虚函数和广泛使用RTTI的来实现的)。相反的,高效的组件通常是以底层、依赖平台的代码写的,即难移植又难维护。模板克服了这些困难,因为模板在编译期而不是运行期检查,因为模板不需要对象之间的
继承关系,也因为他们对基本类型同样有效。最有用的泛型组件是容器和算法。许多年来,程序员实现自己的链表、队列、集合以及其他容器类型以补充语言的不足;但是自制的容器又很多缺点。他们是不可移植的,有时他们100%的包含bug,他们的接口依赖各自的实现方式,他们在运行时间和内存使用上很难做到最优。


在C++标准化的最后阶段,Alex Stepanov提议给C++增加一个容器和算法的泛型库。他基于他以前为Ada设计的库,提出了一个小型泛型库。在1993年11月,委员会正处在要尽快完成标准化的压力中。因此,扩展语言的建议丢弃了一个又一个。但是Stepanov的建议太好,被委员会一致通过了。


提出的泛型库是基于数学数据模型的容器的集合,比如向量,队列,列表和堆栈。其也包括一套泛型算法,比如排序、合并、查找、替换等等。这些库的关键是通过模板实现的。不过只有模板是不够的,因为基本类型、指针、用户定义类型和单个位都有独自的构造器和运算符。运算符重载为容器和算法抽象实际数据类型提供了同一的形式。下面的章节详细讨论了这些组件的细节。


STL头文件的组织


STL组件都在命名空间std中。他们在下列头文件定义。注意标准以前实现的STL可能使用不同的名字,下同。


容器


容器类在下面的头文件种定义(参见表10.1)。联合容器的multimap和multiset分别在<map>和<set>定义。同样的priority_queue和deque在<queue>中定义。(在标准以前实现的中,容器adaptors stack、队列、和priority_queue在<stack.h>中定义)。



表10.1STL容器





头文件




内容






<vector>




T的数组






<list>




T的双向链表






<deque>




T的循环队列






<queue>




T的队列






<stack>




T的堆栈






<map>




T的数组联合






<set>




T的集合






<bitset>




Boolean值的集合






算法


STL的泛型算法适用与元素序列。他们在下列头文件中定义(参见表10.2)。(在标准以前的实现中,泛型算法在<algo.h>中定义)。


表10.2 STL算法





头文件




Contents






<algorithm>




泛型算法的集合





迭代器


用来导航序列。他们在下面的头文件中定义(参见表10.3)。


表10.3 STL迭代器





头文件




内容






<iterator>




各种类型的迭代器和迭代器支持





数字库


STL提供许多位数字计算特别设计的类和算法(参见表10.4)。


表10.4数字容器和算法





头文件




内容






<complex>




复数和相关运算






<valarray>




数学向量和相关运算






<numerics>




通用数学运算





工具


下面的头文件定义了STL容器和算法所使用的辅助组件(参见表10.5).包括函数适配器、对(pairs)和类auto_ptr(稍后讨论)。


表10.5 通用工具





头文件




内容






<utility>




运算符和对






<functional>




函数对象






<memory>




分配器和auto_ptr





容器


容器是将其他对象作为元素来包容的对象。一个泛型容器不局限于特定对象,它可以存储任何类型的对象。C所支持容器的唯一形式是是内建的数组。其他语言支持其他数据模型。例如Pascal有内建的set类型,Lisp支持表(象它的名字一样)。C++继承了C的数组。数组或多或少的有数学向量的许多属性:他们可以存储任何数据类型,他们提供随机存取,不管元素的位置如何存取数据的时间都是一样的。


然而,对数组有许多批评意见,数组不如其他数据模型方便;在数组中建插入一个新的元素是不可能的。也不能在数组尾部追加新的元素。对于其他数据模型(比如链表),插入新元素,追加新元素都是可以的。特殊类型的表heterogenic list,可以保存不同类型元素。


顺序容器


顺序容器将同类型的对象集合按严格的线性关系组成起来。削面是顺序容器的例子:




  • T v[n]内建数组,存储固定数目(n个)元素,提供随机访问。


  • std::vector<T>类数组的容器,存储可变数目的元素,提供随机访问。在vector的部插入和删除元素可以随时进行。


  • std::deque<T>双向队列,可变长度,随机访问,头尾可进行插入和删除。


  • std::list<T>提供随机访问的可变长度表,可尾在任意位置插入删除元素。



有趣的是,内建数组也被认为是顺序容器,因为为数组设计的STL算法认为他们和其他顺序类型一样。


STL容器包含元素的要求


STL容器的元素必须可以拷贝构造(copy-constructible)可赋值( assignable)。本质上来讲,可拷贝构造意味着对象可以拷贝给另一个对象(尽管标准的解释要复杂的多)。同样的,可赋值意味着可以可以将一个对象赋值给另一个对象。这些定义可能听起来是多余的,因为对象一般都是可拷贝构造和可赋值的;然而,稍后(讨论auto_ptr的时候),你就会看到不符合要求的对象,也就不能由 STL容器存储。


另一个附加的要求是容器元素必须有拷贝构造器、默认构造器、赋值运算符和公有申明的销毁器(显式或隐含的)。


容器类vector


标准接口共享一个公用的接口,但是每一个容器也定义了详细的操作。下面是类vector<T>的接口:



namespace std {
template <class T, class Allocator = allocator<T> >
class vector {
public:
// implementation-defined types
typedef implementation defined iterator;
typedef implementation defined const_iterator;
typedef implementation defined size_type;
typedef implementation defined difference_type;
//附加类型
typedef typename Allocator::reference reference;
typedef typename Allocator::const_reference const_reference;
typedef T value_type;
typedef Allocator allocator_type;
typedef typename Allocator::pointer pointer;
typedef typename Allocator::const_pointer const_pointer
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
//构造器、拷贝构造器、销毁器和赋值运算符
explicit vector(const Allocator& = Allocator());
explicit vector(size_type n, const T& value = T(),
const Allocator& = Allocator());
template <class InputIterator>
vector(InputIterator first, InputIterator last,
const Allocator& = Allocator());
vector(const vector<T,Allocator>& x);
~vector();
vector<T,Allocator>& operator=(const vector<T,Allocator>& x);
template <class InputIterator>
void assign(InputIterator first, InputIterator last);
void assign(size_type n, const T& u);
allocator_type get_allocator() const;
//迭代器
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rend();
const_reverse_iterator rend() const;
//容量操作
size_type size() const;
size_type max_size() const;
void resize(size_type sz, T c = T());
size_type capacity() const;
bool empty() const;
void reserve(size_type n);
//元素访问操作
reference operator[](size_type n);
const_reference operator[](size_type n) const;
const_reference at(size_type n) const;
reference at(size_type n);
reference front();
const_reference front() const;
reference back();
const_reference back() const;
//修改操作
void push_back(const T& x);
void pop_back();
iterator insert(iterator position, const T& x);
void insert(iterator position, size_type n, const T& x);
template <class InputIterator>
void insert(iterator position,
InputIterator first, InputIterator last);
iterator erase(iterator position);
iterator erase(iterator first, iterator last);
void swap(vector<T,Allocator>&);
void clear();
}; //class vector
//非成员重载运算符
template <class T, class Allocator>
bool operator==(const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
template <class T, class Allocator>
bool operator< (const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
template <class T, class Allocator>
bool operator!=(const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
template <class T, class Allocator>
bool operator> (const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
template <class T, class Allocator>
bool operator>=(const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
template <class T, class Allocator>
bool operator<=(const vector<T,Allocator>& x,
const vector<T,Allocator>& y);
//特定算法
template <class T, class Allocator>
void swap(vector<T,Allocator>& x, vector<T,Allocator>& y);
}//namespace std

在大多数实现中,参数化类型size_type和difference_type分别有不同的默认值size_t和
ptrdiff_t。但是他们可以被其他部分特殊化所代替。


自动生成STL容器的存储空间是很必要的,程序员可以从乏味容易出错的分配内存中解放出来。例如,vector可以用从键盘输入的数来作元素的个数。



#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <int> vi;
for (;;) //从控制台读如数据直到0
{
int temp;
cout<<"enter a number; press 0 to terminate" <<endl;
cin>>temp;
if (temp == 0 ) break; //退出循环吗?
vi.push_back(temp); //插入缓存
}
cout<< "you entered "<< vi.size() <<" elements" <<endl;
return 0;
}//end main

容器重分配


STL 容器的内存分配方案必须应付两种相反的要求。一方面,容器不能预分配大容量的内存,因为将消弱系统性能。另一方面,让容器在存储少量元素时就重分配内存也是低效的。分配策略如履薄冰。在大多数实现中,容器先分配一小块缓冲区,增加时在重分配。有时候可以提前估计容器将存储多少元素。在这种情况下,用户可以预分配足够数量的内存以避免以后重分配。想象一些ISP的邮件服务:在4 a.m.到9 a.m.之间,服务比较少,但是其他时候肯能没分钟需要处理上千份邮件。在邮件服务器处理邮件之前,邮件存储在一个vector。允许容器自己一点一点的分配大量小的空间来存邮件导致性能下降。


重分配时发生了什么?


重分配过程分为四步。首先,分配一块新的足够存储容器的内存缓冲。第二,将已存在的元素拷贝到新的内存缓冲。第三步,以前存储元素容器的销毁器被调用。最后原来的内存缓冲被释放。显然,重分配是开销很大的操作。你可以通过调用成员函数reserve()来避免重分配。reserve(n)保证容器预分配足够内存来存储至少n个元素,象下面的例子:



class Message { /*...*/};
#include <vector>
using namespace std;
int FillWithMessages(vector<Message>& msg_que); //严格时间要求
int main()
{
vector <Message> msgs;
//在进入时间临界区之前,将空间内填满1000 Messages
msgs.reserve(1000);
//在1000个对象存储到vector之前,不需要重分配
FillWithMessages(msgs);
return 0;
}

capacity()和size()


capacity ()返回容器不需重分配就能存储的元素的总数量。size()返回容器现在存储元素的数量。换句话说,capacity()-size()是不重分配容器中可用“空插口”的总数。可以通过显式的调用reserve()或resize()来改变容器的大小。这些成员函数在概念上不同。resize(n)分配n个对象的内存并默认的初始化他们(你可以提供不同的初始化值作为第二个可选参数)。


reserve()分配生鲜内存而不初始化它。另外,reserve()不改变size()返回的值,它只改变
capacity()返回的值。resize()都改变。例如



#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector <string> vs;
vs.reserve(10); //得到至少10个strings的空间
vs.push_back(string()); //插入元素
cout<<"size: "<< vs.size()<<endl; //输出:1
cout<<"capacity: "<<vs.capacity()<<endl; //输出:10
cout<<"there's room for "<<vs.capacity() - vs.size()
<<" elements before reallocation"<<endl;
//分配10个以上元素,通过string::string()初始化
vs.resize(20);
cout<<"size: "<< vs.size()<<endl; //输出 20
cout<<"capacity: "<<vs.capacity()<<endl; //输出20;
return 0;
}

通过构造器来指定容器的容量


迄今为止,本章的例子都是使用显式操作reserve()或resize()来预分配存储空间。而然,也可以通过构造器来指定所需的存储空间。



#include <vector>
using namespace std;
int main()
{
vector<int> vi(1000); //initial storage for 1000 int's
//vi包含1000元素,通过int::int()初始化
return 0;
}

记住:reserve()分配生鲜内存而不初始化它。另一方面,构造器调用默认构造器来初始化分配的元素。指定不同的初始化值也是可能的,比如:



vector<int> vi(1000, 4); //初始化1000整型数据为4

访问单一元素


重载运算符[]和成员函数at()使直接访问vector的元素成为可能。因为同时有const和非const
版本,所以他们可分别用于访问const元素和非const元素。


重载的[]运算符设计的和内建版本一样有效率。因此,[]也不会检查是否引用的是有效的元素。由于缺乏运行期检查保证了最快的访问速度(运算符[]的调用通常是内联的)。但是非法的下标的运算符[]是未定义行为。当性能十分重要,当代码写的很仔细不可能有非法下标时,使用[]运算符。[]符号也更易读更直观。尽管如此,运行期检查在有些情况下也是不可避免的,例如当下标值是从外部源得到的:函数、数据库记录、或输入的。在这种情况下你要使用成员函数at ()来代替运算符[]。at()执行范围检查,并且在试图访问范围以外成员时它抛出类型std::out_of_range
的异常。这里时一个例子:



#include <vector>
#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;
int main()
{
vector<string> vs; //vs现在没有一个元素
vs.push_back("string"); //增加第一个元素
vs[0] = "overriding string"; //重载运算符[]
try
{
cout<< vs.at(10) <<endl; //超出范围的元素,抛出异常
}
catch(std::out_of_range & except)
{
//下标越界的处理程序
}
}//end main

Front和Back操作


Front 和back操作分别引用容器开始和结尾。成员函数push_back()在容器的结尾附加一个元素。当容器耗尽它的空闲内存时,函数重分配内存再附加元素。成员函数pop_back()将最后一个元素移出容器。成员函数front()和back()分别访问容器开始和结尾的一个元素。front()和 back()有const和非const版本。例如



#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector <short> v;
v.push_back(5);
v.push_back(10);
cout<<"front: " << v.front() << endl; //5
cout<<"back: " << v.back() << endl; //10
v.pop_back(); //remove v[1]
cout<<"back: " << v.back() << endl; //now 5
return 0;
}

容器赋值


STL容器重载赋值运算符,因此允许同类型的容器互相赋值,例如



#include <iostream>
#include<vector>
using namespace std;
int main()
{
vector <int> vi;
vi.push_back(1);
vi.push_back(2);
vector <int> new_vector;
//拷贝vi的内容到new_vector,自动增长到合适大小
new_vector = vi;
cout << new_vector[0] << new_vector[1] << endl; //显显示1和2
return 0;
}

Vectors的邻接区域


C+ +内建的数组居住在连续的大块内存中。但是,标准并没有要求vector元素占用连续的内存。当STL加入标准时,好像凭直觉vectors应该连续的存储它的元素,以致邻近从不是显式的要求。实际上,现在所有STL的实现都将vectors连续化了。当然现在的规范允许用不连续内存来实现。标准化委员会将来可能修正这个漏洞,vectors的连续化将变成标准的要求。


一个vector<Base>不能存储Derived对象


每一个vector元素丢必须是同样的大小。因为派生对象可能有额外的成员,它的大小可能比基类要大。不要将派生对象存储在vector<基类 >中,因为这将导致未定义的向上对象转换(object slicing)。但是你可以通过存储派生对象指针到vector<基类*>来达到多态。


FIFO对象模型


在队列数据模型中(一个队列也叫FIFO先进先出),插入的第一个元素在顶部,其后插入的元素都在其后。队列的两个基本操作是pop()和push()。push()操作将一个元素插入到队列的底部。pop()操作从顶端移出第一次
插入的元素;从而使第二个元素成为顶端元素。STLqueue容器象下面这样使用:



#include <iostream>
#include <queue>
using namespace std;
int main()
{
queue <int> iq;
iq.push(93); //插入第一个元素,它是最顶部的元素
iq.push(250);
iq.push(10); //最后一个插入的元素在底部
cout<<"currently there are "<< iq.size() << " elements" << endl;
while (!iq.empty() )
{
cout <<"the last element is: "<< iq.front() << endl; //front()返回
//顶部元素
iq.pop(); //移出顶部元素
}
return 0;
}

STL也定义了双向队列,或deque(pronounced "deck")容器。deque是能有效处理尾部的队列。其他类型的队列有priority_queue。priority_queue的所有元素以其优先权排序。有高有限权的元素在顶部。为了取得作为priority_queue元素的资格,对象必须定义<运算符(priority_queue在“函数对象”一节讨论)。


迭代器


Iterators可以认为是一种通用指针。他们用于操作容器的元素而不需要知道实际元素类型。许多成员函数,比如begin()和end(),返回一个指向容器尾的迭代器。


begin()和end()


所有的STL容器提供begin()和end()成员函数。begin()返回指向容器第一个元素的迭代器。例



#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector <int> v(1); //单一元素的空间
v[0] = 10;
vector<int>::iterator p = v.begin(); //p指向v的第一个元素
*p = 11; //通过p将一个新的值赋给v[0]
cout << *p; //输出11
return 0;
}

另一方面成员函数end()返回一个指向位于容器最后一个元素之后(past)的迭代器。第一次听到时很不可思议,但是如果你考虑到C风格的字符串是如何表示的,这就不是什么不可理解的事:一个附加的null字符被自动附加到char 数组最后一个元素的后面。在STL中附加元素起同样的作用,标记容器的结尾。让end()返回指向容器最后一个元素之后的迭代器对于for和while循环很有用。例如



vector <int> v(10);
int n=0;
for (vector<int>::iterator p = v.begin(); p<v.end(); p++)
*p = n++;

begin()和end()有两种版本:const和非const。非const版本返回非const 迭代器,它允许用户修改容器元素的值。const版本返回const 迭代器,它不允许用户修改容器


例如



const vector <char> v(10);
vector<char>::iterator p = v.begin(); //错误,必须使用const_iterator
vector<char>::const_iterator cp = v.begin(); //OK
*cp = 'a'; //error,试图改变const对象
cout << *cp; //OK

成员函数rbegin()和rend()(降序begin()和降序end())与begin()和end()十分相似,只是返回降序迭代器,它们适用于降序序列。本质上说,降序迭代器就是普通迭代器,只不过所重载的运算符++不同--。需要降序访问容器的时候,是很有用的。


例如



#include <iostream>
#include <vector>
#include <string>
using namespace std;
void ascending_order()
{
vector <double> v(10);
double d = 0.1;
for (vector<double>::iterator p = v.begin(); p<v.end(); p++) //初始化
{
*p = d;
d+= 0.1;
}
//以升序显式v的元素
for (vector<double>::reverse_iterator rp = v.rbegin(); rp < v.rend(); rp++)
{
cout<< *rp<<endl;
}
}

与begin()和end()一样rbegin()和rend()也有const和非const
版本


迭代器的底层实现


STL 的大多数实现使用指针来实现迭代器。但是一个迭代器不需要一个指针,并且有一个很好的理由。考虑一个存储在6GB磁盘上扫描图象的巨大vector;大多数机器上内建的指针只有32位不够来表示这种巨大的vector。代替指针的一种方案是用64位整数来实现迭代器。同样的,保存位或半元组(nibbles )(内建指针不能引用)也可以通过不同的底层类型的迭代器来来实现,并提供相同的接口然而,有时候纯指针也用于迭代特定实现的容器的元素;例如



#include <vector>
#include <iostream>
using namespace std;
void hack()
{
vector<int> vi;
vi.push_back(5);
int *p = vi.begin();//糟糕的程序设计习惯,尽管它可以工作
*p = 6; //赋值vi[0]
cout<<vi[0]; //输出6(可能)
}

使用纯指针来代替迭代器是一种糟糕的程序设计习惯,应该避免。


迭代器的“const Correctness”


当元素可以被访问但不能改变时,使用容器的const迭代器。作为普通指针类型,使用非const迭代器暗示容器的内容是可改变的。const使编译器可以检测到简单的错误,也更可读。


通过内建数组来初始化Vector的内容


就像前面说的,内建数组是一种有效的序列容器。因此,可以通过数组首尾的地址可以用内建数组的内容来初始化一个vector。例如



#include<vector>
#include <iostream>
using namespace std;
int main()
{
int arr[3];
arr[0] = 4; arr[1] = 8; arr[2] = 16;
vector <int> vi ( &arr[0], // 数组开始地址
&arr[3] ); //必须指向数组最后一个元素之后的元素
cout<< vi[0] << '/t' << vi[1] << '/t' << vi[2] <<endl; //输出4 8 16
return 0;
}

迭代器的失效


当成员函数改变它的容器时将发生重分配。可能改变的成员函数是reserve()和resize(),push_back()和pop_back(), erase(),clear(),insert()和其他。另外,赋值运算符和可能改变的算法也可能导致重分配。当容器重分配它的元素是,他们的地址改变了。因此,存在的迭代器的值无效了。


例如



#include <iostream>
#include <list>
using namespace std;
int main()
{
list <double> payroll;
payroll.push_back(5000.00);
list<double>::const_iterator p = payroll.begin(); //指向第一个元素
for (int i = 0 ; i < 10; i++)
{
payroll.push_back(4500.00); //向payroll插入10以上元素;
//将产生重分配
}
//危险
cout << "first element in payroll: "<< *p <<endl; //p可能无效
return 0;
}

在前面的例子中,payroll可能在插入十个元素的过程中发生重分配,因此p的值可能失效。使用失效的迭代器就像使用指向已删除对象的指针一样,都是为定义行为。为了安全起见,推荐你在调用了可能改变的函数之后因该重赋值迭代器。例如



list<double>::const_iterator p = payroll.begin();//指向第一个元素。
for (int i = 0 ; i < 10; i++)
{
payroll.push_back(4500.00); //可能发生重分配
}
p = payroll.begin(); //重赋值p
cout <<"first element in payroll: "<<*p<<endl; //现在安全了
}

作为选择,你可以通过在实例化预分配足够的内存来避免重分配。例如



int main()
{
list <double> payroll;
payroll.reserve(11);
payroll.push_back(5000.00);
list<double>::const_iterator p = payroll.begin();
for (int i = 0 ; i < 10; i++)
{
payroll.push_back(4500.00); //不会发生重分配
}
cout << "first element in payroll: "<< *p <<endl; // OK
return 0;
}

算法


STL 定义了适用与容器和其他序列的丰富的泛型算法的集合。主要有三种类别的算法:不改变序列的操作、改变序列的操作和排序算法。


不改变序列的操作


不改变序列的操作是不直接改变序列的操作。他们包括查找、检查一致性和计数。


find()算法


泛型算法find()查找一个元素在序列中的位置。find()接受三个参数。头两个是指向序列开头和结尾的迭代器。


第三个参数是要查找的值。find()返回指向第一个与要查找值相匹配元素的迭代器。如果find()不能找到要求的值,它返回指向最后一个元素之后一个位置的迭代器(就是说返回值和end()一样)。例如



#include <algorithm> //find()的定义
#include <list>
#include <iostream>
using namespace std;
int main()
{
list<char> lc;
lc.push_back('A');
lc.push_back('T');
lc.push_back('L');
list<char>::iterator p = find(lc.begin(), lc.end(), 'A'); // find 'A'
if (p != lc.end()) //找到'A'?
*p = 'S'; // 找到后用'S'替换
while (p != lc.end()) //显示改变后的列表
cout<<*p++;
return 0;
}

改变序列的操作


改变序列的算法改变序列的值。他们包括拷贝、填充、替换和变形。


copy()算法


标准库提供了一个泛型拷贝算法,可以用于将一系列对象拷贝到特定的目标。copy()的第一个和第二个参数是标志序列开始和结尾的const迭代器。第三个参数指向要拷贝到的容器。下面的例子示范了拷贝一个list的元素到一个vector:



#include <algorithm>
#include<list>
#include<vector>
using namespace std;
int main()
{
list<int> li; vector <int> vi;
li.push_back(1);
li.push_back(2);
vi.reserve( li.size() ); //必须为要拷贝的元素先分配空间
//拷贝list的元素到vector,从vector的头开始
copy (li.begin(), li.end(), vi.begin() );
return 0;
}

排序操作


这种类型包含了用于排序、合并序列的算法和操作已排序序列类集合算法。包括sort()、partial_sort()、
binary_search()、lower_bound()和许多其他算法。


sort()算法


sort()接受两个指向序列开始和结尾的const类型迭代器。一个可选的第三方算法是断言对象(predicate object),它改变sort的估计(断言对象和适配器马上讨论)。例如



#include <iostream>
#include <algorithm> //sort()的定义
#include <vector>
using namespace std;
int main()
{
vector <int> vi;
vi.push_back(7);
vi.push_back(1);
vi.push_back(19);
sort(vi.begin(), vi.end() ); //排序vi;默认以升序
cout<< vi[0] <<", "<< vi[1] <<", "<< vi[2] <<endl; //输出:1、7、19
return 0;
}

一种使用降序的方法是使用降序迭代器:



sort(vi.rbegin(), vi.rend() ); //以降序重排序
cout<< vi[0] <<", "<<vi[1]<<", "<<vi[2]<<endl; //输出:19、7、1

排序容器的要求


当sort()操作在容器中时,它使用的相关操作是==和容器元素的<。不支持这些操作的用户定
义类型仍然存储在容器中、但是这样的容器不能排序。


函数对象


使用函数指针来调用回调例程是一种习惯。虽然如此,在面向对象环境中可以被封装在函数对象中(function object)(参见第三章“运算符重载”)。使用函数对象来代替函数指针有许多许多优点。函数对象更灵活,因为包含函数的对象可以不影响它的用户就可以方便的修改。另外,编译器可以内联函数对象,对于函数指针这几乎是不可能的。但是函数对象带来的最引人注目的
一点可能是泛型函数对象可以通过成员模板来实现泛型算法。


函数对象的实现


函数对象重载了函数调用运算符。泛型函数对象将重载的函数调用运算符定义为成员函数模板。因此,对象可以象一个函数调用一样使用。记住,重载运算符()可以有可变数目的参数并可以返回任何值。在下面的例子中,函数对象实现了泛型变好操作:



#include <iostream>
#include <vector>
using namespace std;
class negate
{
public : //泛型变号操作
template < class T > T operator() (T t) const { return -t;}
};
void callback(int n, const negate& neg) //传递函数对象而不是函数指针
{
n = neg(n); //调用重载()运算符将n变号
cout << n;
}
int main()
{
callback(5, negate() ); //输出:-5
return 0;
}

函数对象的用途


许多容器操作使用函数对象。例如,priority_queue使用less韩素对象来将内部元素排序。下面的例子示范了存储任务不同的优先权在一个 priority_queue中的调度程序。高优先权的任务在顶部。优先权相同的任务的位置以其插入的顺序而定,和普通队列一样:



#include <functional> //less的定义
#include <queue> //priority_queue的定义
#include <iostream>
using namespace std;
struct Task
{
int priority;
friend bool operator < (const Task& t1, const Task& t2);
Task(int p=0) : priority(p) {}
};
bool operator < (const Task& t1, const Task& t2)
{
return t1.priority < t2.priority;
}
int main()
{
priority_queue<Task> scheduler;
scheduler.push(Task(3));
scheduler.push(Task(5));
scheduler.push(Task(1));
scheduler.push(Task(1));
cout<< scheduler.top().priority <<endl; //输出5
return 0;
}

断言对象


断言是一个返回布尔值的表达式。同样的,返回布尔值的函数对象是断言对象(predicate object)。STL 定义了许多用于改变泛型算法行为的断言对象。这些断言对象在头文件<functional>中定义。在全面的例子中,你看见过算法sort ()。sort()的第三个参数就是一个改变算法行为的断言。例如,断言greater<int>能用来代替默认的升序。同样的,断言 less<int>用来恢复原来的升序:



#include <functional> //STL断言的定义
#include <algorithm> //sort的定义
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <int> vi;
vi.push_back(9);
vi.push_back(5);
vi.push_back(10);
sort(vi.begin(), vi.end(), greater<int> () ); //降序
cout<< vi[0] << '/t' << vi[1] << '/t' << vi[2] <<endl; //输出:10 9 5
sort(vi.begin(), vi.end(), less<int> () ); //现在是升序 now in ascending order
cout<< vi[0] << '/t' << vi[1] << '/t' << vi[2] <<endl; //输出: 5 9 10
return 0;
}

适配器


适配器是用来改变其他组件接口的组件。STL使用许多类型的适配器:序列适配器、迭代器适配器和函数适配器。


序列适配器


序列适配器是建立在其他容器之上并改变容器接口的的容器。例如,容器stack经常是作为一个deque实现的,,它的非stack操作被隐藏起来。另外, stack使用操作back()、push_back()和deque的pop_back()来分别实现操作top()、push()和pop()。例如



#include <string>
#include <stack>
#include <iostream>
using namespace std;
int main()
{
stack <string> strstack;
strstack.push("Bjarne");
strstack.push("Stroustrup");
string topmost = strstack.top();
cout<< "topmost element is: "<< topmost << endl; // "Stroustrup"
strstack.pop();
cout<< "topmost element is: "<< strstack.top() << endl; // "Bjarne"
return 0;
}

调用一个空stack的成员函数pop()是错误的。如果逆不能肯定堆栈是否有元素,逆可以先使用成员函数empty()
来检查它。例如



stack<int> stk;
//...其他代码
if (!stk.empty() ) //在弹出之前检查stack是否空
{
stk.pop();
}

迭代器适配器


迭代器的接口可以被迭代器适配器(iterator adaptor)改变。成员函数rend()和rbegin()返回
降序迭代器,这种迭代器的操作++和--的含义改变了。使用降序迭代器在许多行为中是很有用的。


函数适配器


早先你看到了用于改变sort()行为的greater函数适配器。STL也提供negators,它用于对特定布尔操作结果的取反。Binders是另一种类型的适配器,它通过将一个参数绑定为特定值来将二元函数对象转换成一元函数对象。


分配器(Allocators)


每一个STL容器使用分配器来封装程序使用的内存模型。分配器隐藏依赖平台的细节,比如指针大小、内存组织、重分配模型和内存页的大小。因为容器可以与不同类型的分配器一起工作,它可以轻易在不同环境中工作,只需插入不同的分配器就行了。一个实现为每一个容器提供适当的分配器。一般情况下,用户不需要代替默认的分配器。


专用容器


第九章,“模板”讨论了定义函数特殊化的好处,可以优化和调整主模板用于特殊类型的行为。vector为操作布尔值而优化的特殊化叫vector<bool>。这种特殊化是通过将元素压缩成一个位来实现的,而不是bool变量,虽然
有与vector相同的接口。例如



#include <vector>
#include <iostream>
using namespace std
void transmit(vector <bool> &binarystream)
{
cout<<binarystream[0]; //提供下标运算符
vector<bool>::const_iterator bit_iter = binarystream.begin(); //迭代器
if (binarystream[0] == true)
{/* do something */ }
}

联合容器(Associative Containers)


联合数组(associative array)是索引可以不是整数的数组。联合数组页叫mapdictionary。STL 定义了许多联合容器。例如map,存贮对值;一个作为关键字,另一个是关联值。模板pair<class Key, class Value>作为map的元素。下面的例子中,map用于转换字符串的枚举器到相应的整数值。字符串是关键字它的关联值是
int:



#include <map>
#include <string>
#include <iostream>
using namespace std;
enum directions {up, down};
int main()
{
pair<string, int> Enumerator(string("down"), down); //创建对
map<string, int> mi; //创建map
mi.insert(Enumerator); //插入对
int n = mi["down"]; //n = 1 //字符串用于下标
return 0;
}

一个map只能存储独有的关键字。multimap是能存储相同关键字的map。


set类似与map只是其关联值是不相关的。仅当关键字是重要时,才用set:例如保证数据库事务系统不要试图插入一个关键字在表中以存在的的记录。multiset是允许同样关键字的set。


类auto_ptr


类模板auto_ptr实现“资源获取时初始化(resource acquisition is initialization)”习惯(在第五章“面向对象的编程和设计”)。它通过指向新分配对象的指针来初始化(auto_ptr有默认构造器,所以你可以实例化一个空的auto_ptr之后再将一个指针赋值给他)。auto_ptr的销毁器销毁越界的对象。这种技术可以避免在异常的情况下出现内存泄漏(见第六章“异常处理”),或者可以用来简化传统的必须显式的销毁每一个动态分配对象的程序设计方法。类auto_ptr在标准头文件< memory>重申明。下面是一个使用auto_ptr的例子(对象可能的指针是可数的):



#include <memory>
using namespace std;
void f() { if (condition) throw "err";}
int main()
{
try
{
auto_ptr<double> dptr(new double(0.0));
*dptr = 0.5; //重载* 提供类似指针的语法
f();
} // 1: 没有异常抛出,dptr将在这里销毁
catch(...)
{ // 2: 抛出异常,dptr在这里销毁
}
return 0;
}

它保证了动态分配的内存能正确的释放:如果f()抛出异常,dptr对象在堆栈释放(2)期间被销毁,结果动态分配的内存被释放。否则,当try块结束的时候,dptr被销毁(1)。


STL容器不能存储auto_ptr元素


STL容器的元素必须是可拷贝构造和可赋值的,前面以提到过。在重分配的时候,容器在新的内存中拷贝构造它的元素,并通过调用销毁器来销毁原来的对象。然而,auto_ptr是不可拷贝构造的。尽管。它提供严格所有权关系(strict ownership)语义,这意味着它对保存指针的对象有所有权(所有权关系也在第五章讨论)。拷贝一个auto_ptr对象就要拷贝指针和其所有权到目的地。这与可拷贝构造和可赋值的要求刚好背道而驰:一份auto_ptr的拷贝保存指向空闲内存的对象,尽管其他拷贝没有。(如果多个auto_ptr对同一对象有所有权,结果是未定义的。)因此,auto_ptr对象不能存储在STL容器中。


类容器(Nearly Containers)


STL 定义了三个附加组件,在许多方面他们的行为和普通容器很相似。他们有自动的内存管理,他们有迭代器,他们也共享类似容器接口的成员函数,比如begin ()和end()。尽管如此,他们在STL目录中他们不被认为是“一等公民(first-class citizens)”,因为他们不是泛型的。string类似于vector但是限制于char数据类型。valarray类似vector,尽管其主要用于数值计算。目录中的第三个类是bitset,它是为了提高存储和操作位串的效率而定义的。这些类容器用途有限。除了string之外。


类string


std::string是std::basic_string<char>的简写,就象你在第九章看到的。string提供
许多普通STL容器的操作;例如它符合序列的要求并且也定义了迭代器。但是,string为字符操作了优化。


设计string中考虑了最好的效率,支持C字符串和一般性(就是说,string并不是为特定应用程序而设计的)。


构造器


string有一个默认构造器和5个以上构造器:



namespace std
{
template<class charT, class traits = char_traits<charT>,
class Allocator = allocator<charT> >
class basic_string {
public:
//...
explicit basic_string(const Allocator& a = Allocator());
basic_string(const basic_string& str, size_type pos = 0,
size_type n = npos, const Allocator& a = Allocator());
basic_string(const charT* s,
size_type n, const Allocator& a = Allocator());
basic_string(const charT* s, const Allocator& a = Allocator());
basic_string(size_type n, charT c, const Allocator& a = Allocator());
template<class InputIterator>
basic_string(InputIterator begin, InputIterator end,
const Allocator& a = Allocator());
//...
};
}

换句话说,string可以通过C字符串、通过其他string对象、通过C字符串的部分、通过字符序列、通过其他string的部分来初始化。下面是例子



#include <string>
using namespace std;
void f()
{
const char text[] = "hello world";
string s = text; //用C字符串来初始化string对象
string s2(s); //拷贝构造器
string s3(&text[0], &text[5]); //C字符串的部分;s3 = "hello"
string s4(10, 0); //用一个0的序列来初始化
string s5 ( s2.begin(), s2.find(' ')); //通过其他string的部分来初始化
//s5 = "hello"
}

注意:当用char的指针初始化string,不会检查指针。保证指针有效而且不是NULL是程序员
的责任。否则结果是未定义的。例如



#include<string>
using std::string;
const char * getDescription(int symbol); //可能返回NULL指针
string& writeToString (int symbol)
{
//马虎:可能是NULL;将是未定义行为
string *p = new string(getDescription(symbol));
return *p;
}

string 为了避免性能上的开销,并不检查NULL指针。标准C函数比如strcpy()也不检查指针。即使string检查指针,在碰到NULL的情况下也是不明确的。显然,NULL值不是一个有效的C字符串,所以创建一个空的string是不正确的。抛出异常可能是一个似是而非的方法,但是这导致额外的运行期开销而且并不总是需要的。


简单的实现是在初始化之前保证指针不是NULL:



string& writeToString (int symbol)
{
const char *p = getDescription(symbol);
if (p) //现在安全了
{
string *pstr = new string(p);
return *pstr;
}
return *new string;
}

转换成C字符串


类string提供了两个成员函数返回其const char *表示。下面的章节讨论这些成员函数。


c_str()成员函数


string 没有定义char *转换运算符。有两个原因。第一,当你不希望转换时,隐含的转换可能导致不受欢迎的惊讶(引第三章)。其二,C字符串必须是以null结尾的。string的底层表示是依赖于编译器的,并不一定是以null结尾的序列。因此,隐含的string到以null结尾字符串的隐含转换可能是灾难。由于这些原因,string不提供转换运算符。作为代替的是调用string::c_str()。c_str()返回对象的const char *表示。例如



void f()
{
string s = "Hello";
if( strcmp( s.c_str(), "Hello")== 0)
cout <<"identical"<<endl;
else
cout<<"different"<<endl;
}

从c_str ()返回的指针归string对象所有。用户不能试图delete它或改变与之关联的char数组。
在一个非const成员函数调用之后,返回的指针就不可用了。


data()成员函数


成员函数data()也返回对象的const char *表示(但是结果数组可以不是null结尾的。)


访问单一元素


有两种访问string对象单一元素的方法。第一种是使用重载运算符[],就像下面的例子一样:



#include <string>
using namespace std;
void first()
{
string s = "hello world";
char c = s[0]; //赋值 'h'
}

另一种方法是使用成员函数at()。与类vector类似,string::at()执行范围检查,并且当越界时
抛出类型为std::out_of_range的异常。


清除string的内容


为了显式的删除string的内容,你可以时用成员函数erase()。例如



#include <iostream>
#include <string>
using namespace std;
void f()
{
char key;
string msg = "press any key to continue";
cout<<msg<<endl;
cin<<key;
msg.erase(); //清除msg
}

比较


string定义了三个版本的==运算符:



bool operator == (const string& left, const string right);
bool operator == (const char* left, const string right);
bool operator == (const string& left, const char* right);

这样繁多好像是多余的,因为string有自动将const char *转换成string对象的构造器。因此,只有第一个版本的==是必须的。但是,在有些情况下创建临时string对象的开销是不可接受的:临时 string对象在空闲内存中分配空间,拷贝C字符串再释放内存。标准委员会的意图是使string的比较尽可能的有效率。因此,运算符==额外的版本是为了使比较更有效率而增加的。


附加重载运算符


前面提到,string可以由其他string、C字符串或单一字符来赋值。类似的,string的+=也有三个版本来支持串连其他string、C字符串和单一字符。例如



#include <string>
using namespace std;
void f()
{
string s1 = "ab"
string s2= "cd";
s1+=s2;
s1+= "ef";
s1+='g';
}

string也定义了重载运算符+来连接两个字符串。类似的,运算符<和>来对操作数
进行字典比较。


性能问题


string 大概是C++程序中使用最广泛的类了。它设计和实现的效率是十分重要的。例如,string提供了最优的容器操作,比如专为字符操作设计的find()、 copy()和replace()。有些情况下,string对象在速度和空间上比char *更优效率。下面的章节讨论了string两方面的性能:大小计算和引用计数。


大小


string有一个保存大小的数据成员。因此,计算string对象的大小是很快的常量时间操作。而执行strlen()的时间是O(n)。当使用大字符串和要频繁计算大小时,std::string比C字符串更有效率。


引用计数


在nutshell 中,引用计数模型对有一样状态类实例的个数进行计数。当两个或多个实例共享同样状态时,仅创建一个拷贝并记录以存在引用该拷贝的数目。例如,一个 string数组可以通过单个包括数组元素个数的string对象来表示(引用计数并不限于数组)。刚初始化的时候数组元素共享同一状态(他们都是空的string),仅需要一个对象。当一个数组元素改变它的状态时(例如,如果赋给其不同的值),存在的对象创建多个对象,调用"copy on write"。就像你看到的,应用计数模型可以提高内存使用和速度两方面的性能。标准关于类string的规范阐明:允许但是不强制使用引用计数。


引用计数实现和非引用计数实现必须有相同的语义。例如



string str1("xyz");
string::iterator i = str1.begin();
string str2 = str1;
*i = 'w'; //必须只修改str1

总结


STL用来取得最大的可重用性而不牺牲效率。标准指定了STL容器和算法必须遵循的性能要求。这些性能要求的指定是最低的;编译器实现时可能更好。


STL组件良好的兼容性是值得注意的,它使得用户可以创建其他组件或修改存在组件的接口。其他frameworks和库对用户使用其组件强加了许多要求,限制了兼容性。


STL被C++死亡创立者认为是最近几年加入语言的最重要的特性。掌握STL是值得作的一件事。估计其他程序设计语言也将以STL为样板来提供类似的泛型frameworks。STL的三个主要优点是




  • 可移植性所有支持标准的C++编译器都支持他们。


  • 性能 STL组件的设计和实现都有严格的效率要求。


  • 可靠性 STL容器和算法已经过了debug和测试。

原创粉丝点击