C++中的&符号的运用:引用、取地址和右值引用
C++中的&符号的运用:引用(从汇编层面分析)、取地址和右值引用
记一下笔记
C++中的引用
引用就是变量的别名
一个变量可以有多个别名
引用在声明时一定要初始化
引用的用法:
#include<bits/stdc++.h>
using namespace std;
int main() {
int a = 10;
int &b = a;
int &c = a;
b++;
cout << b << " " << a << " " << c << endl;
}
引用的汇编层面
/*int a = 10;
int &b = a;*/
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
也就是将a的地址赋予b,引用就是一个从变量获取到其地址后赋值给引用变量的过程
指针和引用的区别
1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
5.可以有const指针,但是没有const引用;
6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
7.指针可以有多级指针(**p),而引用只有一级;
8.指针和引用使用++运算符的意义不一样;
9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
C++中的取地址
&(取地址运算符)==>用来获取首地址,在给变量赋初值时出现在等号右边或在执行语句中作为一元运算符出现时表示取对象的地址.
#include<bits/stdc++.h>
using namespace std;
int main() {
int a = 10;
int *b = &a;
cout << b << " " << *b << endl;
}
C++ 中的右值引用
右值引用是C++11中新增加的一个很重要的特性,他主是要用来解决C++98/03中遇到的两个问题。
第一个问题就是临时对象非必要的昂贵的拷贝操作。
第二个问题是在模板函数中如何按照参数的实际类型进行转发。
通过引入右值引用,很好的解决了这两个问题,改进了程序性能,后面将会详细介绍右值引用是如何解决这两个问题的。
首先,对于以下这行代码
int i = getVar();
会产生两种类型的值
一种是左值i
一种是getVar()函数返回的一个临时变量,这个临时变量在表达式结束后就销毁了,这个临时变量是一个右值
我们可以对这个变量i进行右值引用
int &&i = getVar();
对于这一行代码,只会产生一种类型的值
即getVar()产生的临时变量,而这个临时变量会通过右值引用而延续他的声明周期,一直到i的生命周期结束
测试代码:
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A {
A() {
cout << "construct: " << ++g_constructCount << endl;
}
A(const A &a) {
cout << "copy construct: " << ++g_copyConstructCount << endl;
}
~A() {
cout << "destruct: " << ++g_destructCount << endl;
}
};
A GetA() {
return A();
}
int main() {
A i = GetA();
return 0;
}
由于编译器会自动优化临时变量,我们可以通过命令行编译使用-fno-elide-constructors编译选项的方式关闭编译器的优化,这样就可以看见临时变量的生命周期了
D:\buerdepepeqi>cd sublime
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors
D:\buerdepepeqi\sublime>test
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
测试2
int main() {
A &&i = GetA();
return 0;
}
这里处理了一个小bug就是
我本地的环境变量是之前用g++ 编译没有加上编译参数 -std=c++11,使得无法编译右值引用
报错如下:
error: expected unqualified-id before '&&' token
A && a = GetA();
加上std=c++11后
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct: 1
copy construct: 1
destruct: 1
destruct: 2
测试结果如上,我们可以清楚的看到右值引用后的临时变量的生命周期延长了,他是和右值一起消失的
这里还有有一个神奇的用法 ,通过常量左值引用也经常用来做性能优化 ,达到和右值引用相同的效果
int main() {
const A & a = GetA();
return 0;
}
因为常量左值引用是一个“万能”的引用类型
可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如这样的写法是不对的:
int main() {
A & a = GetA();
return 0;
}
因为非常量左值引用只能接受左值。
另外
右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。比如下面的例子:
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。(这里与移动语义和完美转发相关)
需要注意的是,仅仅是当发生自动类型推导(如函数模板的类型自动推导,或auto关键字)的时候,T&&才是universal references
转发
某些函数需要将其中一个或多个实参连同类型不变地转发给其他函数,
在这种情况下,我们需要保持被转发实参的所有性质,包括实参是否是const的、以及是左值还是右值。
只是普通的转发的情况下,eg:
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1 t1, T2 t2) {
f(t1, t2);
cout<<t1<<" "<<t2<<endl;
}
int main() {
int i = 1;
int j = 1;
f(42, i);
cout << i << endl;
func(f, 42, j);
cout << j << endl;
}
测试结果:
42 2
2
42 2
42 2
1
可以看到,挡调用函数func的时候,j并不会变化,这是因为j的值被拷贝到了t2中,t2改变了
当我们使用右值引用时
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1&& t1, T2&& t2) {
f(t1, t2);
cout<<t1<<" "<<t2<<endl;
}
int main() {
int i = 1;
int j = 1;
f(42, i);
cout << i << endl;
func(f, 42, j);
cout << j << endl;
}
我们将参数列表使用右值引用,
得到的结果是
42 2
2
42 2
42 2
2
j的值改变了,因为T1的值被推断为int &,而t1就被折叠为int &了,所以实现了参数的转发,保持了参数类型
当然也可以用forward来转发参数
forward需要显示提供实参类型,返回该实参类型的右值引用
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1 &&t1, T2 &&t2) {
f(forward<T1>(t1), forward<T2>(t2));
cout << t1 << " " << t2 << endl;
}
int main() {
int i = 1;
int j = 1;
f(i, j);
cout << i << " " << j << endl;
func(f,i, j);
cout << i << " " << j << endl;
}
类函数中的参数用右值引用避免指针悬挂等问题
一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数
因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。如果不提供深拷贝的拷贝构造函数,上面的测试代码将会发生错误(编译选项-fno-elide-constructors),内部的m_ptr将会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针悬挂问题。
而深拷贝会在有些时候会造成额外的性能损耗
例如:
#include <iostream>
using namespace std;
class A {
public:
A(): m_ptr(new int(0)) {
cout << "construct" << endl;
}
A(const A &a): m_ptr(new int(*a.m_ptr)) { //深拷贝的拷贝构造函数
cout << "copy construct" << endl;
}
~A() {
cout << "delete" << endl;
delete m_ptr;
}
private:
int *m_ptr;
};
A GetA() {
return A();
}
int main() {
A a = GetA();
return 0;
}
测试结果
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct
copy construct
delete
copy construct
delete
delete
Get函数会返回一个临时变量,然后通过这个临时对象拷贝构造了一个新的对象a
临时变量在拷贝构造完成之后就销毁了,如果堆内存很大的话,那么,这个拷贝构造的代价会很大,带来了额外的性能损失。
每次都会产生临时变量并造成额外的性能损失,有没有办法避免临时变量造成的性能损失呢?答案是肯定的,就是将深拷贝拷贝构造函数中的参数变为右值引用
#include <iostream>
using namespace std;
class A {
public:
A(): m_ptr(new int(0)) {
cout << "construct" << endl;
}
A(const A &a): m_ptr(new int(*a.m_ptr)) { //深拷贝的拷贝构造函数
cout << "copy construct" << endl;
}
A(A &&a) : m_ptr(a.m_ptr) {
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A() {
cout << "delete" << endl;
delete m_ptr;
}
private:
int *m_ptr;
};
A GetA() {
return A();
}
int main() {
A a = GetA();
return 0;
}
测试结果:
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct
move construct
delete
move construct
delete
delete
没有调用拷贝构造函数
参考博客