# std::vector/指针成员 联用时的大坑 下面这段代码有何问题? #include class A { public: A() { p = new int; } ~A() { delete p; }; int * p; }; int main() { std::vector 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 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$分配的,一个不够分配俩,两个不够了下次直接分配四个……当然,这些都是具体实现的事情,谁也管不着微软。 ## 解决方案 定位问题之后解决就简单了,有好几种方案: 1. 手动实现复制构造函数,实现深拷贝(最安全) 2. 事先算出需要多少空间,然后在第一次`push_back()`之前`reserve()`一下(最高效) 3. 使用`shared_ptr`代替裸指针(最简单) 4. 别用指针了,这玩意儿确实麻烦…… ## PS 如果要防止一切可能的拷贝,最安全的办法是手动声明一个`A(const A&)`在那里,设置为`private`并且**不实现**。如果实现了,从理论上讲,其他类把`A`搞成`friend`,还是可以调用的。 然而`std::vector`是要求自己存放的对象有拷贝构造函数的。所以这个办法只是让你提前注意到这个*可能的*问题而已,并不是一个解决方案。