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类型)。
- 内置类型:char int double float
- 自定义类型:class
- PUD类型:struct

struct 为什么可以作为介于两者的类型,而不与class同处于自定义类型呢?
这牵扯到 struct 与 class 的区别。从语法上看,class 定义类型如果不加声明,就是私有的类型;struct 类型是默认公有的。但是它们还是有更深层次的区别的。
- class设计的是一个类型,最终实现的是一个对象,这个对象有两个部分:属性和方法。
- 我们使用 struct 的时候,如果在 struct 中只有数据,只是数据的集合,我们当其与内置类型一致,处理方法一致;如果 struct 中只有方法,只作为纯虚函数的集合,我们把它当成一个接口看待。
右值的特点:
-
字面常量。
-
函数返回过程中的将亡值。
将亡值中就存在了三种类型的将亡值,也就产生了不同的处理方案,导致优化的方式不团。虽然这些处理方案不同,但是终究是为了提高程序的效率。
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。
1367

被折叠的 条评论
为什么被折叠?



