C++左值/右值、左值引用&/右值引用&&、移动语义move、完美转发forward

本文深入探讨C++11中的左值/右值、左值引用/右值引用、移动语义、完美转发及emplace_back等新特性,通过实例讲解如何利用这些特性提升程序性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

   C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能。

  • 右值引用仅仅是通过改变资源的所有者(剪切方式,而不是拷贝方式)来避免内存的拷贝,能大幅度提高性能。
  • forward 能根据参数的实际类型转发给正确的函数。
  • emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。

  阅读本文来看看c++11的这些新特性吧~源码地址gopherWxf1.3.1 智能指针-move-forward-左右值引用

  本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

1. 什么是 左值 / 右值

  左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。

  我们可以从两个维度去判断:左值可以取地址,位于等号左边;有值不能取地址,位于等号右边。换句话说,有变量名称的就是左值,没有名称的就是右值。

int a = 6;

  a可以通过 & 取地址,位于等号左边,所以a是左值;6位于等号右边,6没法通过 & 取地址,所以6是个右值。

struct A {
	A(int a = 0) {
		a_ = a;
	}
	int a_;
};
A a = A();

  a可以通过 & 取地址,位于等号左边,所以a是左值;A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

  可见左右值的概念很清晰,有地址变量就是左值,没有地址的字面值、临时值就是右值。

2. 什么是 左值引用& / 右值引用&&

  引用的本质就是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。

  • 左值引用(符号&):可以指向左值,不能指向右值
  • 右值引用(符号&&):可以指向右值,不能指向左值

2.1 左值引用&

  引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

  这里需要特别注意一点,const左值引用是可以指向右值的const左值引用不会修改指向值,因此可以指向右值

const int &ref_a = 5; // 编译通过

  这也是为什么要使用 const & 作为函数参数的原因之一,如 std::vector 的 push_back

void push_back (const value_type& val);

  如果没有 const , vec.push_back(5) 这样的代码就无法编译通过,因为value_type&左值引用不能指向右值。

value_type & val= 5; // 左值引用指向了右值,会编译失败

2.2 右值引用&&

  即然左值引用不能指向右值,那么我们就能看出,右值引用专门为右值而生,为了使用右值引用去修改右值

int &&ref_a_right = 5; // ok

int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值

ref_a_right = 6; // 右值引用的用途:可以修改右值

  右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。

  通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该右值引用类型变量还活着,那么这个右值临时量将会一直存活下去。

2.3 右值引用有办法指向左值吗?

  有办法,使用 std::move,后续会详细介绍 std::move,这里先做简单理解。

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向

cout << a; // 打印结果:5

  在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。

   std::move是一个非常有迷惑性的函数:

  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,这样理解是错误的!
  • 事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx)不会有性能提升,只是类型转换工具(后续介绍如何使用move提升性能)。

  同样的,右值引用能指向右值:本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6;
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;

2.4 左值引用、右值引用这个变量本身是左值还是右值?

  被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边(认真品读这句话)。仔细看下边代码:

#include <iostream>
using namespace std;
void change(int&& right_value) {
	right_value = 8;
}
int main() {
	int a = 5; // a是个左值
	int &ref_a_left = a; // ref_a_left是个左值引用
	int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
	
	change(a); // 编译不过,a是左值,change参数要求右值
	change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
	change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
	
	change(std::move(a)); // 编译通过
	change(std::move(ref_a_right)); // 编译通过
	change(std::move(ref_a_left)); // 编译通过
	change(5); // 当然可以直接接右值,编译通过
	
	// 打印这三个左值的地址,都是一样的
	cout << &a << ' ';
	cout << &ref_a_left << ' ';
	cout << &ref_a_right;
}

  看完这个代码我们知道了左值引用、右值引用这个变量本身是左值,但是我们还是会有一个疑问:std::move会返回一个右值引用 int && ,它是左值还是右值呢?

  从表达式 int &&ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以move返回的 int && 是个右值。

  这与我们刚才得到的结论冲突,不是刚说好 左值引用/右值引用变量本身 是左值吗?怎么又变成右值了?实则不冲突,上面我们所说的是被声明,有地址,有变量名

  所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值;如果没有名称那就是右值。

  换句话说,作为函数返回值的 && 是右值,直接声明出来的 && 是左值,其实引用和普通变量判断左右值的方法是一样的,等号左边就是左值,右边就是右值。

lvalue    = rvalue
int     a = 5;
int &&ref = std::move(a);

2.5 右值引用&&总结

  1. 性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
#include <iostream>
using namespace std;

