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

news/2024/10/22 13:17:28/

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 &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 constructPasss 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语义


http://www.ppmy.cn/news/70458.html

相关文章

chatGPT生成的:前端学习导航

MDN Web 文档&#xff1a;提供关于 HTML、CSS、JavaScript 等前端技术的详细文档和指南。W3Schools&#xff1a;提供在线教程&#xff0c;覆盖了 HTML、CSS、JavaScript 和其他前端技术的基础知识。freeCodeCamp&#xff1a;一个开源的学习平台&#xff0c;提供免费的编程课程和…

红黑树(RBTree)

红黑树的基本性质 &#xff08;1&#xff09;红黑树是每个结点都带有颜色属性的二叉查找树&#xff0c;颜色或红色或黑色。在二叉搜索树强制一般要求以外&#xff0c;对于任何有效的红黑树我们增加了如下的额外要求: 性质1. 结点是红色或黑色。 性质2. 根结点是黑色。 性质…

包管理工具:pnpm | 京东云技术团队

作者&#xff1a;京东零售 杨秀竹 pnpm 是什么 pnpm&#xff08; performant npm &#xff09;指的是高性能的 npm&#xff0c;与 npm 和 yarn 一样是一款包管理工具&#xff0c;其根据自身独特的包管理方法解决了 npm、yarn 内部潜在的安全及性能问题&#xff0c;在多数情况…

SQL 常用函数总结(二)

字符串处理函数 1. CONCAT() 函数功能&#xff1a;将两个或多个字符串合并成一个字符串。 函数语法&#xff1a; CONCAT(string1, string2, ...)string1、string2 等的数量可以是零个或多个&#xff0c;分别表示需要合并的字符串。 使用示例&#xff1a; 假设现在有一个名…

软件测试工程师的职业发展方向

一、软件测试工程师大致有4个发展方向: 1 资深软件测试工程师 一般情况&#xff0c;软件测试工程师可分为测试工程师、高级测试工程师和资深测试工程师三个等级。 达到这个水平比较困难&#xff0c;这需要了解很多知识&#xff0c;例如C语言&#xff0c;JAVA语言&#xff0c…

多尺度样本熵

多尺度样本熵及其MATLAB实现方法 随着人们对信号处理技术的不断深入研究和发展&#xff0c;在信号非线性、非高斯的情况下&#xff0c;熵的概念成为一种很重要的测量信号复杂度的度量方式。多尺度熵是指在多个尺度范围内测量信号复杂度的一种方法。本文将介绍多尺度样本熵的概…

什么是前端宏任务,什么又是前端微任务呢?一文读懂前端微任务宏任务。

在前端中&#xff0c;宏任务和微任务是异步任务的两种不同类型。 前端有很多中异步任务类型。 可以分为三类&#xff1a; 宏任务 定时器任务用户交互事件任务&#xff08;鼠标事件、键盘事件&#xff09;网络请求任务I/O操作任务&#xff08;读写文件&#xff09; 微任务 Pro…

TPC 网络通信基础(二)

文件下载利用 tcp原理 Ubuntu 20.04 python3.7 三个python文件 客户端.py 服务器.py 文件.py 客户端充当用户 服务器充当提供下载的服务端 客户端代码&#xff1a; import socketdef main():# 创建套接字tcp_socket socket.socket(socket.AF_INET,socket.SOCKET_…