一部分cpp的新特性:左右值的深入理解、函数返回引用报错详解以及在此过程中涉及到的指针和引用的部分区别和一点点关于std::array的简单介绍

目录

写在前面

explicit 关键字

左值(left value)和右值(left value)

引用类型作为函数的返回值

std::array

总结

致谢


写在前面

  • 昨天博主完成了cpp基础的学习的最后一部分,cpp新特性,今天开始来逐一地把这些内容总结上传。

  • 本文带来的是explicit关键字详解,左右值的深入理解、函数返回引用报错详解以及在此过程中涉及到的指针和引用的部分区别。

  • 在总结的过程中,我发现之前感觉简单的部分实际上并不简单,听课和写代码调错误的感受并不一致,这也就是学习和复习本身的意义,输入和输出的对等才能实现真正意义上知识点的掌握,希望大家还是要dirty your hand。

  • 剩下的部分会很快总结并发出来,希望大家共同努力、共同进步。

explicit 关键字

  • 表示构造函数是显示的,不可以进行隐式转换,默认的构造方式是支持隐式构造的。

  • 下面举一个简单的例子:

  •  #include <iostream>#include <string>​class student{public://    explicit student(int age):_age(age), _name("unknown"){}//    explicit student(int age, const std::string& name):_age(age), _name(name){}student(int age):_age(age), _name("unknown"){}student(int age, const std::string& name):_age(age), _name(name){}​private:std::string _name;int _age;};int main() {//implicit constructionstudent st1 = 11;student st3 = {20, "asif"};//explicit constructionstudent st2(11);student st4(20, "asif");​return 0;}

  • 如果使用explicit关键字定义构造函数,则implicit construction会直接报错。


左值(left value)和右值(left value)

  • 首先我们找一个例子来简单地理解下什么是左值和右值

    • 首先定义一个函数返回一个integer 1, 这个返回值可以直接赋值给其他变量。

    • 但是当我们想给这个函数的返回值直接修改,我们就会报错error: lvalue required as left operand of assignment如下面代码14行所示。

    • 这就是一个右值,给我们的感觉就是右值是不可修改的。

    • /** this is a demo script explaining the difference between left and right value* */​#include <iostream>int demo(){int i=0;return i;}​​int main() {int j = demo();//    demo() = 12;//error: lvalue required as left operand of assignment//this is not a left value and cannot be modified​return 0;}

  • 计算机的多级缓存结构:

    • 从左到右速度依次降低,容量依次升高。

    • 寄存器(cpu register) <<==>> 内存(dram):断电就丢 <<==>> 磁盘(disk):断电不丢

  • 和左值和右值有什么关系呢?

  • 左值右值的细入理解:

    • 通俗来讲一个赋值语句中等号左边的是左值,等号右边的是右值。

    •  #include <iostream>​int main() {int a = 666;//  l  rint b = 888;//  l  r  rint c = a + b;​return 0;}

    • 但是这么看来有些数据(a, b)既是左值也可以是右值这是为什么呢?

    • lvalue - 表示一个在内存中有确定位置的对象(一个有具体地址的对象)意味着可以对一个左值进行取地址运算操作&lvalue

    • rvalue - 反之,右值表示一个没有具体确定内存的或者临时的对象,一般存储在寄存器中的对象,右值可以是变量,数组,函数也可以是类对象以及其成员和引用等。

    • 所有的左值都可以转换成右值,因为内存上的数据可以参与构建一个表达式形成一个临时变量。

    • 现在我们来看一看左值和右值在汇编中的表示可能会更加直观一点,直接把上面的代码汇编,然后我们主要看在main函数中的部分:

    •     .file  "main.cpp".text.globl main.type  main, @function
      main:
      .LFB0:.cfi_startprocendbr64pushq  %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq   %rsp, %rbp.cfi_def_cfa_register 6movl   $666, -12(%rbp)movl   $888, -8(%rbp)movl   -12(%rbp), %edxmovl   -8(%rbp), %eaxaddl   %edx, %eaxmovl   %eax, -4(%rbp)movl   $0, %eaxpopq   %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
      .LFE0:.size  main, .-main.ident "GCC: (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0".section   .note.GNU-stack,"",@progbits.section   .note.gnu.property,"a".align 8.long  1f - 0f.long  4f - 1f.long  5
      0:.string    "GNU"
      1:.align 8.long  0xc0000002.long  3f - 2f
      2:.long  0x3
      3:.align 8
      4:

    • 可以看到行14~15 是将666和888移动到了栈上,也就是给他们分配内存,对应与代码中的第四行和第六行,所谓rbp就是栈顶,-12和-8就是偏移量。

    • 行16~18就是将这两个数据移动到寄存器上并进行加法然后存储到eax寄存器中,可以看到在此过程中并没有保存任何数据到内存中,因此这其中的相关数据就是一个右值,即没有地址的临时变量。

    • 第19行就是把数据存储到栈上,也就是c的创建和赋值,因为这时c有了内存地址,所以这是一个左值。

    • 因此我们可以进一步理解为什么我们的cpp或者c语言中不存在a + b = c;这种操作呢?因为a + b是存储在寄存器中的一个右值,没办法通过内存偏移来修改这个临时变量。

  • 把一个函数的返回值变成一个左值:

    • 方法一,在函数中返回一个静态变量的引用

    • /** this is a demo script explaining the difference between left and right value* */​​#include <iostream>int& demo(){//change the variable into a static type//and return a referencestatic int i=0;return i;}​​int main() {demo() = 12;​return 0;}

    • 方法二,传入一个引用并把引用返回出去。

    • 这个方法我们在类运算符重载的友元篇讲过,即std::ostream& operator << (std::ostream &os, const student& right);这样可以允许连续赋值操作,类似的还有operator=的运算符重载,在此不再赘述了。


