std::vector/指针成员 联用时的大坑
下面这段代码有何问题?
#include <vector> class A { public: A() { p = new int; } ~A() { delete p; }; int * p; }; int main() { std::vector<A> vec; for (int i=0; i<5; ++i) { vec.push_back(A()); } return 0; }
看起来好像挺正常的。虽然包括了一个指针,但有new
有delete
一一对应,浓眉大眼的,应该没有问题。
然而,事实是运行起来马上抛异常,诊断发现异常出在delete
,显然是试图释放一个不存在的对象。
补充一个小知识:C++标准规定`delete`应该自己检查指针是否为`null_ptr`,所以任何情况下都不用手动检查`null_ptr`问题,不会在这上面出错的。
debug思路
但是A
中唯一的构造函数确定new
了一个int
出来,p
为何会指向一个不存在的对象?
断点半天看不出来问题,于是上log:
class A { public: A() { std::cout << "construct A" << std::endl; p = new int; } ~A() { std::cout << "destroy A" << std::endl; //delete p; //此bug的另一大坑在于,即使打了log,由于程序很快就会出错,log收集到的信息并不多,所以不容易看出来原因。 //所以诊断代码中需要先屏蔽掉指针操作 }; int * p; }; int main() { std::vector<A> vec; for (int i=0; i<5; ++i) { std::cout << "i = " << i << std::endl; vec.push_back(A()); } std::cout << "cycle end" << std::endl; return 0; }
运行出来的结果很有意思:
construct A destroy A i = 1 construct A destroy A destroy A i = 2 construct A destroy A destroy A destroy A i = 3 construct A destroy A destroy A destroy A destroy A i = 4 construct A destroy A destroy A destroy A destroy A destroy A cycle end destroy A destroy A destroy A destroy A destroy A
每次循环中都正常新建了一个A
。然而,第二次循环中就出现了1次“不应有”的析构,第三次循环中出现了2次,第四次则析构了3个A
……
显然,就是这个情况导致了多次析构,而从第三轮循环开始,如果没有屏蔽掉delelte p
,程序就会死于尝试析构第二轮循环中已经被析构的那个指针,不信可以输出一下指针地址看看。
继续分析可以知道,这些被析构的对象,显然不是由我们手写的构造函数搞出来的,否则不会没有log。那么只剩下一种途径,就是编译器隐式添加的那个默认复制函数A(const &A)
。验证很简单,这里不搞了。
整件事情至此比较明了:每次循环中,整个vector
中的所有原有的元素都被默认复制函数浅拷贝了一次,然后原有的对象被析构。再下一次循环中,这些被复制出来的对象又一次被析构,但上次复制出来的指针现在成了野指针,既不合法又不是null_ptr
,被delete
傻乎乎的拿去用,显然会出错。
为什么会搞出这些反复的复制/析构?也不难猜测:当程序试图增大vector
的时候,如果之前的保留空间不足,为了保证vector
是连续的,STL
会另外开辟一块更大的空间储存所有对象,然后把原先的数组整个干掉。
这个是确实是疏忽了,不过这里(VC 2017)STL
的表现跟以前听说的也不完全一致嘛,以前听说是按$2^n$分配的,一个不够分配俩,两个不够了下次直接分配四个……当然,这些都是具体实现的事情,谁也管不着微软。
解决方案
定位问题之后解决就简单了,有好几种方案:
- 手动实现复制构造函数,实现深拷贝(最安全)
- 事先算出需要多少空间,然后在第一次
push_back()
之前reserve()
一下(最高效) - 使用
shared_ptr
代替裸指针(最简单) - 别用指针了,这玩意儿确实麻烦……
PS
如果要防止一切可能的拷贝,最安全的办法是手动声明一个A(const A&)
在那里,设置为private
并且不实现。如果实现了,从理论上讲,其他类把A
搞成friend
,还是可以调用的。
然而std::vector
是要求自己存放的对象有拷贝构造函数的。所以这个办法只是让你提前注意到这个可能的问题而已,并不是一个解决方案。