Chapter3:左值和右值
- 1. 将右值绑定到 左值
- 2. 将右值绑定到 常量左值引用
- 3. 将右值绑定到右值引用
- 总结:
- 5. 左值,右值和右值引用
- 6. 引用类型可以引用的的值类型
- 7. 全能类型,常量左值引用用途
- 7.1 拷贝构造函数
- 7.2解决浅拷贝(深拷贝)
- 8. 解决深拷贝问题
在C语言中,我们常常会提起左值(lvalue),右值(rvalue)这样的称呼,而在编译程序时,编译器有时也会报出错误信息中包含 左值,右值说法。不过左值、右值通常不是通过一个严谨的定义而为人所知。下面我通过这样一个例子,来引导大家认识: 左值,右值,左值引用,右值引用,常量左值引用。
#include<iostream>struct Copyable{Copyable() {std::cout<< "copied...." << std::endl;}Copyable(const Copyable ©){std::cout<< "copied" << std::endl;}
};Copyable ReturnRvalue()
{// 这是返回的 右值 return Copyable();
}// 1. 接收右值表达式
void AcceptValue(Copyable copy)
{}// 2. 右值引用减少对象开销,并延迟对象生命周期
// 直观意义:为临时变量续命,也就是为右值续命,因为右值在表达式结束后就消亡了,
// 如果想继续使用右值,那就会动用昂贵的拷贝构造函数。
void AcceptRef(Copyable && copy)
{}// 3. 常量左值引用减少对象开销,并延迟对象生命周期
void AcceptRef_2(const Copyable& copy){}int main()
{Copyable copy;std::cout << "Pass by value:" << std::endl;AcceptValue(ReturnRvalue());std::cout << "Passs by reference: " << std::endl;AcceptRef(ReturnRvalue());std::cout << "Passs by reference_2: " << std::endl;AcceptRef(ReturnRvalue());
}
// 打印结果: g++ -std=c++11 main.cpp -fno-elide-constructors// Copyable copy
construct..... Pass by value:// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,一次拷贝函数作为AcceptValue函数实参
construct.....
copied construct
copied constructPasss by reference:
// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
construct.....
copied construct// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
Passs by reference_2:
construct.....
copied construct
上面的例子:我们用到了
- 函数形参为左值,然后将右值表达式作为实参绑定到左值
- 函数形参为右值引用,然后将右值作为实参绑定右值引用
- 函数形参为常量左值引用,然后将右值作为实参绑定到 常量左值引用
1. 将右值绑定到 左值
💚💚 这种绑定方式的特点:
- 函数 ReturnRvalue() 在运行结束后,返回值(右值临时变量)复制一份作为实参传递给 AcceptValue() 函数后就不会存活下去了。
这样会导致:Copyable 这个对象构建两次。浪费内存。
2. 将右值绑定到 常量左值引用
💚💚
- AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
- 这个临时值的生命周期就和 AcceptRef() 生命周期一致
3. 将右值绑定到右值引用
💚💚
- AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
- 这个临时值的生命周期就和 AcceptRef() 生命周期一致。
总结:
🧡 通过上面三个小节,我们总结了 将右值绑定到 :左值,常量左值引用,右值引用的情况,下面以这个为例子,分析左值、右值含义,通常情况下,哪些是左值哪些是右值。
5. 左值,右值和右值引用
6. 引用类型可以引用的的值类型
7. 全能类型,常量左值引用用途
7.1 拷贝构造函数
对C++程序员来说,编写C++程序有一条必须注意的规则,就是在类中红包含了一个指针成员的话,那么就要特别注意 拷贝函数的编写,因为一不小心,就会出现内存泄漏。
#include<iostream>
using namespace std;class HasPtrMem {
public:// 默认构造函数HasPtrMem():d(new int(0)) {}~HasPtrMem(){delete d;}int *d;};int main()
{HasPtrMem a;HasPtrMem b(a);cout<< *a.d << endl;cout<< *b.d << endl;
}
// 打印结果
free(): double free detected
💚💚 根据上图我们作如下分析
- 由于没有提供拷贝函数,C++会默认提供一个拷贝函数 (这个编译器源码编译后可以看出)
- 默认的拷贝函数类似于 memcpy按位拷贝,这样会造成一个问题:a.d 和 b.d 都指向一个内存地址
- 那么当main函数结束后,a和b 对象纷纷调用析构函数,当对象a 析构完毕之后,b.d 就变成了一个悬挂指针,不能指向有效的内存地址
如果在不小心的情况下,对此指针做解引用,那么就势必会引起严重的错误。
这个问题在C++中非常经典,这样的拷贝构造方式在C++上被称为 “浅拷贝” ,在位声明定义拷贝构造函数的情况下,C++会为每个类生成一个 浅拷贝构造函数。
💚💚 解决浅拷贝带来的问题 :自定义拷贝函数,实现深拷贝
我们为 HasPtrMem添加一个拷贝构造函数,拷贝构造函数从堆中分配内存,并将分配的新内存交还给 d,又用 *(h.d) 进行初始化,通过这样的方法很好的避免了 悬挂指针的困扰
7.2解决浅拷贝(深拷贝)
在章节7.1 中,拷贝构造函数为指针成员分配新的内存再进行内容拷贝的做法在C++编程中是不可违背的。
但是这个里面会有一个问题,这个问题就是:临时对象a 以及 对象的成员 int* d 指针指向的堆内容,但是又没有使用到,这就是一种浪费。
看下面这个例子
#include<iostream>
using namespace std;
class HasPtrMem {
public:HasPtrMem():d(new int(0)){cout<< "Construct: "<< ++ n_cstr<< endl;}HasPtrMem(const HasPtrMem& h):d(new int(*h.d)){cout<< "copy construct: " << ++n_cptr << endl;}~HasPtrMem(){cout<< "Destruct: " << ++ n_dstr<< endl;}int *d;static int n_cstr;static int n_cptr;static int n_dstr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr= 0;
int HasPtrMem::n_cptr= 0;
HasPtrMem GetTemp() {return HasPtrMem();}int main()
{HasPtrMem a = GetTemp();
}
// 编译选项 g++ -std=c++11 main_3.18.cpp -fno-elide-constructors
Construct: 1
copy construct: 1
Destruct: 1
copy construct: 2
Destruct: 2
Destruct: 3
💚💚 根据上面的打印可知:HasPtrMem 类构造函数调用了一次,拷贝构造函数调用了两次。析构函数调用了3次,为什么会出现这个情况了 。下面,我通过一张图来说明。
上图显示:GetTemp() 函数需要经历 两次拷贝函数,才可以让 对象 a 使用,由于拷贝函数进行了深拷贝,虽然解决了 指针悬挂问题,但是拷贝函数会 新建 堆内存(new int()) , 这是非常昂贵了。
- 可以想象一下如果类 HasPtrMem 成员 的指针指向非常大的堆内存,那么拷贝构造的过程就会非常昂贵。
- 更加令人堪忧的是,临时变量的产生和销毁对程序员是不可见的,并不会影响程序的运行,即使是性能有所下降,也不容易察觉。
8. 解决深拷贝问题
在第七节中,我们知道深拷贝带来临时对象复制,如果 一个类中存在指针变量,且指向非常大内存,那么在拷贝过程中必然会耗费内存,如果解决这个问题了,C++11 引入了右值和 move 语义,可以极大的提高性能,详细分析见我的下一篇文章。
move语义