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

目录

写在前面

explicit 关键字

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

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

std::array

总结

致谢


写在前面

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

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

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

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

explicit 关键字

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

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

  •  #include 
     #include 
     ​
     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 construction
         student st1 = 11;
         student st3 = {20, "asif"};
         //explicit construction
         student 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 
       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 
       ​
       int main() {
           int a = 666;
           //  l  r
           int b = 888;
           //  l  r  r
           int c = a + b;
       ​
           return 0;
       }

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

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

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

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

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

    •     .file  "main.cpp"
          .text
          .globl main
          .type  main, @function
      main:
      .LFB0:
          .cfi_startproc
          endbr64
          pushq  %rbp
          .cfi_def_cfa_offset 16
          .cfi_offset 6, -16
          movq   %rsp, %rbp
          .cfi_def_cfa_register 6
          movl   $666, -12(%rbp)
          movl   $888, -8(%rbp)
          movl   -12(%rbp), %edx
          movl   -8(%rbp), %eax
          addl   %edx, %eax
          movl   %eax, -4(%rbp)
          movl   $0, %eax
          popq   %rbp
          .cfi_def_cfa 7, 8
          ret
          .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 
       int& demo(){
           //change the variable into a static type
           //and return a reference
           static 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 
       ​
       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 finished
           std::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_ref
       the address of i in test_01: 0x7ffe38d22ff4
       the address of i out of test_01: 0x7ffe38d23014
       ​
       Process finished with exit code 0

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

    •  #include 
       ​
       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 reference
           std::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_ref
       the address of i in case_02: 0x7ffe47f72184 value: 667
       ​
       Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

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

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

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

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

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

      • #include 
         ​
         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 parameter
             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 changed
             std::cout << "the address of ptr out of case_03: " << ptr << " value: " << *ptr << std::endl;
             //once more the dangling reference can not be accessed
             std::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_ref
         the 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 
       ​
       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 changed
           std::cout << "the address of ptr out of case_03: " << ptr << " value: " << *ptr << std::endl;
           //once more the dangling reference can not be accessed
           std::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_ref
       the address of i in case_02: 0x7ffcbe5d6ad4 value: 666
       the 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和其他stl标准库中的数据结构一致,就是为了解决普通数组的安全问题的,其操作方法和标准库中的其他数据结构也类似,在此就不过多赘述了。


总结

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

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

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

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

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

致谢

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

  • 感谢Martin老师的课程。

你可能感兴趣的:(c++学习,c++,开发语言,学习,笔记)