[C转C++之路]初窥引用(下篇)

123 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

前言

        引用是C++的一个重难点,本文就来分享一波笔者对于引用的学习经验和心得,应该算是初级内容(是我太菜),更加深入的内容待笔者修炼有成再续。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

传值、传引用效率比较

       以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

       作为参数时的比较

 #include<time.h>
 struct A{ char 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();
 }

image-20220916193518165

       作为返回值时的比较

 #include<time.h>
 struct A { char a[10000]; };
 A a;
 // 值返回
 A TestFunc1() { return a; }
 // 引用返回
 A& TestFunc2() { return a; }
 void TestReturnByRefOrValue()
 {
     // 以值作为函数的返回值类型
     size_t begin1 = clock();
     for (size_t i = 0; i < 1000000; ++i)
         TestFunc1();
     size_t end1 = clock();
     // 以引用作为函数的返回值类型
     size_t begin2 = clock();
     for (size_t i = 0; i < 1000000; ++i)
         TestFunc2();
     size_t end2 = clock();
     // 计算两个函数运算完成之后的时间
     cout << "TestFunc1 time:" << end1 - begin1 << endl;
     cout << "TestFunc2 time:" << end2 - begin2 << endl;
 }
 int main()
 {
     TestReturnByRefOrValue();
 }

image-20220916193648646

       通过上述代码的比较,发现传值和传引用在作为传参以及返回值类型上效率相差较大 。

       由此也可以看出,传引用返回除了可以修改返回值以外,还有减少拷贝、提高效率的优点;传引用传参除了可以在函数中修改函数外实参以外,还有减少拷贝、提高效率的优点。

常引用

       首先明确一点:指针和引用赋值中,权限可以缩小,但不可以放大。

       我们看看这段代码,const在前面修饰指针,指针指向的内容不可通过该指针修改,相当于把pa指针的权限缩小了(由读写变为只读),而若要把pa的值赋给paa(由只读变为读写),则是放大权限,因为paa没有被const修饰,是可以修改指向的内容的,放大权限是危险行为,编译器检测到就会报错。

 int a = 10;
 const int* pa = &a;
 int* paa = pa;

image-20220916195610344

       对于引用也是类似的,被const修饰的变量其权限由读写变为只读,权限缩小了,如果直接引用会将权限放大,是不被编译器允许的,除非用const修饰的引用。

 void TestConstRef()
 {
     const int a = 10;
     //int& ra = a; // 权限放大,编译时会出错,a为常量,
     const int& ra = a;//权限不变
     // int& b = 10; // 权限放大,编译时会出错,b为常量
     const int& b = 10;//可以引用常量
 }

看看这个代码,想想为什么。

 double d = 12.34;
 int i = d;//行
 int& ri = d;//不行

image-20220916203931509

       对于同类型赋值就不用在意这些,只是说变量被不同类型的值赋值时会用一个具有常量性的临时变量来把值暂存处理成匹配的类型再拷贝到目标变量中。

       加上const修饰限制权限即可:const int& ri = d

       再看看这个代码,想想为什么。

 int Count()
 {
     int n = 0;
     n++;
     return n;
 }
 ​
 int main()
 {
     int& ri = Count();//不行
     return 0;
 }

       这是传值返回,通过一个临时变量返回的,这个临时变量也具有常量性,所以得用const修饰后引用。

对于传参

       如果传引用传参时不想实参被修改,可以像常指针作形参那样让常引用作形参。

       比如

 void Print(const int& a, const int& b)
 {
     cout << a << b << endl;
 }

结合缺省参数

       可以设置缺省值,但是要注意要么设置为一个全局变量,要么用const修饰后设置为一个常量。

 int a = 10;
 void func1(int& N = a){}
 ​
 void func2(const int& M = 10){}

引用和指针的区别

引用的底层实现

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

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

 int main()
 {
     int a = 10;
     
     int& ra = a;
     ra = 20;
     
     int* pa = &a;
     *pa = 20;
     
     return 0;
 }

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

image-20220916204530691

       震惊地发现居然一模一样!说明底层实现引用时还是使用指针实现的,就是套了一层皮,让用户使用时能够忽略底层实现细节而“直接访问”变量。

引用和指针的不同点

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用在定义时必须初始化,指针没有要求

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

  4. 没有NULL引用,但有NULL指针

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

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用靠编译器自己处理(引用表面好像是传值,其本质也是传地址,只是这个工作由编译器来做)

  9. 引用比指针使用起来相对更安全(不让用户直接操作指针,具体细节由编译器来实现以确保行为的合法性和安全性)


以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif