C++:深入理解C++11新特性:Chapter3:左值和右值


在C语言中,我们常常会提起左值(lvalue),右值(rvalue)这样的称呼,而在编译程序时,编译器有时也会报出错误信息中包含 左值,右值说法。不过左值、右值通常不是通过一个严谨的定义而为人所知。下面我通过这样一个例子,来引导大家认识: 左值,右值,左值引用,右值引用,常量左值引用

#include<iostream>

struct Copyable{
	Copyable() {
		std::cout<< "copied...." << std::endl;
	}
	Copyable(const Copyable &copy)
	{
		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 construct


Passs by reference:
// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
construct.....
copied construct

// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
Passs by reference_2:
construct.....
copied construct

	

上面的例子:我们用到了

  1. 函数形参为左值,然后将右值表达式作为实参绑定到左值
  2. 函数形参为右值引用,然后将右值作为实参绑定右值引用
  3. 函数形参为常量左值引用,然后将右值作为实参绑定到 常量左值引用

1. 将右值绑定到 左值

在这里插入图片描述
💚💚 这种绑定方式的特点:

  1. 函数 ReturnRvalue() 在运行结束后,返回值(右值临时变量)复制一份作为实参传递给 AcceptValue() 函数后就不会存活下去了。
    这样会导致:Copyable 这个对象构建两次。浪费内存。

2. 将右值绑定到 常量左值引用

在这里插入图片描述

💚💚

  1. AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
  2. 这个临时值的生命周期就和 AcceptRef() 生命周期一致

3. 将右值绑定到右值引用

在这里插入图片描述
💚💚

  1. AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
  2. 这个临时值的生命周期就和 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

在这里插入图片描述
💚💚 根据上图我们作如下分析

  1. 由于没有提供拷贝函数,C++会默认提供一个拷贝函数 (这个编译器源码编译后可以看出)
  2. 默认的拷贝函数类似于 memcpy按位拷贝,这样会造成一个问题:a.d 和 b.d 都指向一个内存地址
  3. 那么当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()) , 这是非常昂贵了。

  1. 可以想象一下如果类 HasPtrMem 成员 的指针指向非常大的堆内存,那么拷贝构造的过程就会非常昂贵。
  2. 更加令人堪忧的是,临时变量的产生和销毁对程序员是不可见的,并不会影响程序的运行,即使是性能有所下降,也不容易察觉。

8. 解决深拷贝问题

在第七节中,我们知道深拷贝带来临时对象复制,如果 一个类中存在指针变量,且指向非常大内存,那么在拷贝过程中必然会耗费内存,如果解决这个问题了,C++11 引入了右值和 move 语义,可以极大的提高性能,详细分析见我的下一篇文章。
move语义

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值