一.问题的引出:
容器可以存放对象,可以存放指针,这里要谈的是两者的使用问题。就是什么时候存放对象更好,什么时候存放指针更好?
二.问题的分析过程:
1. 首先说下stl容器的工作方式
对于内建类型(int float char等),容器的工作方式是纯粹的位拷贝,这里没有什么需要多说的。
对于自定义的对象,容器容纳了对象(比如通过insert或push_back等),但容器中存放的对象不是你给它们的那个对象,因为两个对象在内存中的位置不一样。此外,当你从容器中获取一个对象时,你所得到的对象不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来。拷贝是STL的方式。
下面的例子可以说明这点:
int main()
{
vector<Object > v;
//我们的对象
Object data;
data.setName("Data");
//对象插入到vector后面
v.push_back(data);
cout << "Address of data : &data = " << &data << " ";
cout << "Address of data in vector: &v[0] = " << &v[0] << " ";
system("pause") ;
return 0;
}
输出如下:
2. 存放对象的情况
明白了容器的工作方式,那么进一步来讨论容器存放对象和指针在操作过程中的开销。内建类型的数据进行拷贝的方式是位拷贝,自定义类型的数据进行拷贝会调用类的拷贝构造函数,这个函数是每个类都有的,如果类中没有显式的声明那么编译器也会给提供一个默认的拷贝构造函数。如果一个类的数据非常多,或者包含其他复杂自定义类型,那么此时类的拷贝构造的开销是非常大的。
此时容器中要是存放对象,那么一个简单的插入操作的代价也是惊人的,更别说什么排序之类的操作,很容易带来性能上的瓶颈,这个时候就需要在容器中存放对象的指针,这样的话就避免了这些多余的拷贝消耗,因为指针就是一个机器字长,拷贝的代价可以忽略不计。
写了个测试代码如下:
typedef std::vector<Object> ObjectVector;
typedef std::vector<Object*> PointerVector;
//下面是存贮对象的情况
begin = GetTickCount();
ObjectVector objectVector;
for (DWORD i = 0; i < MAX; i++)
{
objectVector.push_back(*pCom);
}
end = GetTickCount();
cout << "存放对象消耗的时间:";
cout << end - begin << "毫秒 ";
//下面是存贮指针的情况
begin = GetTickCount();
PointerVector pointerVector;
for (DWORD i = 0; i < MAX; i++)
{
Object* pCom = new Object; //#add,原作者的代码不知道是怎么运行的,错漏得一塌糊涂,而且似乎pCom从未new过,始终指向同一对象,这个new代价不可忽略.
pointerVector.push_back(pCom);
}
end = GetTickCount();
cout << "存放指针消耗的时间:";
cout << end - begin << "毫秒 ";
下面的结果是在Release版本下,并且编译器的优化关闭的情况下,这和我们目前的客户端设置一样:
MAX = 4000
MAX = 40000
MAX = 400000
上面的数据没有用统计学的方法去测试,只是取了一次结果,我测试了多次结果在数量级上是一样的(用上面的数据只是说明拷贝的代价是巨大的,并没有强调必须用指针)。
分析完了拷贝的性能消耗,再看看另一个问题,就是声明了一个存放基类对象的容器,如果此时向容器中插入子类的对象,那么子类特有的那些内容就会被无情剥离(slicing)。这是一个很严重的问题。解决的方法还是使用基于指针的容器。
2. 存放指针的情况
上面提到的两个问题用指针确实比用对象好,问题不是这么绝对。在上面考虑拷贝消耗的时候有个前提:如果一个类的数据非常多,或者包含其他复杂自定义类型,并且需要大量的使用需要容器内部对象拷贝的操作。如果一个对象中就是几个简单的内建类型,或者干脆就是一个简单的内建类型的数据,那么再用指针可真是得不偿失了,因为使用指针需要程序员去管理内存。完全没有必要为了节省几个int类型的拷贝消耗而去自己去做内存的管理,确实完全没有必要。
用指针就需要自己手动的去管理这些指针所指向的内存,stl容器确实可以动态申请内存使自己变大以容纳更多的元素,但这些动态空间存放的是你的指针,而并不是你指针指向的动态内存,你的指针内存当然需要你去管理,如果实在不想做这些管理工作,可以去使用智能指针。
三.问题的总结:
通过上面的分析,总结了一下几点:
1. Stl容器可以存放内建类型、自定义类型、指针类型的元素。
2. 元素如果是内置数据类型,那么就存放数据本身。
3. 元素如果是复杂类型,并且在使用容器的过程中需要容器的元素进行大量的拷贝操作的时候,就要考虑在容器中放入指针;
4. 存放指针容易出现内存的泄露,所以在使用的时候需要考虑清楚,如能接口设计的合理,能保证容器在使用的过程中不进行大量的拷贝工作,在容器中存放对象是最好的了。
5. 使用智能指针是一种两种优点都兼备的,既有指针的操作效率,又避免了自己手动管理内存带来的问题。
6. 指针可以解决派生类对象存放在使用基类实例化的容器中的剥离(slicing)问题。
在考虑容器中是存放对象还是指针的时候脑子里时刻要想到,我的操作需要容器做多少拷贝工作,这些拷贝操作带来的损耗能否接受,从这个本质问题上把握好了,选择起来就不是问题了,要根据实际情况灵活运用。