c++ stl vector erase 操作

现在项目逐渐的采用 C++11 ,原 boost::shared_ptr 也已经被添加到 STL 标准库中 std::shared_ptr 。shared_ptr 是采用引用计数实现的智能指针,并且 shared_ptr 对象可被复制,因此可被添加到容器中(如 vector )。所以,我现在对 shared_ptr 的复制很敏感,因为不注意的话造成 shared_ptr 的引用计数永远不降为 0 ,那么它指向的对象也永远不会被析构。很有可能就造成了内存泄漏。因此以 vector 为例,再次感受一下容器的复制行为。
想把一个对象存放到 vector 中,该对象要能复制(若想添加到 map 中,对象还需要支持 operator< 用于比较),也就是该对象的拷贝构造函数 T(const T&) 和赋值操作符 T& operator=(const T&) 要能被访问。vector 容器的常见复制行为表现如下。

  • push_back 添加对象到容器中。假设添加的对象是 A ,容器中的新对象是 A,此时会调用 A 的拷贝构造函数用 A 来初始化 A` 。
  • operator[] 修改容器对象。假设被修改的容器的对象是 A,被复制的对象的是 A ,此时会调用 A 的复制操作符,用 A 中的数据来修改 A` 。

这两种操作对应了对象的拷贝构造和赋值行为。下面来看一个例子。

#include <vector>
#include <stdio.h>

using namespace std;

class Foo {
public:
	Foo(int t) 
		: tag_(t) {
		printf("Foo ctor tag:%d\n", tag_);
	}

	~Foo() {
		printf("~Foo tag:%d\n", tag_);
	}

	Foo(const Foo &rhs) 
		: tag_(rhs.tag_) {
		printf("Foo copy ctor tag:%d\n", tag_);
	}

	Foo& operator=(const Foo &rhs) {
		if (this == &rhs)
			return *this;
		printf("Foo= tag:%d old:%d\n", rhs.tag_, tag_);
		tag_ = rhs.tag_;
	}

	int tag_;
};

int 
main() {
	{
		vector<Foo> vec;
		vec.reserve(16);
		{
			vec.push_back(Foo(1));
			vec.push_back(Foo(2));
			vec.push_back(Foo(3));
			vec[2] = Foo(33);
		}

		printf("\nerase the first Foo(1)\n");
		vec.erase(vec.begin());
		printf("destroy vec now\n");
	}
	
	{
		printf("\n");
		vector<Foo> vec;
		// vec[0] = Foo(100); // this will segment fault
		vec.reserve(16);
		vec[0] = Foo(100); // call operator= 但是 vec[0] 并未被构造,查看 tag_ 是随机值
		printf("destroy vec now\n");
	}
	printf("\nmain exit\n");
	return 0;
}

编译 g++ -g -o t test.cpp -fno-elide-constructors 运行结果如下。

$ ./t.exe
Foo ctor tag:1
Foo copy ctor tag:1
~Foo tag:1
Foo ctor tag:2
Foo copy ctor tag:2
~Foo tag:2
Foo ctor tag:3
Foo copy ctor tag:3
~Foo tag:3
Foo ctor tag:33
Foo= tag:33 old:3
~Foo tag:33

erase the first Foo(1)
Foo= tag:2 old:1
Foo= tag:33 old:2
~Foo tag:33
destroy vec now, size:2
~Foo tag:2
~Foo tag:33

Foo ctor tag:100
Foo= tag:100 old:7184960
~Foo tag:100
destroy vec now, size:0

main exit

调用 vec.reserve(16) 预先分配 16 个对象。这样的目的是免除容器一点点的增加空间,每次容器增加新空间,都会先分配新空间在新空间构造已经存在的对象,然后再销毁旧的空间。在我们的例子中,这样会不断的调用构造函数和析构函数,不方便看日志,所以就先预留 16 个对象。但是在实际项目中,要注意容器空间增长问题。比如代码中保存了指向容器的某个迭代器或指针,但是之后往容器中添加元素,使得容器不得不重新分配更大的空间存放元素,这样之前代码中保存的迭代器或者指针也就失效了。一般会犯的错误是,在遍历容器元素的循环中,添加新的元素。此刻一定要小心谨慎。


vec.push_back(Foo(3)); 执行完毕后,日志输出 Foo copy ctor tag:3 表示调用拷贝构造函数初始化容器对象, vec[2] = Foo(33); 执行完毕后,日志输出 Foo= tag:33 old:3 表示修改了之前的容器对象。这里你会发现 ~Foo tag:3 这种执行析构函数的日志,其实就是创建的临时对象(如 Foo(3) )被添加到容器后,就被销毁了,已经用处了。
再提一点 C++ 作用域操作符 { } 真的挺好用的,超出作用域的对象就被析构了。


然后调用 erase 删除容器中的第一个元素操作。删除之前容器中元素顺序从前往后是 Foo(1) Foo(2) Foo(33) 调用 erase 后,后面的元素前移,所以顺序变成 Foo(2) Foo(33) 。注意日志输出如下。往前移动元素时,调用了赋值操作符。移动完毕后, erase 前的最后一位元素被执行析构函数销毁 ~Foo tag:33 。所以调用 erase 函数移动前面的元素时,伴随着后面的元素分别往前一位,以及 erase 前最后一位元素被销毁。注意这里的移动 + 销毁逻辑。因为如果没有销毁逻辑,若容器中存放的是 shared_ptr 对象,那么容器中将有 2 个指向同一个对象的 shared_ptr 对象。若容器调用 erase 删除第一位元素,直到删完,但由于我们假设此时 erase 只有移动没有销毁逻辑,那么容器中还存放着 shared_ptr 对象。假如容器对象不销毁,shared_ptr 也不会被销毁,那么 shared_ptr 指向的对象也就不会被销毁了。

erase the first Foo(1)
Foo= tag:2 old:1
Foo= tag:33 old:2
~Foo tag:33

继续看代码, vec[0] = Foo(100); 执行完毕后日志输出了 Foo= tag:100 old:3252800 ,说明执行前虽然 reserve 预留了对象的空间,但是对象并未被构造初始化,目前对象中的值还是随机值。而且这句代码也没有实际意义,因为 vector 的 size 还是 0 。


最后,我想说 C++ 内存管理真的很细节。尤其是使用引用计数智能指针时,要留意,不要造成引述计数无法降为 0 导致指向的对象的内存不能被释放。
还有如果自己实现容器类,也要考虑不要忘记执行构造函数,不要忘记执行析构函数,比如:

  • 预留容器空间时,添加新元素,在已分配好的内存空间上调用构造函数,构造新的元素 new (ptr) T
  • 如果 erase 操作需要移动元素,假设由后向前移动,不要忘记执行最后一位元素的析构函数,但是这个执行析构函数的元素的内存空间可以不被 delete ptr->~T()

转载于:https://my.oschina.net/iirecord/blog/1305778

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值