void f1(const int& n) {
	n += 1; // 编译失败,const左值引用不能修改指向变量
}
void f2(int && n) {
	n += 1; // ok
}
int main() {
	f1(5);
	f2(5);
}
  1. 左值和右值是独立于它们的类型的,右值引用类型自身可能是左值也可能是右值(看有没有名字)(重点记忆)。
  2. auto&&函数参数类型自动推导的T&& 是一个未定的引用类型,被称为 universal references它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。(重点记忆)
  3. 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。(重点记忆)
  4. 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

3. 右值引用&&的使用场景

3.1 浅拷贝重复释放

  对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

#include <iostream>
using namespace std;
class A {
	public:
		A() :m_ptr(new int(0)) {
			cout << "constructor A" << endl;
		}
		~A() {
			cout << "destructor A, m_ptr:" << m_ptr << endl;
			delete m_ptr;
			m_ptr = nullptr;
		}
	private:
		int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag) {
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag) {
		return a;
	} else {
		return b;
	}
}
int main() {
	{
		A a = Get(false); // 运行报错
	}
	cout << "main finish" << endl;
	return 0;
}
constructor A
constructor A
ready return
destructor A, m_ptr:0xa35e40
destructor A, m_ptr:0xa35e20
destructor A, m_ptr:0xa35e40

--------------------------------
Process exited after 1.164 seconds with return value 3221226356

3.2 深拷贝构造函数

在上面的代码中,默认的拷贝构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:

#include <iostream>
using namespace std;
class A {
	public:
		A() :m_ptr(new int(0)) {
			cout << "constructor A" << endl;
		}
		A(const A& a) :m_ptr(new int(*a.m_ptr)) {
			cout << "copy constructor A" << endl;
		}
		~A() {
			cout << "destructor A, m_ptr:" << m_ptr << endl;
			delete m_ptr;
			m_ptr = nullptr;
		}
	private:
		int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag) {
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag) {
		return a;
	} else {
		return b;
	}
}
int main() {
	{
		A a = Get(false); // 运行报错
	}
	cout << "main finish" << endl;
	return 0;
}
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0x1a5e40
destructor A, m_ptr:0x1a5e20
destructor A, m_ptr:0x1a5e60
main finish

3.3 移动构造函数

  这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

#include <iostream>
using namespace std;
class A {
	public:
		A() :m_ptr(new int(0)) {
			cout << "constructor A" << endl;
		}
		A(const A& a) :m_ptr(new int(*a.m_ptr)) {
			cout << "copy constructor A" << endl;
		}
		// 移动构造函数,可以浅拷贝
		A(A&& a) :m_ptr(a.m_ptr) {
			a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
			cout << "move constructor A" << endl;
		}
		~A() {
			cout << "destructor A, m_ptr:" << m_ptr << endl;
			delete m_ptr;
			m_ptr = nullptr;
		}
	private:
		int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag) {
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag) {
		return a;
	} else {
		return b;
	}
}
int main() {
	{
		A a = Get(false); // 运行报错
	}
	cout << "main finish" << endl;
	return 0;
}
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0x95e20
destructor A, m_ptr:0x95e40
main finish

  上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的。

4. 移动语义move

4.1 move介绍

  移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

  我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。

  move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的移动构造构造函数支持。(因为std::move 只是类型转换工具,将左值转换成无名右值,而无名右值会走移动构造函数)。单单move是无法提升性能的,要与移动构造函数结合起来才能提升性能。

在这里插入图片描述

4.2 Demo演示

  以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
	private:
		char* m_data;
		size_t m_len;
		void copy_data(const char *s) {
			m_data = new char[m_len+1];
			memcpy(m_data, s, m_len);
			m_data[m_len] = '\0';
		}
	public:
		//构造函数
		MyString() {
			m_data = NULL;
			m_len = 0;
		}
		//构造函数
		MyString(const char* p) {
			m_len = strlen (p);
			copy_data(p);
		}
		//拷贝构造函数
		MyString(const MyString& str) {
			m_len = str.m_len;
			copy_data(str.m_data);
			std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
		}
		//重载=
		MyString& operator=(const MyString& str) {
			if (this != &str) {
				m_len = str.m_len;
				copy_data(str.m_data);
			}
			std::cout << "Copy Assignment  is called! source: " << str.m_data << std::endl;
			return *this;
		}
		//移动构造函数
		MyString(MyString&& str) {
			std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
			m_len = str.m_len;
			m_data = str.m_data; //避免了不必要的拷贝
			str.m_len = 0;
			str.m_data = NULL;
		}
		//重载=
		MyString& operator=(MyString&& str) {
			std::cout << "Move Assignment  is called! source: " << str.m_data << std::endl;
			if (this != &str) {
				m_len = str.m_len;
				m_data = str.m_data; //避免了不必要的拷贝
				str.m_len = 0;
				str.m_data = NULL;
			}
			return *this;
		}
		virtual ~MyString() {
			if (m_data) free(m_data);
		}
};
int main() {
	MyString a;
	a = MyString("Hello");  			// Move Assignment 因为a已经创建了,所以走=,而不是构造函数 
	MyString b = a; 					// Copy Constructor
	MyString c = std::move(a);  		// Move Constructor std::move将左值转为右值
	std::vector<MyString> vec;
	vec.push_back(MyString("World"));   // Move Constructor
	return 0;
}
Move Assignment  is called! source: Hello
Copy Constructor is called! source: Hello
Move Constructor is called! source: Hello
Move Constructor is called! source: World

  MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

