Effective STL:1 - 12 容器

C++容器选择与使用最佳实践
本文深入探讨了C++容器的选择与使用技巧,包括如何根据特定需求选择合适的数据结构,注意事项,以及如何提高代码的性能和可维护性。

容器比数组功能强大在于:可以动态增长(或者缩减),可以自己管理内存,可以记住自己包含了多少个对象。


1.慎重选择容器类型。

容器可以分为连续内存容器(contiguous-memory container)和基于节点的容器(node-based container)。

连续内存容器有vector,string,deque。

基于节点的容器有list,还有所有的标准关联容器。


C++标准提供的建议:

1)vector是默认应使用的序列类型。

2)当需要频繁地在序列中间做插入和删除操作时,应使用list。

3)当大多数插入和删除在序列的头部和尾部是,deque是应该考虑的数据结构。


此外其他需要考虑的因素有:

1)是否需要在容器任意位置插入元素?如果需要,选择序列容器,关联容器不行。

2)需要的是哪种类型的迭代器?如果必须是随机的,则容器选择就被限定为vector,deque和string。

3)容器的数据布局是否要和C兼容?如果需要,只能选择vector!

4)  发生元素的插入或者删除的操作时,避免移动容器原来的元素是否很重要?如果是,避免连续内存的容器。

5)对插入和删除,是否需要事务语义?也就是说,在插入和删除失败的时候,是否需要回滚的能力?如果需要,要使用基于节点的容器!

6)需要使迭代器、指针、和引用变为无效的次数最少吗?如果是这样,就要使用基于节点的容器。这类容器从来不会使它们无效。而针对连续内存容器的插入和删除一般会使指向该容器的迭代器、指针和引用变为无效。


2.不要试图编写独立于容器的代码。

STL是以泛化(generalization)原则为基础的。数组被泛化为容器,指针被泛化成迭代器,函数被泛化成算法。很多程序员想把容器的概念泛化,虽然出发点是好的,但是这样的泛化毫无意义。


有时候不可避免的要从一种容器转到另一种,可以使用常规的方式来实现这种转变:使用封装技术。最简单的方式是通过对容器类型及其迭代器类型使用类型定义(typedef)。这种方法也可以节省很多编程的工作量。因此不要这样写:

class Widget{};

vector<Widget> vw;

Widget bestWidget;

……

vector<Widget>::iterator i = find(vw.begin(),vw.end(),bestWidget)

而要这样写:

class Widget{};

typedef vector<Widget> WidgetContainer;

typedef WidgetContainer::iterator WCIterator;

WidgetContainer cw;

Widget bestWidget;

……

WCIterator i = find(cw.begin(),cw.end(),bestWidget)


如果不想把自己的选择的容器暴露给客户,可以使用类。

如果需要创建一个顾客列表,不要直接使用list,相反可以创建一个CustomerList类,并把list隐藏在其似有部分:

class CustomerList{

private:

   typedef list<Customer> CustomerContainer;

   typedef CustomerContainer::iterator CCIterator;

   CustomerContainer customer

public:

……    //尽量减少那些通过该接口可见,并且与list相关的信息。

}


3.确保容器中的对象拷贝正确而高效。

STL的工作方式就是各种拷贝,如果向容器中填充对象,而对象的拷贝操作又很费时,那么向容器中填充对象这一简单的操作将会成为程序性能的瓶颈。放入容器中的对象越多,拷贝所需要的内存和时间也越多。


向基类容器插入派生类对象时,派生类的信息将会丢失。这种“剥离”问题意味着向基类容器插入派生类对象总是错误的。


如果想使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法就是使容器包含指针而不是对象


4.调用empty()而不是检测size()是否为0。

理由很简单,empty()对所有的标准容器都是常数时间操作,而对一些list实现,size()耗费线性时间。而list不提供常数时间的size()函数在于其独有的splice操作。

下面这个例子把list2中第一个含有5的节点到最后一个含有10的所有节点移动到list1的末尾。

list<int> list1;
list<int> list2;

list1.splice(list1.end(),list2,
	     find(list2.begin(),list2.end(),5),
	     find(list2.rbegin(),list2.rend(),10).base()
	     );



5.区间成员函数优先于与之对应的单元素成员函数。

当需要完全替换一个容器的内容时,应该先想到赋值。如果想把一个容器拷贝到相同类型的另一个容器时,那么operator=是可以选择的赋值函数,而如果想给容器一组全新的值时,可以使用assign()函数。


所有利用插入迭代器的方式来限定目标区间的调用,其实都可以(也应该)被替换为对区间成员函数的调用。如原来使用copy()函数,现在都可以调用insert()函数。所以,如果看到使用push_front()或者push_back()的循环调用,或者front_inserter或者back_inserter被作为参数传递给copy函数时,采用insert()函数是更好的选择。


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

先从最基本的说起。下面这行代码声明了一个带double参数并返回int的函数:

int f(double d);

下面这行做了同样的事情,参数d两边的括号是多余的,会被忽略:

int f( double (d) );

下面这行声明了同样的函数,只是它省略了参数的名称:

int f(double)


现在再介绍一下函数指针:

int (*pf)():表示pf是一个指向函数入口的指针变量,该函数返回类型是整型,形参为空。关于函数指针和指针函数可以看下面一个链接。最简单的辨别方式就是看函数名前面的指针*号有没有被括号()包含,如果被包含就是函数指针,反之则是指针函数。


例如下面这个函数指针:

void (*fptr)();

把函数的地址赋值给函数指针,可以采用下面两种形式:

fptr=&Function;

fptr=Function;

取地址运算符&不是必需的,因为单单一个函数标识符就标号表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。
可以采用如下两种方式来通过指针调用函数:

