C++ 引用

2023-12-15 04:54:53

目录

要点概述

关于“引用的本质为指针”的讨论

引用和指针区别

常量引用与非常量引用

不要返回临时变量的引用或指针


要点概述

引用,又叫取别名,是C++中一种用于别名分配的特殊语法。它允许将一个已存在的变量作为一个别名来使用,而不是创建一个新的变量。引用可以看作是一个变量的别名,它和原变量共享同一块内存。引用在C++中的使用可以提高代码的可读性和效率。

要点概括:

  1. 引用的使用格式:变量类型& 别名=原名。例:int& b=a。
  2. 同一个变量可以有多个引用。
  3. 引用也可以有引用,依旧是代表的原来的实体变量。
  4. 引用必须进行初始化,且一旦初始化之后就不可更改了。
  5. 引用其实就是给变量取一个别名,引用的地址就等于原变量的地址。所以如果修改了引用或者原变量其中的一个,另一个也会相应变化。(可以当成不同的名字,同一个变量来理解)
  6. 函数可以返回引用,就像函数可以返回指针一样。但注意不要返回临时的引用或指针。
  7. 引用的本质:引用在C++的底层实现其实就是一个指针常量,这也就很合理的解释了第4条。(因为常量必须初始化,不能对常量进行修改)
  8. C++编译器在编译过程中用指针常量作为引用的内部实现,所以一个引用所占的内存空间就等于指针在此环境下所占的内存空间。
  9. 不过要注意,当我们sizeof一个引用的时候,其大小就等于所引用变量的大小,并不是一个指针的大小。这是因为虽然引用的底层实现是指针,但在在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
  10. 由于在语法上引用是一个别名,所以引用传参、引用做返回值时并不会调用拷贝构造。也就是说,引用的好处之一就是可以减少拷贝构造的开销。

关于“引用的本质为指针”的讨论

????????有些书籍和教程会将引用的本质解释为指针常量,这是因为在编译器内部,引用的实现方式确实是通过创建一个指向所引用对象的常量指针来实现的。

????????在C++中,引用本身是没有地址的,编译器在编译时会将引用的访问转化成所引用对象的访问。也就是说,引用本身是不会占用内存空间的。但是,为了使引用能够在程序中被使用,编译器在内部实现引用时,会在编译时创建一个常量指针,将这个指针指向所引用的对象的内存地址。同时,编译器会对这个指针进行一些限制,比如不能对这个指针进行重新赋值,不允许使用空引用等。这些限制使得引用更加安全和易于使用。

????????因此,有些人将引用的本质解释为指针常量是有道理的,但是这并不影响引用作为一种独立的语言特性,以及它在实际编程中的应用和作用。总的来说,从使用者的角度来看,引用和指针有着明显的区别,理解它们各自的特点和用法是编写高质量C++代码的关键所在。

简而言之,引用在程序中的使用是靠编译器内部转换实现的,比如说,对于一个引用变量x,编译器会在内部创建一个指针p,然后将p指向x所引用的那个对象。这个指针p是一个指向常量的指针(也就是说不能改变指针的值),也就是所谓的指针常量。因此,有些人将引用解释为指针常量也是有理有据的。

????????不过从使用者的角度来讲,引用只是一个别名,C++为了实用性而隐藏了引用的底层细节。所以我们并不需要过多的深究引用的底层实现,把引用当作系统为我们封装好的语法糖使用就可以了。

