C++引用探究

1 引用的本质

在我们的概念中,引用一直被灌输为别名,别名听起来就是不占用存储空间的了。然而事实呢?我们看下例子:

#include<iostream>
using namespace std;
class A
{
    int &a;
};
class B
{
};
int main()
{

    using std::cout;
    using std::endl;

    cout << sizeof(A)<<sizeof(B);
} 

输出:

[root@localhost c++]# ./reference
8
1 

可以看到引用是占用空间的,而且占用的是一个指针的空间。实际上引用的底层实现为const指针。引用初始化之后不能改变绑定对象,是因为const指针初始化之后不能修改值的原因。为了证明引用占用空间,我们也可以用特殊的方式来实现,以实现引用的重新绑定:

#include<iostream>
using namespace std;
struct ref
{
	ref(int &a):r(a){}
	int &r;
};
int main()
{

    int i=5;
    int j=6;
  
    //引用绑定到i
    ref ref(i);
    cout<<&ref.r<<endl;
    //获得引用ref对象的地址,实质是引用r地址(x64位8个字节)-->里面存储的是i的地址
    int** tmp = (int**)&ref;

    //打印为i的地址
    cout<<*tmp<<endl;

    //引用重新绑定到j -->存储地址改为j的地址
    *tmp=&j;

 
	cout<<&ref.r<<endl;

	cout<<"=================="<<endl;

    ref.r=10;
    cout<<i<<"  "<<j<<endl;

}

输出:

[root@localhost c++]# ./reference
0x7ffd0fb876e4
0x7ffd0fb876e4
0x7ffd0fb876e0
==================
5  10

可以看到引用ref.r重新绑定到了j。

2 右值引用

为了支持移动操作(包括移动构造函数和移动赋值函数),C++才引入了一种新的引用类型——右值引用,可以自由接管右值引用的对象内容。移动操作?右值?我们在这里先不解释概念,而是先举个例子:

#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
class RValueTest{
public:
	RValueTest(const char *s)
	{
		cout<<"char* constuct"<<endl;
		len = strlen(s);
		str = new char[len+1];
		memcpy(str,s,len);
	}


	//常规拷贝
	RValueTest(RValueTest &v)
	{
		cout<<"normal copy!!!!!!!!"<<endl;
		len = v.size();
		str = new char[len+1];
		memcpy(str,v.str,len);

	}


		//常规赋值
	RValueTest operator=(RValueTest &v)
	{

		len = v.size();
		str = new char[len+1];
		memcpy(str,v.str,len);

	}

	//移动拷贝
	RValueTest(RValueTest &&v)
	{
		cout<<"move copy!!!!!!!!"<<endl;
		
		len = v.size();
		str = v.str;
		v.str=nullptr;
	}

		//移动拷贝
	RValueTest operator=(RValueTest &&v)
	{
		cout<<"move =!!!!!!!!"<<endl;
		
		len = v.size();
		str = v.str;
		v.str=nullptr;
	}
	int size(){
		return len;
	}
	void showData()
	{
		cout<< str <<endl;
	}
	~RValueTest()
	{
		//cout<<"~RValueTest"<<endl;
		if(str!=nullptr)
		{
			cout<< (void*)str <<endl;
			cout<<"delete str: "<<str<<endl<<endl;
			delete[] str;
		}
	}
	char * str;
	int len;
};


RValueTest getA()
{
	RValueTest v("Halo World!");
	return v;

}
RValueTest& getB()
{
	RValueTest v("Halo World!");
	return v;

}

int main()
{
	RValueTest v1("Hello World!");

	RValueTest v2(v1);

	cout<<"======================================="<<endl;

	//这里调用一次移动构造,return 的时候一次
	getA();

	cout<<"======================================="<<endl;
	

	//这里调用两次移动构造,return 的时候一次,将retrun的值作为实参一次
	RValueTest v3(getA());

	cout<<"======================================="<<endl;


}

输出:

char* constuct
normal copy!!!!!!!!
=======================================
char* constuct
move copy!!!!!!!!
0x1896050
delete str: Halo World!

=======================================
char* constuct
move copy!!!!!!!!
move copy!!!!!!!!
=======================================
0x1896050
delete str: Halo World!

0x1896030
delete str: Hello World!

0x1896010
delete str: Hello World!

现在我们结合例子解释什么叫移动操作,这里以移动构造为例。在C++11之前,我们用一个现存对象构造新的对象,默认的构造函数都是浅拷贝的,如果我们需要深拷贝,则需要自定义构造函数,如构造v2,调用了深拷贝构造函数。但是如果现存对象是一个临时对象,我们构造好了新对象,临时对象就会被销毁,如这里的通过getA()函数返回的对象(return的时候讲局部对象拷贝到一个临时对象,临时对象再返回给调用者),这里就造成了多次申请释放str指向的内存。我们能不能直接让v3.str指向原来临时对象构造好的内存区域呢?默认浅拷贝就能实现了。然后这里的构造函数并不能区分实参是不需要销毁的长存对象,还是就要销毁的临时对象。这就是C++11引入右值引用的原因。

我们提前定义好处理临时对象的移动构造函数(参数为右值引用)和处理一般对象的深拷贝构造函数(参数为普通引用)。编译时,编译器判断对象是临时对象,则调用移动构造函数,直接将临时对象管理的内存转移到新的对象。判断是长存一般对象则调用深拷贝构造函数。

可以看到下面语句执行了一次移动构造函数

getA();

而下面语句执行了两次移动构造函数

RValueTest v3(getA());

我们这里解释下,需要了解return语句的实现原理,可以参考https://blog.csdn.net/jmh1996/article/details/78384083,我这里摘抄下结论:

return 栈对象的流程为: 

  1. 在调用返回一个对象的函数前,会在当前的函数所在的作用域生成一块栈内存区域用于存储一个匿名的对象,这个对象没有调用过构造函数。编译器将这个匿名对象的内存首地址作为一个参数压入栈顶,共该返回对象的函数使用。 
  2. return的时候,用return 后面表达式的对象作为参数来调用 1 中的匿名对象的复制构造函数。 
  3. 把匿名对象的地址存入eax 
  4. ret 返回原函数。

所以答案就明了了。getA()里面return返回RValueTest局部对象时,局部对象为临时对象,即将要被销毁,作为实参匹配到移动构造函数,构造匿名临时对象返回给调用者,这里执行一次移动构造函数;然后返回的临时对象作为实参,构造v3对象,匹配到移动构造函数,这里执行一次移动构造函数。所以上面语句分别执行一次和两次移动构造函数。


猜你喜欢

转载自blog.csdn.net/idwtwt/article/details/80782538