【C++】深入理解引用

C++引用深究

有关引用的基本认识可以查看之前文章:
(1)const和引用的关系 (2)const和引用的关系(3)拷贝构造函数


1、引用数组

1.1 引用和数组的基本用法

int main()
{
    int arr[10] = {19,23,66,32,32,1,3,32,1,10};
    
    int &a = arr[0];			//引用数组的某个元素
    
    int &b = arr;				//error:b去引用a。与构成数组的要素不同,数组名 + 数组长度。
    
    //指针数组
    int(*p)[10] = &arr;			//首先是一个指针,大小是10.类型 + 大小
    
    //引用数组
   	int (&br)[10] = arr;
}

问题1:那么引用数组的含义是什么呢?

回答:可以和模板函数结合来进行对数组的操作。

1.2 模板函数定义

首先看一下这段代码:

int main()
{
    int arr[] = {1,3,4,5,3,8,2,3,5};
    double dx[] = {1,2,3,4,5,6,7,3,4,4,5,5,4,3};
    
    //接下来调用函数打印arr和dx
    
    Print_Arr(arr);		//引用数组,类型属性和元素个数。
    Print_Arr(dx);
    
    return 0;
}

我们想使用Print_Arr函数来打印arr数组和dx数组的全部内容,但是接口只是将数组名传递给函数本身,并没有传递数组长度(我们也不知道数组长度)。那么我们该如何定义Print_Arr函数呢?我们通过模板来定义Print_Arr函数。

template<typename Type,int N>		//类型概念,非类型概念
void Print_Arr(Type (&arr)[N])
{
    for(int i = 0; i < N; i++)
    {
        cout << arr[i] << "  ";
    }
    cout << endl;
}

首先我们看模板的定义:template<typename Type,int N>

typename Type是类型概念,int N是非类型概念

上述代码为Print_Arr函数的定义,它在编译的时候会实例化,并且进行模板推演过程,首先类型概念会自动将int 和 double类型 替换进模板函数中;非类型变量在替换过程中类似于,会将原先变量存在的地方进行值替换。如下:

//处理 int类型数组 的函数