引用类型作为函数的返回值

  • 在cpp中引用是一个难点:

    • 当函数返回一个引用时,如果这是一个栈上的变量(局部临时变量),不能成为其他引用的初始值(因为出栈即被销毁,导致悬空引用dangling reference),也不可以作为左值使用(类似上一节最开始的例子)。

    • 返回静态变量或者全局变量,此时可以作为其他引用的初始值,且可以作为左值右值被使用。

    • 返回形参的引用作为返回值,链式编程,运算符重载经常使用。

  • 下面用例子逐个进行解释:

    • 首先我们查看正常返回值的函数的调用来看看他们的地址:

    • #include <iostream>​int case_01(){int i = 666;std::cout << "the address of i in case_01: " << &i << " value: " << i << std::endl;}​void test_01(){int res = case_01();//the returned value is a copy, whose address is totally different from the returned value in the function,//which was destroyed while the call of the function finishedstd::cout << "the address of i out of case_01: " << &res << " value: " << res << std::endl;}​int main() {test_01();return 0;}

    • 输出如下,不出所料完全不同,因为传出来是一个原栈上结果的拷贝。

    •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/thu/function_return_ref/cmake-build-debug/function_return_refthe address of i in test_01: 0x7ffe38d22ff4the address of i out of test_01: 0x7ffe38d23014​Process finished with exit code 0

    • 然后我们定义第二个测试,返回一个局部栈临时对象的引用,然后看看结果如何:

    •  #include <iostream>​int& case_02(){int i = 666;i++;std::cout << "the address of i in case_02: " << &i << " value: " << i << std::endl;return i;}​​void test_02(){int& res = case_02();//dangling reference//calling the reference will result in a corruption since dangling referencestd::cout << "the address of i out of case_02: " << &res << " value: " << res << std::endl;}​int main() {test_02();return 0;}

    • 在g++编译器中直接不给任何访问的机会了(在这个编译器中栈上临时变量的地址在销毁后会被置为0)。

    • 但是在visualstudio中有可能是可以访问的,甚至可以发现在调用后直接打印引用的值都是没有变化的,这是因为栈内存还没刷新,如果在获取这个引用后再做一些别的操作(比如重新调用一个其他的函数)马上就会发现这个引用中的内容变了(Martin 老师在课上进行了操作,但是在博主的电脑上无法复现了,各种编译器有各种的逻辑,但是中心思想就是这个操作是不对的)。

    • 输出结果如下:

    •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/thu/function_return_ref/cmake-build-debug/function_return_refthe address of i in case_02: 0x7ffe47f72184 value: 667​Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

    • 为了理解到底发生了什么,我们传入一个二级指针,通过这个指针参数将该局部变量的地址传递出去。

    • 这里有一点问题那就是为什么传入的是一个二级指针?

      • 如果传入的是一个一级指针,那么在进入这个函数内部就会拷贝一个这个指针,指向和参数相同的地方。

      • 然后在内部修改这个指针的指向,其实你修改的是拷贝,原来的指针根本就没有任何的变化。

      • 所以最后出去函数你的指针还是指向nullptr,一访问就报错,因为代码并没有按照我们思考的方法去执行。

      • #include <iostream>​int& case_03(int* ptr){int i = 666;ptr = &i;std::cout << "the address of i in case_02: " << &i << " value: " << i << std::endl;return i;}​void test_03(){int* ptr = nullptr;//here is changed for the pointer parameterint &res = case_03(ptr);//out of the function, the reference returned local variable will be destroyed, as a result, the memory of which will be writable and non-secure,//somehow programmer can access the value using pointer, but it will be overwritten soon//the address is same as what was in the function, but the contents changedstd::cout << "the address of ptr out of case_03: " << ptr << " value: " << *ptr << std::endl;//once more the dangling reference can not be accessedstd::cout << "the address of i out of case_03: " << &res << " value: " << res << std::endl;}​​int main() {test_03();​​return 0;}

      • 最后的输出结果如下,报了段错误,其实原因是我们的指针并没有被得到修改,还是指向nullptr,大家可以尝试一下debug非常清晰。

      •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/thu/function_return_ref/cmake-build-debug/function_return_refthe address of i in case_02: 0x7ffeb9524f24 value: 666​Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

      • 在这里可以看出,其实指针的参数和任何参数都一样,pass by value 都是传一个拷贝,有时候给我们感觉传递指针好像能够对原始数据进行修改其实是因为拷贝过后指针指向的地方是不变的,但是如果我们想修改的不是指针指向的内容而是指针的指向,那么马上就会体会到和普通变量pass by value一样的无力感,这个时候我们就传递二级指针就好了。

    • 所以现在我们就传入一个二级指针输出我们在函数中的临时变量的地址:

    •  #include <iostream>​int& case_03(int** ptr){int i = 666;*ptr = &i;std::cout << "the address of i in case_02: " << &i << " value: " << i << std::endl;return i;}​void test_03(){int* ptr = nullptr;int &res = case_03(&ptr);//out of the function, the reference returned local variable will be destroyed, as a result, the memory of which will be writable and non-secure,//somehow programmer can access the value using pointer, but it will be overwritten soon//the address is same as what was in the function, but the contents changedstd::cout << "the address of ptr out of case_03: " << ptr << " value: " << *ptr << std::endl;//once more the dangling reference can not be accessedstd::cout << "the address of i out of case_03: " << &res << " value: " << res << std::endl;}​​int main() {test_03();​​return 0;}

    • 输出结果如下:

    •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/thu/function_return_ref/cmake-build-debug/function_return_refthe address of i in case_02: 0x7ffcbe5d6ad4 value: 666the address of ptr out of case_03: 0x7ffcbe5d6ad4 value: 21998​Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

    • 我们看到这个指针被销毁了,返回的指针中携带的数据我们可以看出,里面存储的是一些垃圾值。而我们一旦访问引用结果就会出现悬空引用的未定义行为,直接导致崩溃。这是为什么呢?(下面回答来自chatgpt)

      • 指针行为:当通过指针访问局部变量的内容时,实际上是直接操作内存地址。即使局部变量已经离开其作用域并且其内存可能被释放或重用,指针仍然保持原来的内存地址。因此,尽管这是未定义行为,仍然有机会“偶然”访问到该内存地址上的数据(无论该数据是否已被覆盖或更改)。

      • 引用行为:引用是别名,当尝试通过引用访问局部变量时,编译器可能会采取更严格的内存访问验证措施。在某些编译器实现中,当引用的原始对象(这里是局部变量)不再存在时,尝试通过引用访问该对象可能会被识别为无效操作,并导致程序崩溃(如段错误)。

      • 未定义的行为:不论是通过指针还是引用,访问离开作用域的局部变量都是未定义的行为。这意味着编译器和运行时环境可以以任何方式处理这种情况,包括但不限于返回随机数据、导致程序崩溃、或者看似正常运行。

      • 安全性考虑:即使通过指针偶尔可以访问到数据,这种做法也是非常危险和不可靠的。因为局部变量的内存可能随时被操作系统或运行时环境回收或重用,所以在该内存位置上读取或写入数据可能会导致不可预测的行为或数据损坏。

      • 总结:虽然在某些情况下通过指针可以访问局部变量的内存,但这种行为是不安全和不可靠的。而通过引用可能因为编译器的内存访问检查而导致程序崩溃。在任何情况下,都应避免这种对局部变量的外部访问。

  • 总结来说,不要用临时局部变量的引用作为返回值,不论返回给一个引用,一个数据的初始化,还是作为一个左值,这种操作都是应该被避免的。


