vector容器的push_back和emplace_back

425 阅读1分钟

假设有这么一个类

class Person1 {
public:
	Person1(string name) :pName_(new string(name)) { }
	Person1(const Person1& p) :pName_(p.pName_) { 
		cout << "In copy constructor, pName_ = " << p.pName_ << endl; 
	}
	~Person1() {
		cout << "Run in Person " << pName_ << " destructor!" << endl;
		delete pName_;
	}

	void printName() {
		cout << "Person1 -> " << *pName_ << endl;
	}

public:
	string* pName_;
};

那么,利用push_back往一个vector容器中插入该类的对象时,会先创建一个匿名对象,然后调用拷贝构造函数创建vector中的对象,再析构掉匿名对象。为了省去创建匿名对象和析构匿名对象的开销,C++11出了emplace_back方法。它和push_back的区别是:**当插入rvalue,它节约了一次move构造。当插入lvalue,它节约了一次copy构造。**因此,理论上emplace_back不会发生拷贝,但是vector底层发生了内存分配扩张时,还是会进行拷贝构造。见下面例子:

int main() {
	vector<Person1> person1s;
    
	cout << person1s.capacity() << endl;
	person1s.emplace_back("Hik1");	// 不会创建匿名对象,而是在vector自身的空间中创建对象,因此也不会析构匿名对象
	cout << person1s[0].pName_ << endl;
	person1s.emplace_back("Hik2");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
        cout << person1s.capacity() << endl;
    
	return 0;
}

输出:
0					// 一开始时,容量为0
01204F40				// 第一次创建对象,该数字是"Hik1"字符串在内存中的地址
In copy constructor, pName_ = 01204F40  // 第二次创建对象,调用拷贝,从"pName_ = 01204F40"可以看出,是利用第一次的对象"Hik1"来创建了一个匿名对象
Run in Person 01204F40 destructor!	// 第二次创建对象完毕,析构掉匿名对象,也即把"Hik1"字符串的空间释放了,此时对象1的pName_已是野指针了
01204F40				// 对象1的pName_,值还在,空间及内容已释放
0120F2A0				// 对象2的pName_值
2					// 容量扩充为2

第一次emplace_back时有没有调用拷贝构造函数呢?乍看上面的打印似乎没有,但是如果做个测试,发现其实还是调用的。第一次emplace_back对象时,容量从0增加到1,也在扩容,而扩容时就会调用。只是第一次扩容时,没有现成的元素对象作为被拷贝,所以没有打印。同样,析构也是一样,没有现成的而已。可以这样测试,直接Person1(const Person1&) = delete;,此时编译器报错“尝试引用已删除的函数”。

更进一步,再emplace_back一个对象试下

int main() {
	vector<Person1> person1s;
    
	cout << person1s.capacity() << endl;
	person1s.emplace_back("Hik1");	
	cout << person1s[0].pName_ << endl;
	person1s.emplace_back("Hik2");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
	person1s.emplace_back("Hik3");
	cout << person1s.capacity() << endl;

	return 0;
}

输出:
0
00A54F40
In copy constructor, pName_ = 00A54F40
Run in Person 00A54F40 destructor!
00A54F40
00A61538
In copy constructor, pName_ = 00A54F40	// 从这里看出,利用对象1和对象2进了两次拷贝构造函数,说明直接扩充了2个空间(虽然只是插入一个元素)
In copy constructor, pName_ = 00A61538	// 所以推断vector容器在扩容时是在当前容量的基础上直接翻倍
Run in Person 00A54F40 destructor!	// 这里要析构两个匿名对象,由于对象1在前一次扩容时已经析构了,所以程序运行到这里崩溃

D:\Debug\cpp11_14.exe (进程 11428)已退出,代码为 -1。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

鉴于上面的问题,可以采用在开始时直接预留空间的方式,防止反复扩容,多次调用拷贝构造和析构函数,提升效率。见如下代码:

int main() {
	vector<Person1> person1s;
    
	person1s.reserve(10);
	cout << person1s.capacity() << endl;
	person1s.emplace_back("Hik1");
	cout << person1s[0].pName_ << endl;
	person1s.emplace_back("Hik2");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
	person1s.emplace_back("Hik3");
	cout << person1s.capacity() << endl;

	return 0;
}

输出:
10		// 容量是10
00EA5230
00EA5230
00EAF430
10		// 容量还是10

可以看见,当预留了足够的空间后,没有发生扩容,也没有调用拷贝构造函数和析构函数,程序运行效率得到了提升。

当然,可以采用deque,该容器没有扩容一说,如下:

int main() {
	deque<Person1> person1s;	// 采用deque

	person1s.emplace_back("Hik1");
	cout << person1s[0].pName_ << endl;
	cout << endl;
    
	person1s.emplace_back("Hik2");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
	cout << endl;
    
	person1s.emplace_back("Hik3");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
	cout << person1s[2].pName_ << endl;
	cout << endl;
    
	person1s.emplace_back("Hik4");
	cout << person1s[0].pName_ << endl;
	cout << person1s[1].pName_ << endl;
	cout << person1s[2].pName_ << endl;
	cout << person1s[3].pName_ << endl;
	cout << endl;
	person1s.front().printName();
    
	return 0;
}

输出:
011E9760

011E9760
011EF2C0

011E9760
011EF2C0
011EF1E8

011E9760
011EF2C0
011EF1E8
011EF668

Person1 -> Hik1	    // 没有进入到拷贝构造函数,也没有析构匿名对象,程序也没有出现崩溃。