typedef int Type;
void Print_Arr<int ,7>(Type(&arr)[7])	//N被7替换
{
    for(int i = 0; i < 7; i++)			//N被7替换
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

//处理double类型数组的函数
typedef double Type;

void Print_Arr<double,7>(Type(&arr)[7])	//N被7替换
{
    for(int i = 0; i < 7; i++)			//N被7替换
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

过程图如下:

在这里插入图片描述

注意:此处的7都不是变量,我们可以将它理解成宏,编译的时候不存在N这个变量,我们用数值常量将非类型变量替换掉。

数组是由两个部分构成的:数组名和长度。

1.3 模板类的定义

下次更新。

2、左值引用,右值引用,将亡值引用

2.1、左值引用:

凡是可以寻址的值叫做左值。

int main()
{
    int a = 10;			//left
    const int b = 20;	
    //&b;		可以取地址,就是左值		//20是右值,不可以取地址。
    
    int &c = a;
    
}

2.2、右值引用:

不可寻址的值叫做右值。

问题:如何引用字面常量:

int main()
{
    int &a = 10			
}

不能使用&a引用10,因为10是右值,不可以使用左值引用来引用10.

​ ① 我们可以使用万能引用(const)

int main()
{
    const int& a = 10;
    
    //常引用底层实现方式:定义一个临时空间,用指针a来指向这个空间。
    //int tmp = 10;
    //const int *const a = &tmp;
    
}

​ ② 也可以使用&&b(右值引用)来引用10:

int main()
{
    int &&b = 10;			//rvalue
    
    //底层实现方式:
    //b = 10;
    //int tmp = 10;
    //int *const b = &tmp;
}

注意:此处不是引用的引用,没用引用的引用。这个引用我们称之为右值引用

右值引用的引入目的:

​ C++中,不同的类型有着不同的处理规则。这个处理规则可以分为三大类:内置类型、自定义类型、介于自定义和内置类型的类型(PUD类型)。

  1. 内置类型:char int double float
  2. 自定义类型:class
  3. PUD类型:struct

在这里插入图片描述

struct 为什么可以作为介于两者的类型,而不与class同处于自定义类型呢?

这牵扯到 struct 与 class 的区别。从语法上看,class 定义类型如果不加声明,就是私有的类型;struct 类型是默认公有的。但是它们还是有更深层次的区别的。

  • class设计的是一个类型,最终实现的是一个对象,这个对象有两个部分:属性和方法。
  • 我们使用 struct 的时候,如果在 struct 中只有数据,只是数据的集合,我们当其与内置类型一致,处理方法一致;如果 struct 中只有方法,只作为纯虚函数的集合,我们把它当成一个接口看待。

右值的特点:

  1. 字面常量。

  2. 函数返回过程中的将亡值。

    将亡值中就存在了三种类型的将亡值,也就产生了不同的处理方案,导致优化的方式不团。虽然这些处理方案不同,但是终究是为了提高程序的效率。

2.3、将亡值引用

int fun()
{
    int a = 10;
    return a;
}

int main()
{
    int x = fun();
    
    int &b = fun();				//error;
    
    const int &c = fun();		//常引用
    
    int &&d	= fun();			//右值引用,没问题
}

上述main函数中的四种函数的接收值分别是:普通变量,整型引用,常引用,右值引用。

我们在下方逐一分析:

  • 普通变量

    int fun()
    {
        int a = 10;
        return a;
    }
    
    int main()
    {
        int x = fun();
        cout << x << endl;
        
        return 0;
    }
    

    输出结果:

    在这里插入图片描述

    分析流程:首先我们在主函数中调用fun函数,进入fun函数return a的时候,将a返回给eax寄存器中,作为一个临时量存在。当返回到主函数的时候将临时量传递给变量x。

    在这里插入图片描述
    在这里插入图片描述

  • 整型引用

    int &b = fun();		//error;
    
  • 常引用

    如果以常引用来接收fun函数的返回值,情况又会不同。

    int fun()
    {
        int a = 10;
        return a;
    }
    
    int main()
    {
        const int &c = fun();
        cout << c << endl;
        
        return 0;
    }
    

    运行结果:

    在这里插入图片描述

    分析过程:

    在这里插入图片描述

    我们将在main函数的栈帧空间开辟一块地方,来存储fun函数的返回值a。接着,我们会将c引用这片空间。

    在这里插入图片描述

    注意:我们在此处引用的是一个将亡值,但是因为添加了const 限定,导致我们不可以改变main栈帧中存储的a。

  • 右值引用

    int fun()
    {
        int a = 10;
        return a;
    }
    
    int main()
    {
        int &&d = fun();
       	cout << d << endl;
        
        return 0;
    }
    

    运行结果:

    在这里插入图片描述

    分析过程:

    右值引用将将亡值的生存周期拉长,拉长到与d的名字的生存期相同。当d消失,它才消失。

    在这里插入图片描述

    问题:常引用和右值引用有什么区别呢?

    常引用由于添加了const限制,导致临时量的空间存储区域不可被修改,是只读的形式;

    右值引用与常引用的存储方式类似,但是没用const进行限制,导致临时两的空间存储区域可以被修改。

3、临时对象能否以引用返回

首先来回顾两个程序:详细情况可以查看之前的文章

3.1 不以引用返回

class Object
{
    int value;
public:
    Object ()   
    {
        cout << "create:" << this << endl;
    }                  
    Object (int x = 0):value(x) {cout << "create:" << this << endl;} 
    ~Object()                       
    {
        cout << "~Objcet() " << this << endl;
    }
    
    Object(const Object& obj) :value(obj.value)
    {
        cout << "Copy create:" << this << endl;
    }
    
    Object& operator=(const Object &obj)
    {
        //obja = objb = objc;		//连续赋值
        if(&obj != this)			//obj = obj
        {
            this->value = obj.value;
        }
        cout << this << "=" << &obj << endl;
        return *this;
    }

    int & Value()
    {
        return value;
    }

    const int &Value() const 
    {
        return value;
    }  
      
};

Object fun(const Object& obj)
{
    int val = obj.Value() + 10;
    Object obja(val);
    
    return obja;
}

int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);			//以值返回
    cout << objy.Value() << endl;
    cout << &objx << endl;
    return 0;
}