std::array

  • array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。

  • array是将元素置于一个固定数组中加以管理的容器。

  • array可以随机存取元素,支持索引值直接存取, 用[]操作符或at()方法对元素进行操作,也可以使用迭代器访问

  • 不支持动态的新增删除操作

  • array可以完全替代C语言中的数组,使操作数组元素更加安全!

  • #include <array>

array特点

  • array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。

  • 总体来说array和其他stl标准库中的数据结构一致,就是为了解决普通数组的安全问题的,其操作方法和标准库中的其他数据结构也类似,在此就不过多赘述了。


总结

  • 希望大家在学习过程中不要尝试百分百的复现操作,因为编译器各有各的逻辑,领会内容更重要。

  • 指针的操作很绕,还是要多多学习多多熟练。

  • 左右值的理解大家可以简单按照本文给出的内存地址有无来理解。

  • 对于函数返回值的理解其实根本逻辑就是变量本身的生命周期。

  • 标准库中的操作如果记不下来就去查一下cppreference也是可以的,大家应该把重点放在数据结构和算法的理解上。

致谢

  • 感谢各位的支持,祝大家的cpp水平越来越强。

  • 感谢Martin老师的课程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/660843.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI工具【OCR 01】Java可使用的OCR工具Tess4J使用举例(身份证信息识别核心代码及信息提取方法分享)

