引用、指针的一些小细节

先要简单了解一下值类别。https://www.cnblogs.com/gqzz/p/18724082

引用

左值引用

左值引用的底层实现

在c++中,大部分情况下,引用实际上是通过常量指针实现的,可以认为在编译时,编译器将引用转换为了指针。
但最好还是通过概念理解,引用的使用是无关编译器的实现和优化的。

int i = 1;
int& ref = i;
ref = 0;
//与下面的代码在汇编等价
int i = 1;
int* const pi = &i;
*pi = 0;

因此,引用的本质就是所引用对象的地址,同样也会占用内存,但对于程序员是透明的。在例子中,引用ref存放的就是i的地址,但编译器屏蔽了这一层。
这时我们就能够理解引用的特性了:

  • 引用在定义时必须进行初始化,且无法更换绑定对象
  • 一个对象可以有多个引用
  • 左值引用要求类型严格匹配
  • 左值引用只能绑定到可修改的左值

对const的左值引用

  • 对const的左值引用可以绑定到可修改的左值,不可修改的左值。
  • 还可以绑定到右值,也可以绑定到类型不同的值。在这两种情况下,编译器将创建一个与引用类型相同的临时对象,使用该值对其进行初始化,然后将引用绑定到临时对象。
    考虑这个例子
#include <iostream>

int main()
{
    short bombs { 1 };

    const int& you { bombs };
    --bombs;

    if (you)
    {
        std::cout << 1 \n";
    }

    return 0;
}

结果将会输出1,因为you绑定在了一个临时变量而不是bombs。

  • 绑定到临时对象的 Const 引用可延长临时对象的生存期
    临时对象通常在创建它们的表达式结束时销毁。 const左值引用直接绑定到临时对象时,临时对象的生存期将延长以匹配引用的生存期。

按左值引用传递

  • 当函数需要修改大型对象或容器时,使用引用可以避免复制整个对象,从而提高效率;
  • 并且,通过引用传递允许我们更改参数的值;
  • 按引用传递只能接受可修改的左值参数

按const左值引用传递

  • 对 const 的引用可以绑定到可修改的左值、不可修改的左值和右值;
  • 还保证无法更改被引用的值;
  • 如果参数的类型与引用的类型不匹配,由于会发生临时对象的转换,可能导致意外的低效;

按引用返回

按引用返回返回一个绑定到所返回对象的引用,从而避免复制返回值。
程序员必须确保被引用的对象比返回引用的函数生命周期更长。否则,返回的引用将悬空,并且使用该引用将导致未定义的行为。
当使用static也需要注意:

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 }; // note: variable is non-const
    ++s_x; // generate the next id
    return s_x; // and return a reference to it
}

int main()
{
    const int& id1 { getNextId() }; // id1 is a reference
    const int& id2 { getNextId() }; // id2 is a reference

    std::cout << id1 << id2 << '\n';
    return 0;
}

这将输出:22。因为id1和id2都是对同一个变量的引用,因此修改该值时,所有引用都在访问修改后的值

  • 使用返回的引用分配/初始化普通变量会创建一个副本,然后复制到变量中
//将上一个程序修改部分
const int id1 { getNextId() };
const int id2 { getNextId() };

这将输出:12

  • 由 const 引用传递的右值可以由 const 引用返回。
#include <iostream>
#include <string>

const std::string& foo(const std::string& s)
{
    return s;
}

std::string getHello()
{
    return "Hello"; // implicit conversion to std::string
}

int main()
{
    const std::string s{ foo(getHello()) };

    std::cout << s;

    return 0;
}

在这个例子中,函数foo()中,由于getHello()返回的右值是通过const引用传递的,其生命周期将会与函数foo()中的s相同。
因此在主函数中对s能够正确初始化。

引用的类型推导

  • 类型推导会删除引用,如果需要的话需要在定义点重新应用引用;
  • 类型推导会删除顶层const(适用于对象本身),而不会删除底层const(适用于被引用或指向的对象);
  • 需要特别注意,类型推导的过程是首先删除引用,然后考虑是否重新应用,然后删除这个结果中的所有顶层const。因此,即使引用只存在底层const,删除引用可能会将底层const 更改为顶层const。
#include <string>

const std::string& getConstRef(); // some function that returns a const reference

int main()
{
    auto ref1{ getConstRef() };        // std::string (删除引用后,底层const变为了顶层const)
    const auto ref2{ getConstRef() };  // const std::string

    auto& ref3{ getConstRef() };       // const std::string& (因为重新适用了&,const依然是底层const)
    const auto& ref4{ getConstRef() }; // const std::string&

    return 0;
}

为了防止明确意图并预防错误,如果需要 const 引用,重新应用限定符。

  • constexpr 不是表达式类型的一部分,因此不能被auto推导

指针

指针反而没有这么多需要注意的东西。

  • 类型推导不会丢弃指针。但在这里有一点需要特别注意:当使用auto* 与 auto的区别。
#include <string>

int main()
{
    std::string s{};
    const std::string* const ptr { &s };

    auto ptr1{ ptr };  // const std::string*
    auto* ptr2{ ptr }; // const std::string*

    auto const ptr3{ ptr };  // const std::string* const
    const auto ptr4{ ptr };  // const std::string* const

    auto* const ptr5{ ptr }; // const std::string* const
    const auto* ptr6{ ptr }; // const std::string*

    const auto const ptr7{ ptr };  // error,添加了两次顶层const;auto推导出const std::string* (顶层const被忽略了),
	//在const auto中的const将会修饰auto 推导出的类型,这是一个顶层const,而剩下的那个const则是修饰整个变量,也是一个顶层const,这里就出现了重复
    const auto* const ptr8{ ptr }; // const std::string* ;const auto*中的const修饰指针指向的内容,是一个底层const ,而剩下的const修饰变量本身,是一个顶层const
	
    return 0;
}
posted @ 2025-02-21 15:12  名字好难想zzz  阅读(23)  评论(0)    收藏  举报