❤️前言
大家好,今天我们要一起来学习C++对于C的重要拓展——引用。
正文
C++中的引用在语法上是一个变量(或对象)的别名,它没有自己的空间而与引用的变量(对象)共用一个内存空间。可以说,C++的引用是在C指针的基础上进行的一种重要扩充,在使用上两者有很多相似性。接下来让我们一起来了解C++引用的用法。
基本用法
引用的基本用法就是通过将声明符写成 类型名& 引用变量名(对象名) = 引用实体,这样定义了引用类型之后,我们就可以用这个引用来对这个变量(对象)进行访问和操作,也就是相当于我们为这个变量(对象)起了一个“别名”。
// 将b,c,d都定义为a的引用
int a = 0;
int& b = a;
// 相当于引用之间的赋值
int& c = b;
int& d = c;
// 让我们看看它们的内存地址是否一致
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
代码的结果如下:
可以看到,如果我们对变量(对象)本身和其引用取地址,得到的结果是一致的,这和我们对于引用是引用实体的别名,且它们共用一块空间的理解如出一辙。(虽然在编译的时候引用是类似指针的实现,但是我们编写程序时只需要认为引用与引用实体共用一块空间即可)
引用的特性
引用有一些使用上的特性:
- 引用在定义的时候就需要初始化
- 一个变量可以具有多个引用
- 我们不能改变一个引用的指向
一般来说在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序吧引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,所以我们必须在定义引用时就将它初始化。
// b、c、d都是a的引用
// 当我们定义引用时必须初始化
int a = 0;
int& b = a;
int& c = b;
int& d = c;
// 我们无法改变引用的指向
// 这样做是对引用实体进行赋值
int x = 1;
b = x;
b++;
cout << "a = " << a << endl;
cout << "x = " << x << endl;
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
运行后我们得到以下结果:
我们可以看到,a的值由0变成了2,并且x的值并没有发生改变,此时我们对a和b取地址,便会发现它们的地址依然是一致的。
常引用
上面的内容中我们了解到了对于对于一般的变量的引用,现在我们来看对于常量的引用。
const int a = 0;
// 如果我们直接用int&引用a,编译器会报错,因为a是个常量
// int& b = a;
// 正确的写法应该是这样:
const int& b = a;
// 当我们定义常引用,我们无法通过这个引用改变引用实体
// 也就是相当于这个引用对这块内存的访问权限小于等于引用实体
// 直接写出的常量也必须用const修饰的引用
const int& c = 0;
double d = 1.23;
// 由于数据类型不同,我们无法这样引用
// int& x = d;
// 但是这样进行引用却可以通过,这是为什么呢?
const int& x = d;
// 让我们看看两者的地址
cout << "&x = " << &x << endl;
cout << "&d = " << &d << endl;
结果如下:
我们可以发现,这个常引用 x 的地址与 d 的地址并不相同(这一点我们通过无法直接跨类型引用可以推断出),那么这个x是对什么东西的引用呢?
这里我们联系一下之前的一些内容,当我们用不同类型的变量进行赋值时,编译器会对要赋的值进行截断和提升,也就是之前所说的隐式类型转换。我们知道,发生隐式类型转换时的原值是不会发生变化的,那么编译器是以什么东西进行运算呢?其实,编译器在进行隐式类型转换时,会先拷贝出一个临时变量,然后对这个临时变量进行操作,最后将操作后的值再赋给需要赋值的变量,而这个临时变量具有常性,也可以说它是一个常量,那么我们要定义一个常量的引用就需要加上const,这样就成为一个常引用,也就是说这里的x代表的其实是那个临时变量。
引用的使用场景
与指针类似,我们经常会在自定义函数时用到引用,我们首先来看引用做函数参数的场景,这里就以之前C语言构造单链表的尾插来举例子:
// 之前的写法:
// 单链表尾插
// 尾插时有可能需要改变头指针的值,因此我选择传入二级指针作为参数
void SListPushBack(SListNode** pplist, SLTDataType x)
{
SListNode* newnode = BuySListNode(x);//申请一个节点
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SListNode* cur = *pplist;
while (cur->next)
{
cur = cur->next;
}
cur->next = newnode;
}
}
// 现在的写法:
// 传参中有对指针的引用
void SListPushBack(SListNode*& plist, SLTDataType x)
{
SListNode* newnode = BuySListNode(x);//申请一个节点
if (plist == NULL)
{
plist = newnode;
}
else
{
SListNode* cur = plist;
while (cur->next)
{
cur = cur->next;
}
cur->next = newnode;
}
}
我们会发现,引用似乎比我们曾经使用的较为复杂的多级指针更加方便,但是引用在作为参数方面目前来看对于指针传参似乎没有很明显的提升。
现在我们再来看引用类型在作为返回值时的表现,这里以顺序表的位置插入为例:
// 假设已经做过命名空间的声明
// 这段代码的结果是什么呢?
// C++中结构被升级成了类,其中可以定义函数
struct List
{
int a[100];
int size;
// 访问数组中的数值
int& Visit(List& list, int pos)
{
assert(list.size > 0 && list.size <= 100);
return list.a[pos];
}
};
int main()
{
List list;
list.size = 1;
// 找到该数组中的0号元素并返回其引用
int& ret = list.Visit(list, 0);
// 对引用进行赋值
ret = 233;
cout << list.a[0] << endl;
return 0;
}
其结果:
我们发现这种返回引用然后访问其引用实体的方法十分的方便,有了这种对引用的使用方式,想必大家能在各种各样的场景下对我们之前的C代码进行很好的优化,让其使用和代码都更加简洁。
注意:当我们返回引用时,需要注意该引用所使用的空间是否随着函数栈帧的销毁(向操作系统归还内存的使用权)而销毁了,如果是这样就会产生“野引用”的错误,就像是野指针一样,使用它会造成非法的访问,十分危险!
引用的效率
引用的效率方面与指针相差不大,当它们作为传入参数时,可以节省对于大对象传入参数的消耗,当它们作为返回值时,都可以用节省拷贝次数,提高效率(因为返回一个变量时,编译器会生成一个临时变量,并将返回值拷贝给这个临时变量,而返回引用或指针就不会发生这种情况)。
引用和指针
引用和指针有很多相似之处,也有很多不同之处,相同之处我们根据上面的学习可以发现,它们的很多使用方式其实是大同小异的,现在我们罗列一下它们的不同点:
- 引用概念上定义一个变量的别名,语法上引用不开辟空间,而指针存储一个变量地址,需要开辟空间
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有空引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位平台下占8个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要解引用操作符,而引用是由编译器自己处理
- 引用比指针使用起来相对更安全
🍀结语
在本文与大家一起探讨了C++中引用的相关知识,谢谢大家的阅读,衷心希望大家生活愉快,天天开心!