【C++要笑着学】引用的概念 | 引用的应用 | 引用的探讨 | 常引用(二)

简介: 本章将对C++的基础,引用 部分的知识进行讲解

Ⅲ. 关于引用的探讨


0x00 比较传值和传引用的效率


❓ 那传值返回和传引用返回的区别是什么呢?


💡 传引用返回速度更快。


📚 以值作为参数或者返回值类型,在传参和返回期间,


函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝。


因此值作为参数或者返回值类型,效率是非常低下的,


尤其是当参数或者返回值类型非常大时,效率就更低。


传值和传引用的效率比较:


#include <iostream>
#include <time.h>
using namespace std;
struct S {
  int arr[10000];
};
void CallByValue(S a) {
  ;
}
void CallByReference(S& a) {
  ;
}
void TimeCompare() {
  S s1;
  /* 以值作为函数参数 */
  size_t begin1 = clock();
  for (size_t i = 0; i < 100000; i++) {
  CallByValue(s1);
  }
  size_t end1 = clock();
  /* 以引用作为函数参数 */
  size_t begin2 = clock();
  for (size_t i = 0; i < 100000; i++) {
  CallByReference(s1);
  }
  size_t end2 = clock();
  /* 计算两个函数运行结束后的时间 */
  cout << "Call by Value: " << end1 - begin1 << endl;
  cout << "Call By Reference: " << end2 - begin2 << endl;
}
int main(void)
{
  TimeCompare();
  return 0;
}

🚩 运行结果:

f20e20e07ea0f21e00bb9513e8896e5a_1d0274ea4c6f419b921c869530fc2855.png


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


💬 记录起始时间和结束时间,从而计算出两个函数完成之后的时间。


#include <iostream>
#include <time.h>
using namespace std;
struct S {
  int arr[10000];
};
void ByValue(S a) {
  ;
}
void ReturnByReference(S& a) {
  ;
}
void TimeCompare() {
  S s1;
  /* 以值作为函数参数 */
  size_t begin1 = clock();
  for (size_t i = 0; i < 100000; i++) {
  ByValue(s1);
  }
  size_t end1 = clock();
  /* 以引用作为函数参数 */
  size_t begin2 = clock();
  for (size_t i = 0; i < 100000; i++) {
  ReturnByReference(s1);
  }
  size_t end2 = clock();
  /* 计算两个函数运行结束后的时间 */
  cout << "Return By Value: " << end1 - begin1 << endl;
  cout << "Return By Reference: " << end2 - begin2 << endl;
}
int main(void)
{
  TimeCompare();
  return 0;
}


🚩 运行结果如下:

3010b9c01f1f383db9cd570dae1c79ad_02634526a53f4745bfc6837980253f41.png


传值返回会创建临时变量,每次会拷贝 40000 byte。


而传引用返回没有拷贝,所以速度会快很多很多,因为是全局变量所以栈帧不销毁。


所以这种场景我们就可以使用传引用返回,从而提高程序的运行效率。


🔺 总结:传值和船只真在作为传参以及返回值类型上效率相差十分悬殊。


引用的作用主要体现在传参和传返回值:


① 引用传参和传返回值,有些场景下面,可以提高性能(大对象 + 深拷贝对象)。


② 引用传参和传返回值,输出型参数和输出型返回值。


有些场景下面,形参的改变可以改变实参。


有些场景下面,引用返回,可以减少拷贝、改变返回对象。(了解一下,后面会学)


引用后面用的非常的多!非常重要!


0x01 引用和指针的区别

在语法概念上:引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

2a850addb4c1296e8d6e7eaee270c21b_f67b06fc7750458cb0da31c95a3e7094.png


但是在底层的实现上:实际上是有空间的,因为引用是按照指针方式来实现的。


#include <iostream>
#include <time.h>
using namespace std;
int main(void)
{
  int a = 10;
  int& ra = a;
  ra = 20;
  int* pa = &a;
  *pa = 20;
  return 0;
}

🔍 我们来看看引用和指针的汇编代码对比:

40a9073b4cf1244b7ead56ab482db214_bab0cd1d22b44f98b642e191060bc6f2.png


0x02 指针和引用的不同点

总结 ❌      整活 ✅


① 引用是在概念上定义一个变量的别名,而指针是存储一个变量的地址。


② 引用在定义时必须初始化,而指针是最好初始化,不初始化也不会报错。


int a = 0;
int& ra;    ❌ 必须初始化!
int* pa;    ✅ 可以不初始化

③ 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体。


④ 有空指针,但是没有空引用。

68f4a7afe1762bd7a2274dff7664cfb5_0c7df444f38741b1afbe0dded861a333.png

⑤ 在 sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节数(64位平台下占8个字节)


 
         