x=(*fptr)();

x=fptr();


介绍完函数指针之后,再看下面三个函数声明:

下面这行代码声明了一个函数g,它的参数是一个指向不带任何参数的函数的函数指针,该函数返回double值。

int g(double (*pf)())

如果按照上面的理解, (*pf)()可以替换成pf(),所以上面的函数声明可以有另外一种表达方式:

int g( double pf() )  //pf为隐式指针

如果把参数名称省略,则下面则是g的第三种声明:

int g(double () )    //省去参数名


所以list<int> data(istream_iterator<int>(dataFile),

                                istream_iterator<int>())代表这样的一个函数:

1.返回值是list<int>类型,函数名为data,共两个参数。

2.第一个参数名称是dataFile,它的类型是istream_iterator<int>。dataFile两边的括号是多余的,可以忽略。

3.第二个参数没有名称。它的类型是一个指向不带参数的函数指针,参数名已经省去了,函数的返回类型是istream_iterator<int>。


解决方法就是给函数的参数加括号,如上面的,可以改成这样:

list<int> data(  (istream_iterator<int>(dataFile) ), istream_iterator<int>())


7.如果容器中包含了通过new创建的指针,切记在容器对象析构之前将指针delete掉。

先看下面一段代码:

void dosomething()
{
	vector<Widget*> vwp;
	for(int i = 0; i < SOME_MAGIC_MUMBER; ++i)
		vwp.push_back(new Widget);
	……
}
当vwp的作用域结束时,它的元素全部被析构,但是这并没有改变通过new创建的对象没有被删除这一事实。


除了用for循环依次释放之外,还可以用for_each()函数来释放对象。如:

template<typename T>
class DeleteObject
{
public:
	void operator()(const T* ptr) const
	{
		delete ptr;
	}
};
调用则为:

for_each(vwp.begin(),vwp.end(),DeleteObject<Wdiget>());

值得注意的是,因为使用了模板,所以必须指明DeleteObject要删除的对象类型。除了要显示指定类型之外,还有可能导致难以追踪的错误。如string没有虚析构函数,如果一个类继承string就很危险,因为 对没有虚析构函数的类进行公有继承是C++的一项重要禁忌


通过让编译器推断出传给DeleteObject::operator()的指针类型,我们可以消除这个错误。只需要把模板化从DeleteObject移动到它的operator()中:

class DeleteObject
{
public:
	template<typename T>
	void operator()(const T* ptr) const
	{
		delete ptr;
	}
};
这样的话编译器会自动知道传递给DeleteObject::operator()的指针类型。这样调用则为:

for_each(vwp.begin(),vwp.end(),DeleteObject());

当然,为了根本上解决这个问题,最简单的方法还是采用智能指针代替指针容器。如shared_ptr( 注:现已成为C++标准的一部分),头文件为<memory>。

void dosomething()
{
	typedef shared_ptr<Widget> SPW;
	vector<SPW> vwp;
	for(int i = 0; i < SOME_MAGIC_MUMBER; ++i)
		vwp.push_back(SPW(new Widget));  //注意这里的智能指针的初始化方式。
	……
}


8.切勿创建包含auto_ptr的容器对象。

包含只能指针的容器是没有问题的,但是auto_ptr不是这样的智能指针!


9.慎重选择删除元素的方法。

1)删除特定值

如果是vector,string ,deque,采用erase-remove方法。

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


如果是list,更好的方法是直接调用remove()成员函数:
c.remove(10);


如果是关联容器,直接调用erase()成员函数,返回被删除的个数

c.erase(10);

2) 删除使判别式为true的所有对象

如果有这样一个判别式:

bool badValue(int);  

如果是vector,string ,deque,采用erase-remove方法。

c.erase(remove_if(c.begin(),c.end(),badValue),c.end());


如果是list,更好的方法是直接调用remove()成员函数:

c.remove_if(badValue);

而对于关联容器,则没有很直接的方法,解决这一问题有两种方法,一种易于编码,一种则效率更高。简单但是效率稍低的方法是,利用remove_copy_if把需要的值拷贝到一个新的容器中,然后把原来的容器和新的容器值相互交换。

#include <iostream>
#include <algorithm>
#include <iterator>
#include <set>

using namespace std;

int main()
{
	set<int> iset;
	set<int> iset2;

	for(int i = 10; i <= 20; ++i)
		iset.insert(iset.end(),i);

	remove_copy_if(iset.begin(),iset.end(),inserter(iset2,iset2.end()),bind2nd(greater
		<int>(),15));

	iset.swap(iset2);

	//输出:10 11 12 13 14 15
	copy(iset.begin(),iset.end(),ostream_iterator<int>(cout," "));

	return 0;

}


第二种方法则是自己写循环来删除元素。


#include <iostream>
#include <algorithm>
#include <iterator>
#include <set>

using namespace std;

int main()
{
	set<int> iset;

	for(int i = 10; i <= 20; ++i)
		iset.insert(iset.end(),i);

	for(set<int>::iterator it = iset.begin(); it != iset.end();)
	{
		if(*it > 15)
			iset.erase(it++);
		else
			++it;
	}

	//输出:10 11 12 13 14 15
	copy(iset.begin(),iset.end(),ostream_iterator<int>(cout," "));

	return 0;

}

如果要对vector,string和deque采用这种方法,因为erase()是有返回值的,所以只需小改动一下:

if(*it > 15)
    it = ivec.erase(it);
else
    ++it;

10.了解分配子(allocator)的约定和限制。


11.理解自定义分配子的合理用法。


12.切勿对STL容器的线程安全性有不切实际的依赖。

这一点以后补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值