Java可使用的OCR工具Tess4J使用举例 1.简介1.1 简单介绍1.2 官方说明 2.使用举例2.1 依赖及语言数据包2.2 核心代码2.3 识别身份证信息2.3.1 核心代码2.3.2 截取指定字符2.3.3 去掉字符串里的非中文字符2.3.4 提取出生日期&#xff08;待优化&#xff09;2.3.5 实测 3.总结 1.简…

Unity SRP 管线【第九讲:URP 点光源与聚光灯】

文章目录 CPU数据搜集GPU数据使用光照计算 CPU数据搜集 我们只能支持有限数量的其他灯。并将这些灯光数据&#xff08;位置、颜色、阴影强度、方向光光源、灯光遮蔽Probe、灯光层级Mask&#xff09;发送到GPU以供场景中所有物体渲染使用。 //ForwardLights.cs 额外光源数量与…

字符串string详细使用(计算机考研复试上机知识点)

字符串去重&#xff1a; 字符串去重 #include <bits/stdc.h> using namespace std;int main() { a"11322";sort(a.begin(),a.end());a.erase(unique(a.begin(),a.end()),a.end());cout<<a<<endl;return 0; } //unique函数返回没有重复区间的区间…

uniform initialization(一致化初始化)

目录 std::initializer_list 和 std::array std::initializer_list 的作用&#xff1a; std::array 的作用&#xff1a; std::initializer_list 和 std::array 使用联系 标准库里面的initializer_list 使用 vector std::initializer_list 和 std::array std::initializer_…

RabbitMQ实战指南(二)—— 基本概念

RabbitMQ实战指南&#xff08;二&#xff09;—— 基本概念 RabbitMQ是一个开源的消息中间件&#xff0c;用于在应用程序之间进行可靠的消息传递。它遵循AMQP&#xff08;高级消息队列协议&#xff09;标准&#xff0c;支持多种编程语言和平台。 下面是RabbitMQ的基本概念&…

qt内存自动释放的两种情况

qt内存管理机制 QObject的parent 我们时常能看到QWidget或者其他的控件的构造函数中有一项参数parent&#xff0c;默认值都为NULL&#xff0c;例如&#xff1a; QLineEdit(const QString &contents, QWidget *parent nullptr); QWidget(QWidget *parent nullptr, Qt::…

vue3学习——初始化项目及配置

初始化项目 环境 node 16pnpm 8.0.0 命令 pnpm create vite进行以下选择 &#x1f447; – 项目名 – VUe – Ts – cd/目录 – pnpm run dev 浏览器自动打开 package.json 配置eslint 安装依赖包 pnpm i eslint -D npx eslint --init // 生成配置文件进行以下选择 &a…

【2024】大三寒假再回首:缺乏自我意识是毒药,反思和回顾是解药

2024年初&#xff0c;学习状态回顾 开稿时间&#xff1a;2024-1-23 归家百里去&#xff0c;飘雪送客迟。 搁笔日又久&#xff0c;一顾迷惘时。 我们饱含着过去的习惯&#xff0c;缺乏自我意识是毒药&#xff0c;反思和回顾是解药。 文章目录 2024年初&#xff0c;学习状态回顾一…