//移动构造函数
MyString(MyString&& str) {
	std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
	m_len = str.m_len;
	m_data = str.m_data; //避免了不必要的拷贝
	str.m_len = 0;
	str.m_data = NULL;
}
//重载=
MyString& operator=(MyString&& str) {
	std::cout << "Move Assignment  is called! source: " << str.m_data << std::endl;
	if (this != &str) {
		m_len = str.m_len;
		m_data = str.m_data; //避免了不必要的拷贝
		str.m_len = 0;
		str.m_data = NULL;
	}
	return *this;
}

  有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

5. 完美转发forward

5.1 forward介绍

Template<class T>
void func(T &&val);

  auto&&函数参数类型自动推导的T&& 是一个未定的引用类型,被称为 universal references它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。 但要注意,引用以后,这个T类型它本质上是一个左值!

  forward 完美转发实现了参数在传递过程中保持其 参数模板 属性的功能,什么意思呢?在用右值引用做参数形参的时候,这个右值引用这个变量是左值,那么在函数内部转发此函数给其他函数就会变成左值。使用std::forward()可以实现完美转发,完美转发被赋值之前的类型,也就是说,之前是左值,转发之后就是左值,之前是右值,转发之后就是右值。不会因为形参是有名称,形参是左值而影响到原来的类型

  forward 比 move 更强大,通过它的参数模板,它可以转发为左值,也可以转发为右值

int &&a = 10;
int &&b = a; //错误

  注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。

int &&a = 10;
int &&b = std::forward<int>(a);//以右值的方式转发,因为int是右值

5.2 Demo演示

来看下面这个例子

#include <iostream>
using namespace std;
template <class T>
void Print(T &t) {
	cout << "L" << t << endl;
}
template <class T>
void Print(T &&t) {
	cout << "R" << t << endl;
}
template <class T>
void func(T &&t) {
	Print(t);//L,参数始终是左值
	Print(std::move(t));//R,move之后始终是右值
	Print(std::forward<T>(t));//完美转发
}
int main() {
	cout << "-- func(1)\n" << endl;
	func(1);//1 本身是 R

	cout << "-- func(x)\n" << endl;
	int x = 10;
	func(x); //x本身是 L

	int y = 20;
	cout << "-- func(std::forward<int>(y))\n" << endl;
	func(std::forward<int>(y));  //T模板参数为int右值 ,以右值方式转发y,实参为右值

	cout << "-- func(std::forward<int&>(y))\n" << endl;
	func(std::forward<int&>(y)); //T模板参数为左值引用,以左值方式转发y,实参为左值

	cout << "-- func(std::forward<int&&>(y))\n" << endl;
	func(std::forward<int&&>(y));//T模板参数为右值引用,以右值方式转发y,实参为右值

	return 0;
}
-- func(1)
L1
R1
R1
-- func(x)
L10
R10
L10
-- func(std::forward<int>(y))
L20
R20
R20
-- func(std::forward<int&>(y))
L20
R20
L20
-- func(std::forward<int&&>(y))
L20
R20
R20

  解释:

  • func(1)
- Print(t);//L,具名参数始终是左值
- Print(std::move(t));//R,move之后始终是右值
- Print(std::forward<T>(t));//完美转发,forward模板参数为右值,以右值转发,所以转发后实参为右值
  • func(x)
- Print(t);//L,具名参数始终是左值
- Print(std::move(t));//R,move之后始终是右值
- Print(std::forward<T>(t));//完美转发,forward模板参数为左值,以左值转发,所以转发后实参为左值
  • func(std::forward(y))
- Print(t);//L,具名参数始终是左值
- Print(std::move(t));//R,move之后始终是右值
- Print(std::forward<T>(t));//完美转发,forward模板参数为右值,以右值转发,所以转发后实参为右值
  • func(std::forward<int&>(y))