🚩 运行结果如下:(本机为64位环境)

83a1fa09d8be052f4fd636ceef816497_3f0d5025c2a944438b2c986b1566ba04.png


⑥ 引用++即引用的实体增加1,指针++即指针向后偏移一个类型的大小。

#include <iostream>
using namespace std;
int main(void)
{
    int a = 10;
    int& ra = a;
    int* pa = &a;
    cout << "ra加加前:" << ra << endl;
    ra++;
    cout << "ra加加后:" << ra << endl;
    cout << "pa加加前:" << pa << endl;
    pa++;
    cout << "pa加加后:" << pa << endl;
    return 0;
}

🚩 运行结果如下:

fdb319e8f07d1d6ae344a467a242ae10_c9c97998b0204795bf75add1873afea6.png


⑦ 有多级指针,但是没有多级引用。

b9281fe4aab25caef4780429cb9f563d_55abd641af65467abf0b8dc382fde2a8.png


⑧ 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。


#include <iostream>
using namespace std;
int main(void)
{
    int a = 10;
    int& ra = a;
    int* pa = &a;
    cout << ra << endl;    // 引用直接是编译器自己处理,即取即用。
    cout << *pa << endl;   // 指针得加解引用操作符*,才能取到。
    return 0;
}

🚩 运行结果如下:

b5db464568ab4dbe12386664f1633c99_448dfd53a6564f939f949dab6de9074f.png


⑨ 引用比指针使用起来相对更加安全。

e86d3c37d0c700d27ed2c9c036f21370_6f14678ed71540acbe2f9beabb34a751.png


🔺 总结:指针使用起来更复杂一些,更容易出错一些。(指针和引用的区别,面试经常考察)


使用指针有考虑空指针,野指针等等问题,指针太灵活了,所以相对而言没有引用安全!

61d6ba265c0a619fa66a522656c0fafe_63d61ab4c752483faaeab2f18aaef057.png


Ⅳ.  常引用


0x00 常引用的概念

如果既要利用引用来提高程序的效率,又想要保护传递给函数的数据不能在函数中被改变,就应使用常引用。


📜 语法:const 数据类型&  引用名 =  引用实体;


一共有三种情况:分别是权限的放大、保持权限不变、权限的缩小。


0x01 权限的放大

💬 下面是一个引用的例子:


int main(void)
{
    int a = 10;
    int& ra = a;
    return 0;
}

💬 如果对引用实体使用 const 修饰,直接引用会导致报错:


int main(void)
{
    const int a = 10;
    int& ra = a;
    return 0;
}

🚩 运行结果如下:(报错)

57cf6eff5f3d7d935c6bcc52c8901d76_941ea356fa1e4f2292b596f082853733.png


🔑 分析:导致这种问题的原因是,我本身标明了 const,这块空间上的值不能被修改。


我自己都不能修改,你 ra 变成我 a 的引用,意味着你 ra 可以修改我的 a,


这就是属于权限的放大问题,a 是可读的,你 ra 要变成可读可写的,当然不行。


这要是能让你随随便便修改,那我岂不是 const 了个寂寞?


这合理吗?这不合理!


❓ 那么如何解决这样的问题,我们继续往下看。


0x02 保持权限的一致

既然引用实体用了 const 进行修饰,我直接引用的话属于权限的放大,


我们可以给引用前面也加上 const,让他们的权限保持不变。


💬 给引用前面加上 const:


int main(void)
{
    const int a = 10;
    const int& ra = a;
    return 0;
}

🔑 解读:const int& ra = a 的意思就是,我变成你的别名,但是我不能修改你。


这样 a 是可读不可写的,ra 也是可读不可写的,这样就保持了权限的不变。


如果我们想使用引用,但是不希望它被修改,我们就可以使用常引用来解决。


0x03 权限的缩小

如果引用实体并没有被 const 修饰,是可读可写的,


但是我希望它的引用不能修改它,我们可以用常引用来解决。


💬 a 是可读可写的,但是我限制 ra 是可读单不可写:


int main(void)
{
    int a = 10;
    const int& ra = a;
    return 0;
}

🔑 解读:这当然是可以的,这就是权限的缩小。


举个例子,就好比你办身份证,你的本名是可以印在身份证上的,


但是你的绰号可以印在身份证上吗?

1e7d4159c52e43ef147e3d7550cddec4_dd0140c11e7548be9198f72b7de5efe7.png


所以就需要加以限制,你的绰号可以被人喊,但是不能写在身份证上。


所以,权限的缩小,你可以理解为是一种自我的约束。


0x04 常引用的应用

💬 举个例子:


假设 x 是一个大对象,或者是后面学的深拷贝的对象