运行结果:

在这里插入图片描述

运行流程:

首先,我们构造处objx和objy对象。

接着,调用fun函数,fun函数定义如下:Object fun(const Object& obj)。可以发现,函数的形参是引用类型,所以我们传递objx参数给函数,并不用实例化一个新的对象,而是将objx的地址传递给形参。

在fun函数的栈帧空间中,创建一个val变量,又创建了一个obja对象。当函数返回的时候,由于函数返回值是Object类型,会产生一个将亡值对象,通过拷贝构造函数来创建。存放在main函数的栈帧中。

当结束fun函数的调用,obja对象会被析构。真正保存的obja值的是拷贝构造函数创建的将亡值对象,不必担心函数调用结束后返回不了。

在这里插入图片描述

注意:将亡值对象obja存储在main函数的栈帧中。

3.2 以引用返回

...
Object &fun(const Object& obj)			//此处函数返回值添加引用
{
    int val = obj.Value() + 10;
    Object obja(val);
    
    return obja;
}

int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);			//以值返回
    cout << objy.Value() << endl;
    cout << &objx << endl;
    return 0;
}

运行结果:

将程序fun函数改为引用类型,程序打印随机值。

在这里插入图片描述

运行流程:

在这里插入图片描述

3.3 解析等号运算符重载

Object& operator=(const Object &obj)
{
	//obja = objb = objc;		//连续赋值
	if(&obj != this)			//obj = obj
	{
		this->value = obj.value;
	}
	cout << this << "==" << &obj << endl;
	return *this;
}

Object &fun(const Object& obj)			//此处函数返回值添加引用
{
    int val = obj.Value() + 10;
    Object obja(val);
    
    return obja;
}

int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);			//以值返回
    cout << objy.Value() << endl;
    cout << &objx << endl;
    return 0;
}

上述代码可能会出现另一种运行结果:

在这里插入图片描述

问题1:为什么输出的结果还是10,不是随机数呢呢?

运行流程:

首先,主函数运行,我们创建了objx和objy对象,存储在main函数的栈帧中。

接着调用fun函数,创建fun函数的栈帧空间。我们将objx的地址传递给fun函数的形参obj。obj中存放的是objx的地址,然后定义val变量,创建obja对象。

fun函数结束时,由于返回值是引用类型,所以不会将该将亡值对象存储到一个临时空间中,而是将obja对象的地址作为返回值返回给主函数。当fun函数结束,fun函数申请的空间会进行回收,并且清扫空间。我们只在形参定义了一个引用,并没有定义任何变量,导致空间清扫的力度不大,原先的栈帧空间只清扫了下面的一部分。(10来个字节)导致obja对象原先的地址的值并没有任何变化。

接着调用赋值语句,会接着使用之前被销毁的空间。当执行到this->value = obj.value的时候,对象obja虽然已死亡,但是它存储的值并没有做出任何的变化,所以会打印出10。

在这里插入图片描述

问题2:那么如何进行足够空间清扫呢?

回答:对赋值函数进行足够的空间扰动,比如定义变量等

//对赋值函数进行足够的空间扰动,让它彻底清扫空间

Object& operator=(const Object &obj)
{
    int a = 100;
    int b = 100;
    int c = 100;
	//obja = objb = objc;		//连续赋值
	if(&obj != this)			//obj = obj
	{
		this->value = obj.value;
	}
	cout << this << "==" << &obj << endl;
	return *this;
}

运行结果:

在这里插入图片描述
清扫成功。

3.4 总结

在一个函数中,不允许将在此函数中构造的对象,进行引用返回。

的值并没有做出任何的变化,所以会打印出10。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值