1.浅拷贝和深拷贝
左值和右值使用得当可以减少开销和提升性能,在代码的层面来说,提升性能的点在于浅拷贝和深拷贝。
定义一个带指针的类,编写一个浅拷贝和深拷贝的成员函数:
class PClass {
public:
PClass(int num) : m_num(num) {
m_p = new int[num];
for (int i = 0; i < num; ++i) {
m_p[i] = i;
}
}
~PClass() {
if (m_p) {
delete m_p;
}
}
// 浅拷贝
void shallowCopy(PClass& p) {
m_num = p.m_num;
m_p = p.m_p;
p.m_p = nullptr;
}
// 深拷贝
void deepCopy(PClass& p) {
m_num = p.m_num;
m_p = new int[m_num];
for (int i = 0; i < m_num; ++i) {
m_p[i] = p.m_p[i];
}
}
private:
int* m_p = nullptr;
int m_num;
};
int main()
{
PClass a = PClass(10);
PClass b;
PClass c;
b.deepCopy(a);
c.shallowCopy(a); // 转移a中m_p指向的内存的使用权给c
return 0;
}
可以看到深拷贝会重新申请内存空间,b把拷贝对象a中申请的内存的数据逐个拷贝至新对象b中,这种拷贝方法开销较大。
而浅拷贝仅仅只是把a的m_p指针指向的内存的使用权”转移“给新的对象c,并未申请新的内存空间实际拷贝,这种方法开销较少,但要保证a无法再操作该内存了,否则会出现问题。
如果a是一个临时对象,被拷贝后就不存在了,那么就能保证浅拷贝不出问题。
2.左值和右值
c98中的左值和右值
左值:持久存在的值,一般有变量名。
右值:表达式结束后就会消失的值,一般没有变量名。字面值(不包括字符串字面值)、临时的表达式值、临时的函数返还值这些短暂存在的值都是右值。
字符串字面值存储在静态内存区,是持久存在的,因此不属于右值
int a; // 左值
int b = a + 1; // a + 1表达式的返回值为右值
int c = 1; // 1为右值
fun(); // 右值
3.右值引用与std::move
c++中&表示左值引用,如果作为形参,则匹配左值:
void fun(PClass& p); // 该函数接受一个左值作为参数
在c++11中引入了右值引用,用&&表示
void fun(PClass&& p); // 该函数接受一个右值作为参数
c++11提供std::move,可以将左值转换为右值引用,虽然这个值可以一直存在,并不是个临时的、马上就会消失的值。
此时,就可以更优雅的解决浅拷贝和深拷贝的问题了,为PClass增加拷贝构造函数和移动构造函数:
class PClass {
public:
...
// 拷贝构造函数接受一个右值 进行浅拷贝 即移动构造函数
PClass(PClass&& p) {
std::cout << "Move construct" << std::endl;
m_num = p.m_num;
m_p = p.m_p;
p.m_p = nullptr;
}
// 拷贝构造函数接受一个左值 进行深拷贝 即拷贝构造函数
PClass(PClass& p) {
std::cout << "Copy construct" << std::endl;
m_num = p.m_num;
m_p = new int[m_num];
for (int i = 0; i < m_num; ++i) {
m_p[i] = p.m_p[i];
}
}
...
};
int main()
{
PClass a(10);
PClass b(a); // 调用拷贝构造函数
PClass c(std::move(a)); // 将b转换为右值引用 此时调用PClass的移动构造函数
// a中的资源使用权交给了c 采用浅拷贝减少内存开销 但要保证a不会再操作这部分内存或者a不会再被使用
return 0;
}
此时如果用右值作为参数去构造PClass,就会调用移动构造函数,从使用浅拷贝减少开销。
但要注意的是:右值引用一般只用在函数返回值和形参中,如果定义一个右值,并不能作为一个右值来使用
int main()
{
PClass&& e = PClass(); // 定义一个右值引用
PClass f(e); // 使用该右值引用实例化PClass 此时发现调用的是拷贝构造函数 也就是说e被当做左值来使用
return 0;
}
右值引用类型只是用于匹配右值,而并非表示一个右值。因此,尽量不要声明右值引用类型的变量。
3.左值、将亡值、纯右值
c++11中进行了如下分类,目的是为了更好的符合移动语义
- 左值:指持久存在的对象,或者左值引用的返回值
- 将亡值:右值引用的返回值
- 纯右值:即之前提到的右值(临时对象)
将亡值和纯右值都是右值
PClass& func1();
PClass&& func2();
PClass func3();
int main(){
PClass a;
a; //左值表达式
func1(); //左值表达式,因为返回值类型是左值引用
func2(); //将亡值表达式,因为返回值类型是右值引用,即使指代的对象即使非临时。
func3(); //纯右值表达式,返回值为临时值。
std::move(a); //将亡值表达式,std::move本质也是个函数,同上。
PClass(); //纯右值表达式
}
std::move函数的实现原理就是的将参数强转成右值引用类型返回
4.参数和返回值
比较常用的地方就是函数在参数传递时,引起的拷贝构造和移动构造,此处可能决定开销大小。
void fun1(PClass p) { return; }
void fun2(PClass&& p) { return; }
int main()
{
PClass a(10);
PClass& b = a;
PClass c(10);
PClass d(10);
fun1(a); // a为左值 引起拷贝构造
fun1(b); // b为左值 引起拷贝构造
fun1(std::move(c)); // fun1的实参为右值 实例化fun1参数p时引起移动构造 c中的数据交给参数p c中m_p为nullptr (1)
fun1(d); // fun1的实参为左值 引起拷贝构造 (2)
fun2(std::move(d)); // 未引起任何构造函数调用 此处虽然fun2的实参为右值 但并不是临时值 而是持久存在的 因此右值不能再理解为临时值了
return 0;
}
可以看到代码中(1) (2)处如果c和d不再使用,则使用std::move转换为右值,(1)相比(2)处,会减少开销。
对于返回值:
PClass fun1() { PClass a(10); return a; }
int main()
{
PClass a = fun1(); // 因为fun1()的返回值为右值 所以此处引起移动构造函数的调用 减少开销
PClass b(fun1()); // 因为fun1()的返回值为右值 所以此处引起移动构造函数的调用 减少开销
return 0;
}
参考:
https://www.cnblogs.com/KillerAery/p/12802771.html