那么尽量用引用传参,以减少拷贝。


如果 Func 函数中不需要改变 x,那么这里就尽量使用 const 引用传参。


void Func(int& x) {
    cout << x << endl;
}
int main(void)
{
    const int a = 10;
    int b = 20;  
    Func(a);  // ❌ 报错,涉及权限的放大
    Func(b);  // 权限是一致的,没问题
    return 0;
}

加 const 后,让权限保持一致:


// "加上保持权限的一致"
           👇
void Func(const int& x) {
    cout << x << endl;
}
int main(void)
{
     👇
    const int a = 10;
    int b = 20;  
    Func(a);  // 权限是一致的
    Func(b);  // 权限的缩小
    return 0;
}

🔑 解读:如此一来,a 是可读不可写的,传进 Func 函数中也是可读不可写的,


就保持了权限的一致了。b 是可读可写的,刚才形参还没使用 const 修饰之前,


x 是可读可写的,但是加上 const 后,属于权限的缩小,x 就是可读但不可写的了。


常引用后期会用的比较多,现在理解的不深刻也没关系,早晚的事情。


后面讲类和对象的时候会反复讲的,印象会不断加深的。


0x05 带常性的变量的引用

💬 先看代码:


int main(void)
{
    double d = 3.14;
    int i = d;
    return 0;
}

这里的 d 是可以给 i 的,这个在C语言里面叫做 隐式类型转换 。


它会把 d 的整型部分给 i,浮点数部分直接丢掉。


❓ 但是我在这里加一个引用呢?


int main(void)
{
    double d = 3.14;
    int& i = d;  // 我能不能用i去引用d呢?
    return 0;
}

🚩 运行结果:(报错)


直接用 i 去引用 d 是会报错的,思考下是为什么?


这里可能有的朋友要说,d 是浮点型,i 是整型啊,会不会是因为类型不同导致的?


但是奇葩的是 —— 如果我在它前面加上一个 const 修饰,


却又不报错了,这又是为什么?


int main(void)
{
    double d = 3.14;
    const int& i = d;  // ??? 又可以了
    return 0;
}

哎它* *滴!const,const 为什么行!!!


🔑 解析:因为 内置类型产生的临时变量具有常性,不能被修改。


隐式类型转换不是直接发生的,而是现在中间产生一个临时变量。


是先把 d 给了临时变量,然后再把东西交给 i 的:

6071366b36c0cd8b2c4dc6873b1ef1b4_8c1bc85bcd174732a883bfbd566a9bb6.png

如果这里用了引用,生成的是临时变量的别名,


又因为临时变量是一个右值,是不可以被修改的,所以导致了报错。


🔺 结论:如果引用的是一个带有常性的变量,就要用带 const 的引用。


0x06 常引用做参数

使用引用传参,如果函数中不改变参数的值,建议使用 const 引用


💬 举个例子:


一般栈的打印,是不需要改变参数的值的,这里就可以加上 const

void StackPrint(const struct Stack& st) {...}

const 数据类型&  可以接收各种类型的对象。


使用 const 的引用好处有很多,如果传入的是 const 对象,就是权限保持不变;


普通的对象,就是权限的缩小;中间产生临时变量,也可以解决。


因为 const 引用的通吃的,它的价值就在这个地方,如果不加 const 就只能传普通对象。

image.png

相关文章
|
21天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
53 2
|
2月前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
22 3
|
1月前
|
程序员 C++ 开发者
C++入门教程:掌握函数重载、引用与内联函数的概念
通过上述介绍和实例,我们可以看到,函数重载提供了多态性;引用提高了函数调用的效率和便捷性;内联函数则在保证代码清晰的同时,提高了程序的运行效率。掌握这些概念,对于初学者来说是非常重要的,它们是提升C++编程技能的基石。
21 0
|
3月前
|
存储 算法 C++
C++ STL应用宝典:高效处理数据的艺术与实战技巧大揭秘!
【8月更文挑战第22天】C++ STL(标准模板库)是一组高效的数据结构与算法集合,极大提升编程效率与代码可读性。它包括容器、迭代器、算法等组件。例如,统计文本中单词频率可用`std::map`和`std::ifstream`实现;对数据排序及找极值则可通过`std::vector`结合`std::sort`、`std::min/max_element`完成;而快速查找字符串则适合使用`std::set`配合其内置的`find`方法。这些示例展示了STL的强大功能,有助于编写简洁高效的代码。
48 2
|
3月前
|
存储 搜索推荐 Serverless
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
36 1
|
4月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
32 3
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
74 0
|
4月前
|
JSON Go C++
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
45 1
|
3月前
|
JSON Android开发 C++
Android c++ core guideline checker 应用
Android c++ core guideline checker 应用