- Print(t);//L,具名参数始终是左值
- Print(std::move(t));//R,move之后始终是右值
- Print(std::forward<T>(t));//完美转发,forward模板参数为左值引用,以左值转发,所以转发后实参为左值
  • func(std::forward<int&&>(y))
- Print(t);//L,具名参数始终是左值
- Print(std::move(t));//R,move之后始终是右值
- Print(std::forward<T>(t));//完美转发,forward模板参数为右值引用,以右值转发,所以转发后实参为右值

  下面是一个综合demo

#include "stdio.h"
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class A {
	public:
		A() : m_ptr(NULL), m_nSize(0) {}
		A(int *ptr, int nSize) {
			m_nSize = nSize;
			m_ptr = new int[nSize];
			printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr);
			if (m_ptr) {
				memcpy(m_ptr, ptr, sizeof(sizeof(int) * nSize));
			}
		}
		// 拷贝构造函数实现深拷贝
		A(const A &other) {
			m_nSize = other.m_nSize;
			if (other.m_ptr) {
				printf("A(const A &other) m_ptr:%p\n", m_ptr);
				if(m_ptr) {
					delete[] m_ptr;
				}
				printf("delete[] m_ptr\n");
				m_ptr = new int[m_nSize];
				memcpy(m_ptr, other.m_ptr, sizeof(sizeof(int) * m_nSize));
			} else {
				if(m_ptr) {
					delete[] m_ptr;
				}

				m_ptr = NULL;
			}
			cout << "A(const int &i)" << endl;
		}
		// 右值引用移动构造函数
		A(A &&other) {
			m_ptr = NULL;
			m_nSize = other.m_nSize;
			if (other.m_ptr) {
				m_ptr = move(other.m_ptr); // 移动语义
				other.m_ptr = NULL;
			}
		}
		~A() {
			if (m_ptr) {
				delete[] m_ptr;
				m_ptr = NULL;
			}
		}
		void deleteptr() {
			if (m_ptr) {
				delete[] m_ptr;
				m_ptr = NULL;
			}
		}
		int *m_ptr = NULL; // 增加初始化
		int m_nSize = 0;
};
int main() {
	int arr[] = {1, 2, 3};
	A a(arr, sizeof(arr) / sizeof(arr[0]));
	cout << "m_ptr in a Addr: 0x" << a.m_ptr << endl;
	A b(a);
	cout << "m_ptr in b Addr: 0x" << b.m_ptr << endl;
	b.deleteptr();
	// 完美转换,右值 
	A c(std::forward<A>(a)); 
	cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl;
	c.deleteptr();
	vector<int> vect {1, 2, 3, 4, 5};
	cout << "before move vect size: " << vect.size() << endl;
	vector<int> vect1 = move(vect);
	cout << "after move vect size: " << vect.size() << endl;
	cout << "new vect1 size: " << vect1.size() << endl;
	return 0;
}
A(int *ptr, int nSize) m_ptr:0000000000BB5E20
m_ptr in a Addr: 0x0xbb5e20
A(const A &other) m_ptr:0000000000000000
delete[] m_ptr
A(const int &i)
m_ptr in b Addr: 0x0xbb5e40
m_ptr in c Addr: 0x0xbb5e20
before move vect size: 5
after move vect size: 0
new vect1 size: 5

6. emplace_back 减少内存拷贝和移动

6.1 emplace_back 介绍

  在C++11后,引入了emplace_back接口。emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

来看下面的代码

vector<string> testVec;
testVec.push_back(string(16, 'a'));

  将一个string对象添加到testVec中。底层实现:

  1. 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
  2. 其次,vector内会创建一个新的string对象,这是第二次构造。
  3. 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次string构造和一次析构。

  c++11可以用emplace_back代替push_back,emplace_back可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而达到优化的目的。

6.2 Demo演示

  • time_interval.h
//time_interval.h
#ifndef TIME_INTERVAL_H
#define TIME_INTERVAL_H
#include <iostream>
#include <memory>
#include <string>
#ifdef GCC
#include <sys/time.h>
#else
#include <ctime>
#endif // GCC
class TimeInterval {
	public:
		TimeInterval(const std::string& d) : detail(d) {
			init();
		}
		TimeInterval() {
			init();
		}
		~TimeInterval() {
#ifdef GCC
			gettimeofday(&end, NULL);
			std::cout << detail
			          << 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec -
			                  start.tv_usec) / 1000
			          << " ms" << endl;
#else
			end = clock();
			std::cout << detail
			          << (double)(end - start) << " ms" << std::endl;
#endif // GCC
		}
	protected:
		void init() {
#ifdef GCC
			gettimeofday(&start, NULL);
#else
			start = clock();
#endif // GCC
		}
	private:
		std::string detail;