引用和指针区别

  1. 从概念上理解,引用就是变量的别名,和其引用实体共用同一块空间。而指针是一个单独的变量,存储的是变量的地址。
  2. 引用在定义时必须初始化,指针则没有强制要求。但有些编译器,比如VS2022,也是要求指针必须初始化的。不过从标准的语法角度是没有要求指针必须要初始化的,只是要求引用必须初始化。
  3. 引用在初始化引用一个实体后,就不能再引用其他实体了,而指针可以在任何时候指向任何一个同类型实体。
  4. 没有空引用,但有空指针。
  5. 在sizeof中的含义不同:sizeof引用的结果为所引用类型的大小,但指针始终是地址空间所占字节个数。(即位宽,32位4字节、64位8字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。即对引用操作就是对实体变量进行操作,对指针操作,就是对指针所指向的那块地址进行操作。
  7. 有多级指针,但是没有多级引用。
  8. 访问实体方式不同:指针需要显式地解引用,而引用是由编译器封装处理好的,本身就是引用,所以不需要显示地解引用操作。
  9. 引用相比指针使用起来相对更安全。

常量引用与非常量引用

在此之前我们需要了解一下引用绑定的概念。

引用绑定: 引用是一个别名,它是一个已存在对象的别名。当我们声明一个引用时,它必须初始化为绑定到一个对象。引用在绑定时就确定了其所指向的对象。

常量引用:

const引用不允许通过引用对所绑定的对象进行修改。它只能读取对象的值,不允许修改。试图通过const引用修改对象的值将导致编译错误。

当一个引用被声明为const时,它表示引用的值是只读的,不能通过该引用修改所引用的对象。这意味着编译器会在编译期间强制执行这个规则,确保我们不会通过该引用修改对象的值。因此,const修饰的引用可以绑定到不同类型的数据,并且允许接收临时对象,因为我们不能通过该引用来修改这些数据的值,只能读取。它允许类型之间的隐式转换,包括常规类型转换和用户定义的类型转换。这是因为const引用只用于读取对象的值,不会修改对象,所以允许在一定程度上放宽类型匹配的要求。

举个例子:假设有一个const引用类型为const int&,但我们试图将其绑定到一个double类型的变量。由于存在从doubleint的隐式转换,编译器会允许这种隐式转换并将double类型的值转换为int类型,然后将其绑定到const int&引用。
不过需要注意的是,虽然const修饰的引用允许隐式转换,但这并不意味着它可以绑定到任何类型,仍然需要满足基本的类型兼容性规则。所谓的兼容性规则指的是 a = b 这个操作行得通,例如系统基础的数据类型 int、double、char 等之间都可以,但如果是两个类或者结构体的话需要有对应的operator=才可以。

非常量引用:

在此之前,需要先简单认识一下左值和右值:

左值(L-value):左值是指具有标识符的表达式,或者是可以取地址的表达式。左值可以出现在赋值操作的左侧和右侧,表示一个具体的存储位置或对象。

右值(R-value):右值是指不能取地址的表达式,或者是临时的、即将销毁的值。右值只能出现在赋值操作的右侧。

const修饰的引用可以修改所引用对象的值。即通过非const引用,可以修改对象的值,包括赋予新值、修改成员等操作。所以,非const引用的类型必须与所引用对象的类型完全匹配,否则会导致编译错误。并且,非const引用的初始值必须为左值。这是为了避免悬空引用,确保引用的有效性或者防止意外的修改等。

悬空引用: 当一个引用被绑定到一个左值时,它将引用该左值所代表的具体对象或存储位置。这意味着引用可以安全地访问和修改所绑定的对象。但是,如果引用被绑定到一个右值(例如临时值、临时对象、表达式的结果、函数的临时返回值等),当该右值超出范围并被销毁后,引用将变成悬空引用。悬空引用指向一个不再存在的对象,使用它可能导致一种很危险的未定义行为。所以为了避免这种情况,C++要求非常量引用的初始值必须是左值,以确保引用绑定到持久性的对象上。

防止意外的修改: 因为非常量引用允许修改所绑定对象的值。所以如果允许非常量引用绑定到右值(临时对象),那么修改临时对象的值可能会导致不可预料的行为。由于右值可能在其他地方被共享或使那么允许修改它们可能会对程序的正确性和安全性产生负面影响。因此,C++限制了非常量引用的初始值必须是左值,以确保我们只能修改持久性的对象。

不要返回临时变量的引用或指针

所谓的不可以返回局部变量的引用或指针,指的是不能返回局部变量的引用或地址给引用或指针。
事实上还是看该局部变量的内存空间是在栈区还是堆区的。若是在栈区,函数调用结束,这块空间就被释放了,其对应的局部变量的指针或引用也就紧跟着销毁了,如果此时接收了返回的局部指针或引用并使用的话,会导致未定义问题,存在着一定的风险。
而如果该局部变量的内存空间是在堆区的,那么当函数调用结束时,函数栈帧被销毁,但堆区的变量并不会被销毁,所以可以继续正常使用。也可以说,如果不返回或者处理这些堆区的局部变量或引用,甚至可能会造成内存泄漏。

案例分析

int& fun()
{
    int a = 10;
    // 不可以,尝试返回 a 的地址给引用变量,a是存在栈里的,函数结束调用栈被销毁。
    return a; 
}  
int* fun()
{
    int a = 10;
    // 不可以,尝试返回 a 的地址给指针,a是存在栈里的,函数结束调用栈被销毁。
    return &a;
}    
int fun()
{
    int a = 10;
    // 可以,函数在返回时,产生一个临时对象,用于存放局部变量a的值的一份临时拷贝
    return a;
}    
int* fun()
{
    int a = 10;
    int *p = &a
    // 不可以,返回的是变量a的地址,而a是在栈区的
    return p;    
}
char* fun()
{
    char *s = "1sfsdg"; //C语言可以这么写,但C++中必须写成const char*
    // 可以,相对于返回的是 "1sfsdg" 的地址,
    // 而 "1sfsdg" 存储在字符常量区,也就在静态存储区,
    // 函数栈帧销毁时并不会跟着释放。
    // 不过要注意此时返回的不是char*,而是隐式的cosnt char*
    return s;
}

函数返回引用和非引用的区别

  • 返回类型为非引用:

当函数的返回值类型为非引用类型时,其返回值可以是局部对象,也可以是求解表达式。这是因为函数在返回时其实是先将最终的返回值放到一个临时变量中,然后紧接着函数栈帧销毁,再将这个临时变量作为函数调用的返回值,在主调函数处使用,紧接着临时变量也就释放了。也就是说,在变量接收返回值之前,函数栈帧就已经销毁了,接收的只是一个临时变量的值。

  • 返回类型为引用:

当函数的返回值类型为引用类型时,不借助临时变量,其返回的就是对象本身。所以千万不要返回局部对象的引用或者指向局部对象的指针。这是因为当函数执行完毕后,系统将会自动释放分配给函数及其内部的局部变量的存储空间。此时局部对象的引用就会指向不确定的内存,返回的指针就变成了不再存在对象的悬空指针。

文章来源:https://blog.csdn.net/m0_73759312/article/details/135007444
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。