# 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`是要求自己存放的对象有拷贝构造函数的。所以这个办法只是让你提前注意到这个*可能的*问题而已,并不是一个解决方案。