#ifdef GCC
		timeval start, end;
#else
		clock_t start, end;
#endif // GCC
};
#define TIME_INTERVAL_SCOPE(d) std::shared_ptr<TimeInterval>time_interval_scope_begin = std::make_shared<TimeInterval>(d)
#endif // TIME_INTERVAL_H

  • main.cpp
#include <vector>
#include <string>
#include "time_interval.h"

int main() {
    std::vector<std::string> v;
    int count = 10000000;
    v.reserve(count); //预分配十万大小,排除掉分配内存的时间
    {
        TIME_INTERVAL_SCOPE("push_back string:");
        for (int i = 0; i < count; i++) {
            std::string temp("ceshi");
            v.push_back(temp);// push_back(const string&),参数是左值引用,内部做深拷贝
        }
    }
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back move(string):");
        for (int i = 0; i < count; i++) {
            std::string temp("ceshi");
            v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用,内部做move
        }
    }
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(string):");
        for (int i = 0; i < count; i++) {
            v.push_back(std::string("ceshi"));// push_back(string &&), 参数是右值引用
        }
    }
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(c string):");
        for (int i = 0; i < count; i++) {
            v.push_back("ceshi");// push_back(string &&), 参数是右值引用
        }
    }
    v.clear();
    {
        TIME_INTERVAL_SCOPE("emplace_back(c string):");
        for (int i = 0; i < count; i++) {
            v.emplace_back("ceshi");// 只有一次构造函数,不调用拷贝构造函数,速度最快
        }
    }
}
push_back string:408045 ms
push_back move(string):293146 ms
push_back(string):282487 ms
push_back(c string):272085 ms
emplace_back(c string):228662 ms

  第1中方法耗时最长,原因显而易见,将调用左值引用的push_back,且将会调用一次string的拷贝构造函数,比较耗时,这里的string还算很短的,如果很长的话,差异会更大

  第2、3、4中方法耗时基本一样,参数为右值,将调用右值引用的push_back,故调用string的移动构造函数,移动构造函数耗时比拷贝构造函数少,因为不需要重新分配内存空间。

  第5中方法耗时最少,因为emplace_back只调用构造函数,没有移动构造函数,也没有拷贝构造函数。

  为了证实上述论断,我们自定义一个类,并在普通构造函数、拷贝构造函数、移动构造函数中打印相应描述:

#include <vector>
#include <string>
#include "time_interval.h"

using namespace std;

class Foo {
public:
    //带参构造
    Foo(std::string str) : name(str) {
        std::cout << "constructor" << std::endl;
    }
    //拷贝构造
    Foo(const Foo &f) : name(f.name) {
        std::cout << "copy constructor" << std::endl;
    }
    //移动构造
    Foo(Foo &&f) : name(std::move(f.name)) {
        std::cout << "move constructor" << std::endl;
    }

private:
    std::string name;
};

int main() {
    std::vector<Foo> v;
    int count = 10000000;
    v.reserve(count); //预分配十万大小,排除掉分配内存的时间
    {
        TIME_INTERVAL_SCOPE("push_back T:");
        Foo temp("test");
        v.push_back(temp);// push_back(const T&),参数是左值引用
        //打印结果:
        //constructor
        //copy constructor
    }
    cout << " ---------------------\n" << endl;
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back move(T):");
        Foo temp("test");
        v.push_back(std::move(temp));// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
    cout << " ---------------------\n" << endl;
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(T&&):");
        v.push_back(Foo("test"));// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
    cout << " ---------------------\n" << endl;
    v.clear();
    {
        std::string temp = "test";
        TIME_INTERVAL_SCOPE("push_back(string):");
        v.push_back(temp);// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
    cout << " ---------------------\n" << endl;
    v.clear();
    {
        std::string temp = "test";
        TIME_INTERVAL_SCOPE("emplace_back(string):");
        v.emplace_back(temp);// 只有一次构造函数,不调用拷贝构造函数,速度最快
        //打印结果:
        //constructor
    }
}

constructor
copy constructor
push_back T:78 ms
 ---------------------

constructor
move constructor
push_back move(T):3 ms
 ---------------------

constructor
move constructor
push_back(T&&):2 ms
 ---------------------

constructor
move constructor
push_back(string):2 ms
 ---------------------

constructor
emplace_back(string):1 ms

进程已结束,退出代码0

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cheems~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值