新闻动态

行业新闻企业新闻安博电竞

【C++】C++ 入门(二)(引用) 3bRzGne8

安博电竞

目录

一 、入门前言

二、引用引用

1、入门引用的引用概念

2、引用特性

3、入门使用场景

3.1、引用做参数

3.2、入门做返回值 

4、引用传值 、入门传引用效率比较

值和引用作为参数的引用性能比较

值和引用作为返回值类型的性能比较

5 、常引用

6 、入门引用和指针的引用区别


一 、前言

上一篇文章我们讲解了 C++ 的入门命名空间 、缺省参数、引用函数重载等内容,接下来我们继续讲解引用相关的入门知识。

二 、引用

1、引用的概念

 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。

需要注意的是:引用类型必须和引用实体是同种类型的。 

2、引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

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 、做返回值 

目的是:

  1. 减少拷贝
  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 #include 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 #include 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 :取地址

可以发现不管是引用还是指针,汇编语言基本相同,都会访问变量的地址。在汇编语言的角度,引用依然是开辟了空间的 。

引用和指针的不同点总结:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

关于引用的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

chatgpt免费软件,chatgpt api 免费接口,chatgpt 聊天机器人教程,chatgpt 指令大全,chatgpt app

【C++】C++ 入门(二)(引用)

百度的CHATGPT与创意的碰撞

CHATGPT不仅是一个强大的工具 ,还是创意的源泉。百度的CHATGPT可以帮助用户生成有趣、富有创意的文本内容,为内容创作者提供了全新的创作灵感 。