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

本文主要是介绍一部分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老师的课程。

这篇关于一部分cpp的新特性:左右值的深入理解、函数返回引用报错详解以及在此过程中涉及到的指针和引用的部分区别和一点点关于std::array的简单介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/669530

相关文章

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

MyBatis常用XML语法详解

《MyBatis常用XML语法详解》文章介绍了MyBatis常用XML语法,包括结果映射、查询语句、插入语句、更新语句、删除语句、动态SQL标签以及ehcache.xml文件的使用,感兴趣的朋友跟随小... 目录1、定义结果映射2、查询语句3、插入语句4、更新语句5、删除语句6、动态 SQL 标签7、ehc

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

Python版本信息获取方法详解与实战

《Python版本信息获取方法详解与实战》在Python开发中,获取Python版本号是调试、兼容性检查和版本控制的重要基础操作,本文详细介绍了如何使用sys和platform模块获取Python的主... 目录1. python版本号获取基础2. 使用sys模块获取版本信息2.1 sys模块概述2.1.1

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4