引用、指针的一些小细节
先要简单了解一下值类别。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;
}

浙公网安备 33010602011771号