vue——实现多行粘贴到table事件——技能提升

最近在写后台管理系统时&#xff0c;遇到一个需求&#xff0c;就是要从excel表格中复制多行内容&#xff0c;然后粘贴到后台系统中的table表格中。 如下图所示&#xff1a;一次性复制三行内容&#xff0c;光标放在红框中的第一个框中&#xff0c;然后按ctrlv粘贴事件&#xff0…

掌上医院预约挂号缴费系统源码,与医院信息系统共享数据,实现在线预约挂号、移动支付、医保支付、检验检查报告查看、门诊病历查询等功能。

随着信息技术的发展和互联网的普及&#xff0c;越来越多的患者开始习惯于通过互联网获取医疗服务。网上预约挂号是近年来开展的一项便民就医服务&#xff0c;旨在缓解看病难、挂号难的就医难题&#xff0c;许多患者为看一次病要跑很多次医院&#xff0c;最终还不一定能保证看得…

MySQL数据控制语言DCL

MySQL数据控制语言DCL 目录 MySQL数据控制语言DCLDCL关键字1.事务事务的四大特性START TRANSACTION&#xff1a;开始事务ROLLBACK&#xff1a;回滚COMMIT&#xff1a;提交事务 2.用户权限CREATE USER&#xff1a;创建新的用户并指定权限DROP USER&#xff1a;删除用户ALTER USE…

深度强化学习(王树森)笔记10

深度强化学习&#xff08;DRL&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。本文在ChatGPT辅助下完成。 参考链接 Deep Reinforcement Learning官方链接&#xff1a;https://github.com/wangshusen/DRL 源代码链接&#xff1a;https://github.c…

LeetCode —— 17. 电话号码的字母组合

&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️…

基于人体姿态的目标追踪实现

文章目录 概要人体姿态识别人体关键点转检测框实时跟踪器概要 这篇博文简单介绍了如何实现基于人体姿态的多目标跟踪算法。主要分为以下几个步骤: 基于FastDeploy实现人体姿态识别;将人体姿态转化为检测框;基于SORT算法实现目标跟踪。人体姿态识别 参考Fastdeploy实现。 模…

Ubuntu系统硬盘分区攻略(磁盘分区、RAID磁盘阵列、RAID阵列、固态硬盘分区、机械硬盘分区、swap分区、swap交换分区)

文章目录 分区需求分区方案分区顺序相关疑问swap分区不是应该放在最后吗&#xff1f;我安装系统分区的时候&#xff0c;上面有available devices&#xff0c;下面有create software raid(md)&#xff0c;我该用哪个&#xff1f;我available devices下面有个893G的固态&#xff…

Selenium自动化测试 —— 模拟鼠标键盘的操作事件

鼠标操作事件 在实际的web产品测试中&#xff0c;对于鼠标的操作&#xff0c;不单单只有click()&#xff0c;有时候还要用到右击、双击、拖动等操作&#xff0c;这些操作包含在ActionChains类中。 ActionChains类中鼠标操作常用方法&#xff1a; 首先导入ActionChains类&#…

Python OpenCV实现图片像素区域缩放

Python OpenCV实现图片像素区域缩放 前言项目安装OpenCV和Pillow思路代码编写 前言 遇到一个要将大量图片缩放成统一规格的难题&#xff0c;并且这些图片周围还有很多空白像素&#xff0c;所以用Python实现一下。 项目 安装OpenCV和Pillow pip install opencv-python pip …

C++:异常体系

异常体系 异常1.C语言传统的处理错误的方式2.C异常概念3.异常的使用3.1异常的抛出和捕获3.2 异常的重新抛出3.3异常安全3.4 异常规范 4.C标准库的异常体系5.异常的优缺点 异常 1.C语言传统的处理错误的方式 终止程序&#xff0c;如assert&#xff0c;缺陷&#xff1a;用户难以…

MyBatisPlus的基本使用之QueryWrapper

QueryWrapper是MyBatis-Plus中的一个查询封装类&#xff0c;用于构建带有条件的查询语句。 1. QueryWrapper 使用普通的方式来设置查询条件&#xff0c;而不是使用Lambda表达式。 一系列方法设置查询条件。手动指定数据库表的列名作为方法的参数 select 设置查询的字段 eq、…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之TextClock组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之TextClock组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、TextClock组件 TextClock组件通过文本将当前系统时间显示在设备上。支持不同…