C++ 引用分析
引用
- 左值引用,建立既存对象的别名
- 右值引用,可用于为临时对象延长生命周期
- 转发引用,保持函数实参的类别
- 悬置引用,对象生命周期已经结束的引用,访问改引用为未定义行为
- 值类别,左值,纯右值,亡值
- std::move, std::forward
类型推导
引用塌缩(折叠)
可以通过模板或者 typedef 中的类型操作构成引用的引用,但是C++不认识多个& 的,所以就产生一个规则,左值引用 &, 右值引用 &&,在结合的时候,可以把左值引用看作是显性基因,只要有左值引用,那么结合就折叠成左值引用,要两个都是隐形基因(&&)的情况,才不会进行折叠。
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
右值引用作为函数实参 的类型推导
- 左值引用 (模板参数为右值引用).
- 左值(普通函数调用)
写个小例子就可以看出效果了,普通函数的情况如下,模板的示例见 std::forward 分析
int foo(int &&arg) { std::cout << "int &&\n"; } // 不会被调用
int foo(int &arg) {std::cout << "int &\n";} // 两个函数只能存在一个
// int foo(int arg) { std::cout << "int\n"; }
int main() {
int &&rref = 1;
foo(rref); // int 或者 int &
}
指针与引用的联系与区别
指针和引用经常会一起出现,个人的理解
- 指针,存储地址的变量,能够存储任何的地址,自身也需要分配内存,比如 nullptr,并且能够任意修改(无cv限定情况)。
- 引用,对象或者函数的别名,必须初始化且不能修改,语义上不分配内存,故指针不能指向引用,反之,引用可以绑定指针(指针自身是具名对象)。但在实现上(gcc)还是会分配内存
通过一个例子就可以看的很清楚,两者都是 访问地址 来实现的,但由于历史原因我们一说到地址就会想到指针。
void ref() {
int value = 13;
int &lref = value;
lref = 9;
int *p = nullptr;
p = &value;
*p = 21;
}
_Z3refv:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $13, -20(%rbp)
leaq -20(%rbp), %rax # 取 value 的地址 &value
movq %rax, -8(%rbp) # 将 value 的地址转移,这两步可以不需要的
movq -8(%rbp), %rax
movl $9, (%rax) # 赋值 lref = 9
movq $0, -16(%rbp) # 指针初始化
leaq -20(%rbp), %rax # 同上,取地址
movq %rax, -16(%rbp)
movq -16(%rbp), %rax
movl $21, (%rax) # 赋值 *p = 21
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
在使用上来说,引用优于指针的地方在于,引用避免了空指针的判断,并且在使用上和值语义相近。
google 的 coding style 上也有针对引用和指针参数的规范,入参如果不能够被改变的话,使用 const T &,如果是需要使用指针或者参数可变的情况下使用指针入参。
// 形式如下
void do_something(const std::string& in, char *out);
左值引用和悬置引用
左值引用的定义清晰,就是既存对象的别名,当作披着地址的皮来使用就可以,并且也能延长生命周期(const T & 接收),见延长右值引用分析。
悬置引用在使用不当的时候可能出现,如下
struct Foo {
Foo() : value(13) {}
~Foo() { value = -1; }
int value;
};
Foo &get_foo() {
Foo f;
return f;
}
int main() { Foo &f = get_foo(); }
// 反汇编,只截取 get_foo()
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
subq $24, %rsp
.cfi_offset 3, -24
leaq -20(%rbp), %rax // 对象 f 的地址
movq %rax, %rdi // 构造函数的隐藏参数
call _ZN3FooC1Ev // 调用构造函数
movl $0, %ebx
leaq -20(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev // 析构函数
movq %rbx, %rax // 最后返回的是 rax(rax = rbx),但是这个 rbx 是没有来源的,访问直接段错误
addq $24, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
当出现这种悬置引用的时候,再去访问就不知道是什么错误了,好消息是编译器可以识别这个问题并且发出警告的。
右值引用
右值引用就是为了延长生命周期而生的,这里再扯一下,左值引用也是可以做到这一点的,但是不能够通过左值引用修改。
拿一下 cppreference 中的例子,右值引用是通过 && 使得编译器指令重排而延长生命周期的,而左值引用是 const T & 进行py交易的,
在以上函数增加一个友元函数,重载 + 操作符。
friend Foo operator+(const Foo &lhs, const Foo &rhs) {
Foo foo;
foo.value = lhs.value + rhs.value;
return foo;
}
int main() {
Foo f1;
const Foo &lref = f1 + f1;
// rf.value = 1;
Foo &&rref = f1 + f1; // 临时变量 f1 + f2 的引用
rref.value = 4; // 相同
}
// 反汇编取重载函数和main函数代码
_ZplRK3FooS1_:
.LFB6:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq %rdi, -8(%rbp) // rdi 是构造函数的第一个参数,当函数返回对象时,就是这样做的
movq %rsi, -16(%rbp) // lhs
movq %rdx, -24(%rbp) // rhs
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN3FooC1Ev // 调用构造函数
movq -16(%rbp), %rax
movl (%rax), %edx // lhs.value
movq -24(%rbp), %rax
movl (%rax), %eax // rhs.value
addl %eax, %edx // edx = lhs.value + rhs.value
movq -8(%rbp), %rax
movl %edx, (%rax) // foo.value = edx
nop
movq -8(%rbp), %rax // return foo
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
main:
.LFB8:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
leaq -28(%rbp), %rax // 取 f1 的地址
movq %rax, %rdi
call _ZN3FooC1Ev // Foo f1;
leaq -24(%rbp), %rax // 重载函数内的 临时对象,当重载函数返回对象时,编译器便把对象指针传进去
leaq -28(%rbp), %rdx // rhs,f1
leaq -28(%rbp), %rcx
movq %rcx, %rsi // lhs,f1
movq %rax, %rdi
call _ZplRK3FooS1_ // 调用重载函数
leaq -24(%rbp), %rax
movq %rax, -8(%rbp)
leaq -20(%rbp), %rax // 第二次调用的重载函数内的 临时对象指针
leaq -28(%rbp), %rdx // rhs,f1
leaq -28(%rbp), %rcx
movq %rcx, %rsi // lhs,f1
movq %rax, %rdi
call _ZplRK3FooS1_ // 第二次调用重载函数
leaq -20(%rbp), %rax // 这两个值是相等的,也就是返回的临时对象指针
movq %rax, -16(%rbp)
movq -16(%rbp), %rax
movl $4, (%rax) // rref.value = 4;
leaq -20(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev // 析构函数被移动到作用域之外也就是main函数里面了
leaq -24(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev
leaq -28(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
可以看到 && 和 const T & 产生的汇编代码几乎是一样的,两者都提供了常量引用的语义,是编译器的实现也在函数返回对象的情况下模糊了这两者的区别(生成汇编代码),所以在有些情况下,在未提供 f(T &&) 重载则会调用 f(const T &)。但是区别在于常量左值引用是不可修改的。
一些函数提供了两个引用的重载版本,如 std::vector::push_back(),允许自动选择copy构造函数和移动构造函数。
值类别
左值
简单粗暴的理解就是在操作符的左边的表达式,但是C++的概念比较的多,例如,++i 这个是左值,i++ 就是纯右值了,字符串常量也没有想到是左值吧,因为不能修改,所以不能存在于表达式的左边。
cppreference 中的概念陈述的非常多,简单而言就是有分配内存的对象就是左值,只有这种情况才能够用于初始话左值引用(字符串常量,const char *)。纯右值
取不到地址的表达式,如内建类型值,this指针,lambda亡值
差不多可以理解为,作为一个临时量,内存中存在数据,如果不延长生命周期的话,该对象就会被销毁。std::move 产生的就是亡值。
然后上面的种类繁多,又有混合类别产生:
- 泛左值,左值和亡值,也就是内存有数据的对象
- 右值,纯右值和亡值,不能被左值引用绑定的对象
std::move std::forward
std::move
右值引用变量的名称是左值,而若要绑定到接受 右值引用参数的重载,就必须转换到亡值,这是移动构造函数与移动赋值运算符典型地使用 std::move 的原因。
函数名称和目的相关,但内部实现没有什么移动的操作,就一个转换类型,见 libstdcxx 源码。
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
std::forward
转发引用利用 std::forward 保持实参值类型进行完美转发,完美转发详细的说一下,它的实现也不是很复杂,有两个重载函数,实际上都是类型转换,
// 转发左值为左值或右值,依赖于 T
template <typename _Tp>
constexpr _Tp &&
forward(typename std::remove_reference<_Tp>::type &__t) noexcept {
return static_cast<_Tp &&>(__t);
}
// 转发右值为右值并禁止右值的转发为左值
template <typename _Tp>
constexpr _Tp &&
forward(typename std::remove_reference<_Tp>::type &&__t) noexcept {
static_assert(!std::is_lvalue_reference<_Tp>::value,
"template argument substituting _Tp is an lvalue reference type");
return static_cast<_Tp &&>(__t);
}
参考上面的 引用折叠 ,以下给定例子的参数类型推导:
template <typename T> void foo(const T &arg) { std::cout << "const T &\n"; }
template <typename T> void foo(T &arg) { std::cout << "T &\n"; }
template <typename T> void foo(T &&arg) { std::cout << "T &&\n"; }
template <typename T> void wrapper(T &&arg) { foo(std::forward<T>(arg)); }
int main() {
Foo f1;
const Foo f2;
wrapper(f1); // T &
wrapper(f1 + f1); // T &&
wrapper(f2); // const T &
}
- 若 wrapper 调用的入参为右值,则 T 被推导为
Foo, 这样std::forward就把右值引用转发给 foo - 若 wrapper 调用的入参为
const限定左值,则推导 T 为const Foo &,在引用折叠下std::forward将 const 左值引用传递给 foo - 若 wrapper 掉用的入参为非const左值,则推到 T 为
Foo &,在引用折叠下std::forward将非 const 左值引用传递给 foo
另外,对类型的推导过程都是在编译期完成的,不同的限定或者引用类型的c++代码生成的汇编代码没有区别,为了编译期匹配到正确的函数调用。
参考
- 引用声明,cppreference 引用声明。
C++ 引用分析的更多相关文章
- python的计数引用分析(一)
python的垃圾回收采用的是引用计数机制为主和分代回收机制为辅的结合机制,当对象的引用计数变为0时,对象将被销毁,除了解释器默认创建的对象外.(默认对象的引用计数永远不会变成0) 所有的计数引用+1 ...
- JVM内存回收对象及引用分析
自动垃圾回收是Java相较于C++的一个重要的特点,想了解JVM的垃圾回收机制,首先我们要知道垃圾回收是回收什么地方的垃圾,我在我的上一篇博客<JVM内存区域划分>里面有写到JVM里面的内 ...
- NSTimer定时器进阶——详细介绍,循环引用分析与解决
引言 定时器:A timer waits until a certain time interval has elapsed and then fires, sending a specified m ...
- Spring核心框架体系结构(jar包引用分析)[转]
很多人都在用spring开发java项目,普通添加lib目录拷贝jar包,或者创建maven项目时,配置maven依赖的时候并不能明确要配置哪些spring的jar,经常是胡乱添加一堆,编译或运行报错 ...
- jQuery 2.0.3 源码分析core - 整体架构
拜读一个开源框架,最想学到的就是设计的思想和实现的技巧. 废话不多说,jquery这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery ...
- 使用JProfiler进行内存分析
在最近的工作中,通过JProfiler解决了一个内存泄漏的问题,现将检测的步骤和一些分析记录下来,已备今后遇到相似问题时可以作为参考. 运行环境: Tomcat6,jdk6,JProfiler8 内存 ...
- 解决NSTimer循环引用Retain Cycle问题
解决NSTimer循环引用Retain Cycle问题 iOS开发中以下的情况会产生循环引用 block delegate NSTimer 循环引用导致一些对象无法销毁,一定的情况下会对我们横须造成影 ...
- JProfiler进行Java运行时内存分析
原文地址:https://www.cnblogs.com/onmyway20xx/p/3963735.html 在最近的工作中,通过JProfiler解决了一个内存泄漏的问题,现将检测的步骤和一些分析 ...
- C++解析(4):引用的本质
0.目录 1.引用的意义 2.特殊的引用 3.引用的本质 4.函数返回引用 5.小结 1.引用的意义 引用作为变量別名而存在,因此在一些场合可以代替指针 引用相对于指针来说具有更好的可读性和实用性 注 ...
随机推荐
- Python3使用过程中需要注意的点
命名规则 变量 变量名只能是数字.字母或下划线的任意组合 变量名的第一个字符不能是数字 不能使用关键字作为变量名 变量的定义要具有可描述性 变量名不宜过长.不宜使用中文.拼音 常量(常用在配置文件中) ...
- vue-learning:12-vue获取模板内容的方式
vue获取模板内容的方式 目录 outerHTML获取内容 template属性获取内容 ES6的字符串模板 <template>标签 <srcipt type="text ...
- IdentityServer4 sign-in
原文地址 Sign-in IdentityServer 代表 user 分配token之前,user必须登录IdentityServer Cookie authentication 使用 cookie ...
- sklearn各种分类器简单使用
sklearn中有很多经典分类器,使用非常简单:1.导入数据 2.导入模型 3.fit--->predict 下面的示例为在iris数据集上用各种分类器进行分类: #用各种方式在iris数据集上 ...
- 一篇文章带你了解 ZooKeeper 架构
上一篇文章,我们讲解了 ZooKeeper 入门知识,这篇文章主要讲解下 ZooKeeper 的架构,理解 ZooKeeper 的架构可以帮助我们更好地设计协同服务. 首先我们来看下 ZooKeepe ...
- 使用原生JDBC方式对数据库进行操作
使用原生JDBC方式对数据库进行操作,包括六个步骤: 1.加载JDBC驱动程序 在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM.可以通过java.lang.Class类的静态方法forNa ...
- DEVOPS技术实践_13:使用Jenkins持续传送设计-CD基础
1. 分支策略 持续集成中使用的分支策略包括以下三个: The master branch The integration branch The feature branch 而CD只在Integra ...
- $Poj1737\ Connected\ Graph$ 计数类$DP$
AcWing Description 求$N$个节点的无向连通图有多少个,节点有标号,编号为$1~N$. $1<=N<=50$ Sol 在计数类$DP$中,通常要把一个问题划分成若干个子问 ...
- BridgePattern(桥接模式)-----Java/.Net
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化.这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦
- 重新精读《Java 编程思想》系列之final关键字
在java中final关键字标识无法被修改.接下来从final修饰数据.方法和类进行介绍. final数据 final用来告知编译器这一块数据是恒定不变的.数据恒定不变又如下作用: 1.一个永不改变的 ...