美文网首页
1. 引用和指针

1. 引用和指针

作者: 木头石头骨头 | 来源:发表于2019-08-22 18:27 被阅读0次

    引用

    参考文档

    在C++中,引用相当于为变量起了个别名。引用和指针一样,是一种复合类型(compound type),是指基于其他类型定义的类型。

    使用引用时需注意:

    1. 定义引用时,引用和它的初始值绑定(bind)在一起,不是拷贝,所以引用必须初始化。
    2. 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
    3. 使用引用时,直接用引用名,不能加&号。
    4. 引用的类型必须与之绑定的变量类型一样,而且,引用只能与对象绑定,不能和字面值,表达式绑定,引用与引用之间也不能绑定。
    int i = 0;
    int& ret = i;  // 绑定引用,引用声明后必须初始化
    

    右值引用

    C++11 引入了右值引用这个新概念。

    C++ 中所有的值都必然属于左值、右值二者之一。左值是值表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。

    哪些是右值呢?表达式是右值;函数返回值是右值;字面值是右值。

    所以右值引用就是可以绑定右值的引用。因此在 C11 后,原来的引用也称为左值引用。下面一段代码帮你区分左值和右值:

    int foo(){
      return 1;
    }
    
    int a = 1+2; // a 是左值,表达式 1+2 是右值
    int b = 1;   // b 是左值,字面值 1 是右值
    int c = foo();   // c 是左值,foo 的返回值是右值
    
    int& refA = a;    // refA 左值引用,绑定一个左值 a
    int& refA1 = 1+2; // refA1 左值引用,绑定一个右值,编译器报错
    
    int&& rrefA = 1+2; // rrefA 右值引用,绑定一个右值,编译通过
    

    注意:语法规定左值引用只能绑定左值,右值引用只能绑定右值;但是,常量左值引用却既可以绑定左值又可以绑定右值,比如:const int& ref = foo(); 它有右值引用的功能,不过它却只能读不能改。所以我们常用常量左值引用作为函数的形参,它即可接受左值,又可接受右值,而且还不会发生内存拷贝也避免更改原变量的值。

    右值引用的作用?

    1. 延长右值的生命:在没引入右值引用之前,一个赋值表达式,等号左侧的是左值,等号右侧的是右值。当赋值表达式执行完后,左值继续存在,而右值的生命也就终结了。所以右值引用延长了右值的生命,当一个右值绑定一个右值饮用后,它的生命周期就会继续下去。
    2. 通过移动语意,可以避免无谓的复制,提高程序性能。

    移动语意(move)

    当你自己去实现一个 MyString 字符串类的时候,你回去怎么做,考虑这个构造函数 MyString("hello"),当我们用右值 "hello" 去构造这个这个类的时候,传统的构造函数可以怎样写:

    MyString(const char* str){
      if(str){ 
          m_data = new char[strlen(str) + 1];
          strcpy(m_data, str.m_data);
      }
      else{
         m_data = new char[1];
         m_data[0] = '\0';
      }
    }
    

    这个过程为了避免浅拷贝,而不得不在构造的过程中进行内存拷贝,而对于右值 "hello" 这个右值,其实是不需要内存拷贝的,那么通过右值引用实现移动构造函数,就可以避免这样的内存拷贝。

    MyString(MyString&& str){
      m_data(str.m_data);
      str.m_data = nullptr; //不再指向之前的资源了
    }
    

    同样的,有些局部变量,是左值,但它们的生命周期很短,也想使用移动语意,那有没有办法呢?C++11 特地新增了一个标准库函数 std::move() 可以将左值转换成右值,允许这个左值使用移动语意。

    MyString str1("hello");
    MyString str2(std::move(str1)); // 将局部变量 str1,转换成右值,调用移动构造函数初始化 str2,
    

    注意:将左值转换成右值后,这个左值并没有析构,只是转交了它资源的所有权,之后的代码也不要再使用 左值了,因为它内部已经没有内容。

    除此之外,也常用 std::move 实现交换函数,这也是为了减少内存的拷贝。但一定要实现移动运算符 operator=

    通用引用

    我们查看 std::move 的代码的时候可以看到:

    template<typename _Tp>
    move(_Tp&& __t);
    

    不是说,move 是将左值引用转换成右值引用吗,参数应该是左值引用才对啊,这里怎么是个右值引用?其实不然,当 && 和模板类型相结合的时候,这里表示的是一个通用引用,也就是它是左值引用还是右值引用取决于它的初始化。如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。比如我们也可以将一个右值赋给 move 函数

    int&& a = std::move(1);
    

    这里只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个通用引用:

    int temp = 1;
    auto&& a = 1;    // 类型推导,a 是右值引用
    auto&& b = temp; // 类型推导,b 是左值引用
    

    注:auto 不能作为函数形参类型,但是可以作为函数返回值

    指针

    指针与引用类似,实现了对其他对象的间接访问。然而指针与引用又有很多不同点:

    1. 指针本身就是一个的对象,允许对指针赋值和拷贝,而且指针在其身命周期可以先后指向几个不同的对象。
    2. 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

    指针值

    指针的值(即地址)应属下列4种状态之一:

    1. 指向一个对象
    2. 指向紧邻对象所占空间的下一个位置
    3. 空指针,意味着指针没有指向任何对象
    4. 无效指针,也就是上述情况之外的其他值

    使用指针时需注意:

    1. 指针的类型与之指向的对象的类型一样,因为指针本身也是对象,所有可以有指向指针的指针,绑定指针的引用。
    2. 当试图拷贝或其他方式访问无效指针时,都将引发错误。因此建议初始化所有的指针。如果是在不清楚指针应该指向何处,就把它初始化为nullptr(空指针,C11新标准)或0,表示不指向任何对象(在 if 判断时,当成false处理)。
    3. 使用指针时,不带 * 号,意味着指针表示的内存地址的值,所以给指针赋值的时候不带 * 号。而带 * 号表示该指针指向的对象的值,所以改变它的值就是改变对象的值。
    4. 将一个指针直接赋值给另一个指针时,将会发生浅拷贝,也就是这两个指针指向同一段内存空间,对其中一个指针操作,另一个也会发生改变。比如delete,或free掉其中一个指针,那么这段内存空间将会释放,另一个指针将会变成无效指针,程序将会出错。C11提出了很多新概念解决指针拷贝的问题,比如智能指针(shared_ptr)移动等。

    void* 指针

    void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void* 指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址种到底是个什么类型的对象并不了解。

    void*指针能做的事:

    • 利用void*指针,可以和别的指针比较
    • 利用void指针,作为函数的输入或输出(free函数的参数就是void类型)
    • 利用void指针,赋给另一个void指针

    void*指针不能做的事:

    • 不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上所做的操作

    概况来说,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。比如 自己实现 memcpy 的实现:

    void *memcpy (void * __dest, const void * __src, size_t __n){
        void * ret = __dest;
        if (nullptr ==__dest||nullptr ==__src)
            return __dest;
            
        if(__dest == __src)
            return __dest;
            
        while (__n--) {
            *(char *)__dest = *(char *)__src;
            __dest = (char *)__dest + 1;
            __src = (char *)__src + 1;
       }
       return(ret);
    }
    

    NULL 与 nullptr

    在 C99 中定义了 NULL 代表空指针,它是个预处理变量,定义在头文件 cstdlib 中:

    #ifndef NULL
        #ifdef __cplusplus
            #define NULL 0
        #else
            #define NULL ((void *)0)
        #endif
    #endif
    

    所以在用 NULL 初始化指针和用 0 初始化指针是一样的。在新标准下,现在的 C++ 程序最好使用 nullptr,同时尽量避免使用 NULL。

    nullptr 是 C11 引入的新关键字,用以代替 NULL 预处理变量。nullptr 是一种特殊的字面值,它可以转换成任意其他的指针类型。

    在 C++ 程序中建议初始化所有指针。使用未经初始化的指针是引发运行时错误的一大原因。在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果该指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的。因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在分不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0,这样程序就能检测并知道它没有指向任何具体的对象了。

    用 nullptr 解决 NULL 不能解决的问题

    因为 NULL 本质上是 0,如果一个如下重载:

    #include <iostream>
    
    void func(int) {
        std::cout << "func1" << std::endl;
    }
    
    void func(void*) {
        std::cout << "func2" << std::endl;
    }
    
    int main()
    {
        func(NULL);
        func(nullptr);
        return 0;
    }
    

    运行结果:

    func1
    func2
    

    一般来说,我们传进去一个NULL,一般想的是要传一个指针,可是在上面的程序中,我们却调用的是int的版本。如果传进 nullptr,它符合我们的设想,这是因为C++规定nullptr可以转为指针类型。而且是 void * (void* 可用于存放任意对象,所以 nullptr 可以转换为任意类型的指针,自定义类型指针也可以)。

    因为 nullptr 在使用时候更智能,不是将空指针机械的转换为 0,所以在新标准下,空指针都初始化为 nullptr。

    与 const 相结合

    const 与引用结合

    int a = 1;
    int b = 2;
    const int& ref1 = a; // 只有这一种 const 结合方式
    ref1 = b;  // 错误,既不能改变 ref1 的绑定对象,也不能改变 ref1 绑定对象的值
    

    指针与数组

    字符串

    在 C++ 中表示字符串有两种方式:

    • 指针形式:char* str = "123ad*12"; str 表示指向这串字符串首地址的指针
    • 数组形式:char str[] = "123ad*12"; sstr 表示一个字符数组

    这两种表示方法肯定是有所区别的,一个是指向字符串首地址的指针,一个是数组,但它们都是以 '\0' 结尾;在 C11 标准之后, 指针形式的字符串就不在允许了,虽然有的编译器还是可以通过,但还是推荐使用数组形式作为字符串的容器。

    判断下面程序的输出:

    void IsTheSameString(){
        char str1[] = "hello world";
        char str2[] = "hello world";
    
        char* pStr1 = "hello world";
        char* pStr2 = "hello world";
    
    
        if(str1 == str2)
            std::cout << "str1 == str2" << std::endl;
        else
            std::cout << "str1 != str2" << std::endl;
    
        if(pStr1 == pStr2)
            std::cout << "pStr1 == pStr2" << std::endl;
        else
            std::cout << "pStr1 != pStr2" << std::endl;
    }
    

    输出:

    str1 != str2
    pStr1 == pStr2
    

    原因:

    • str1 和 str2 是两个字符串数组, 分别分配 12 个字节的存储空间, 并把字符串内容复制了进去
    • pStr1 和 pStr2 没有分配内存空间, 他们只是都指向了 hello world 这个字符的地址

    指针与数组

    通过字符串的例子,我们可以看到指针和数组的相似之处。我们同样可以用一个指针去指向一个数组:

    int a[5] = {1,2,3,4,5};
    int* p = a;
    int* pArray = new int[5];  // 动态数组
    

    上面是一位数组的情况,如果是二维数组:

    int a[5][2] = {{5,2},{2,6},{5,6},{7,8},{1,9}};
    int (*p)[2] = a; // 需要指定第二个下标的大小,相当于一个二维指针
    

    指针的加减法

    我们知道指针的字面值实际上是一个整形,如果它指向的是一段连续的内存空间(数组),那么它的加减法就有意义。

    int a[5][2] = {{5,2},{2,6},{5,6},{7,8},{1,9}};
    int (*p)[2] = a;
    p += 1;
    
    int* p2 = &a[0][0];
    p2 += 1;
    

    通过单步调试这个程序,当将 p + 1 后,它指向的值是 {2,6},二维矩阵中的第二个数组,它的地址的值增加了 8。相应的 p2 指向了 2,指针值增加了4。 所以我们可以看到,指针 +1 不是它的字面值 +1,而是表示指向连续地址中的下一个地址。这里的每个元素有两个 int ,所以下一个元素的地址自然 +8。

    const 与指针结合

    int* const p2 = &a;  // const 修饰 p2 的值,所以理解为 p2 的值不可以改变,即 p2 只能指向固定的一个变量地址
    p2 = &b;  // 错误,p2 不是可修改的左值
    const int* p3 = &b;  // 顶层 const,顶层指针表示指针本身是一个常量
    *p3 = 4;  // 错误,*p3 不是可修改的左值
    

    const 与函数结合

    一个函数:① const Stock & Stock::topval (②const Stock & s) ③const
    ①处const:确保返回的Stock 对象在以后的使用中不能被修改
    ②处const:确保此方法不修改传递的参数 S
    ③处 const:保证此方法不修改调用它的对象,const 对象只能调用 const 成员函 数,不能调用非const 函数

    什么时候使用引用当参数,什么时候用指针当参数?

    使用引用参数的主要原因有两个:

    1. 程序员能修改调用函数中的数据对象
    2. 通过传递引用而不是整个数据–对象,可以提高程序的运行速度

    一般的原则

    对于使用引用的值而不做修改的函数:

    1. 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
    2. 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
    3. 如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
    4. 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)

    对于修改函数中数据的函数:

    1. 如果数据是内置数据类型,则使用指针
    2. 如果数据对象是数组,则只能使用指针
    3. 如果数据对象是结构,则使用引用或者指针
    4. 如果数据是类对象,则使用引用

    相关文章

      网友评论

          本文标题:1. 引用和指针

          本文链接:https://www.haomeiwen.com/subject/bxigsctx.html