目录
一、前言
二、引用
1、引用的概念
2、引用特性
3、使用场景
3.1、做参数
3.2、做返回值
4、传值、传引用效率比较
值和引用作为参数的性能比较
值和引用作为返回值类型的性能比较
5、常引用
6、引用和指针的区别
一、前言
上一篇文章我们讲解了 C++ 的命名空间、缺省参数、函数重载等内容,接下来我们继续讲解引用相关的知识。
二、引用
1、引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
需要注意的是:引用类型必须和引用实体是同种类型的。
2、引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
3、使用场景
3.1、做参数
目的是让形参的改变影响实参。
例一、
我们在学习C语言函数时,一定写过交换函数,用来实现两个数值的交换:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
交换函数中参数一定是以指针形式存在的。因为如果不是指针,那么形参的改变不会影响实参,自然也就无法实现实参之间的交换。
学了C++之后,我们又多了一种方法可以实现两值交换,那便是引用:
void Swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
此时函数调用所传递的形参是实参的别名,所以使用实参的别名来交换数值,可以影响到实参。
例二、
在学习链表过程中,因为在进行插入删除操作时有可能需要改变头节点的指针,所以我们之前都是采用传递二级指针的方法实现的。
typedef struct Node
{
struct Node* next;
int val;
}Node, *PNode;
void PushBack(Node** phead, int x)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if(*phead == NULL)
{
*phead = newnode;
}
//.............
}
int main()
{
Node* head = NULL;
PushBack(&head, 1);
PushBack(&head, 2);
PushBack(&head, 3);
return 0;
}
使用引用的话就不需要传递二级指针了,因为传递过去的形参其实是实参的别名,改变形参可以改变实参。
typedef struct Node
{
struct Node* next;
int val;
}Node, *PNode;
void PushBack(Node*& phead, int x)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if(phead == NULL)
{
phead = newnode;
}
//.............
}
int main()
{
Node* head = NULL;
PushBack(head, 1);
PushBack(head, 2);
PushBack(head, 3);
return 0;
}
3.2、做返回值
目的是:
- 减少拷贝
- 让调用者可以修改返回对象
在C语言中,我们调用函数获取返回值是通过临时变量来传递的。因为函数栈帧在函数调用结束后会被销毁,栈帧中的值无法直接被传递,具体相关知识可见这篇文章:
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
如果返回值占据空间较小,那么通常由寄存器来充当临时变量。如果返回值占据空间较大,那么这个临时变量会提前在main函数栈帧中开辟好。
现在我们把 n 创建为一个静态变量。
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
此时变量 n 就不在 Count 函数的函数栈帧中了,而是在静态区里,也就是说在 Count 函数栈帧销毁后,变量 n 仍然保留。
这是不是说明返回 n 的值时不需要借助临时变量,而是直接返回 n 呢?
其实不会,编译器不会擅自做出这样聪明的改动,返回 n 的值时仍然会借用临时变量,尽管没有必要。
编译器不会主动做出这样的改动,但是人为可以。我们使用引用来作为返回值,直接返回变量 n 的引用:
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
不再需要借助临时变量,直接返回 n 的引用。 可以理解为直接返回了 n 。
再来举个例子理解一下:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
大家认为输出的结果是什么样的呢?
如果没有理解可以看下面这张图中的讲解:
注意:当函数返回时,如果出了函数作用域,返回对象还在(还没还给系统),则可以使用
引用返回。如果已经还给系统了,则必须使用传值返回,如果使用引用返回,结果是未定义的。
比如返回对象在静态区、堆区,或者被创建在上一层栈帧中时都可以使用引用返回。
4、传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。使用引用返回则可以提升效率。
这里提供一份测试性能的模板,大家有兴趣可以自己测试一下:
值和引用作为参数的性能比较
#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
值和引用作为返回值类型的性能比较
#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
5、常引用
变量的权限可以缩小,但是不能放大。
比如我们定义一个常变量,常变量是只读的,所以我们不能直接给常变量起别名:
int main()
{
const int a = 10;
int& b = a;//这种写法是报错的
const int* p1 = NULL;
int* p2 = p1;//这种写法也是错的
}
因为常变量 a 本身不能修改,但是给他取了别名 b 之后, b 却是能修改的了,这属于权限放大,很不合理。
所以应该这样写:
int main()
{
const int a = 10;
const int& b = a;
const int* p1 = NULL;
const int* p2 = p1;
}
这就叫做常引用。
当然我们缩小变量权限也是可以的:
int main()
{
int a = 10;
const int& b = a;
int* p1 = NULL;
const int* p2 = p1;
}
当我们使用引用来接收函数返回值时,也不能进行权限放大:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int& ret = Count();//这样写是错误的
return 0;
}
因为函数返回值是通过临时变量返回的,为传值返回。临时变量具有 常性 ,不可修改,如果直接引用属于权限放大,所以依然需要使用 const 来修饰:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
const int& ret = Count();
return 0;
}
换个例子:
int main()
{
int i = 0;
double& rd = i;//这种写法是错误的
}
这种错误是因为 i 的类型是 int ,但是 rd 的类型是 double ,不匹配。
但是这样写为什么就可以了呢?
int main()
{
int i = 0;
const double& rd = i;
}
这是因为我们在进行类型转换时会产生临时变量。
int i = 10; double d = i;
这里的赋值并不是把 i 的值直接赋给 d ,而是在中间产生一个临时变量,这个临时变量是 double 类型的。 把 i 的值先进行类型转换后传给临时变量,再把临时变量的值传给 d 。
所以代码 double& rd = i 中, rd 是临时变量的别名,而不是 i 的别名。而临时变量具有 常性,所以为了不会权限放大,要写成: const double& rd = i 。
6、引用和指针的区别
在 语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。而指针有自己的独立空间。
但是在 底层实现上 引用实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们把这段代码反汇编观察一下:
lea :取地址
可以发现不管是引用还是指针,汇编语言基本相同,都会访问变量的地址。在汇编语言的角度,引用依然是开辟了空间的。
引用和指针的不同点总结:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
关于引用的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!