C++——引用简介

来源:互联网 发布:浙江大学知乎 编辑:程序博客网 时间:2024/05/19 16:23

为获得更好观感,可访问https://www.zybuluo.com/HolyCipher/note/682545

一、什么是引用?

  C++相比于C新增了一种复合类型——引用变量。引用是已定义的变量的一个别名,例如将dad作为father的引用,则dadfather是指同一个对象。那么这种别名有什么作用呢?可以让程序变得像一篇文章吗,避免重复词汇的出现?那真是太蠢了。其实引用变量的主要用途是用作函数的形参。通过将引用变量作为参数传给函数,函数将使用原始数据,而不是其副本(如果不清楚形参和实参的区别,请补充相关知识)。
  这意味着,在C++中除了指针以外,引用为函数修改原始数据的值提供了十分方便的途径;从另一个角度来看,引用也为函数处理大型结构提供了非常便捷的途径,一般为了构建大型结构的副本,将会耗费大量资源

二、怎么创建引用变量?

  C和C++都使用&符号来指示变量的地址。而在C++中,&获得了新的含义,将其用于声明中,则意味着声明了一个引用变量。例如要将dad声明为father的引用,应该这样做:

int dad;int& father = dad;

  在此可以类比指针的声明操作,int* ptr中的*并不是解引用运算符,而是类型标识符的一部分,表示这是一个指针。类似的,int& father = dad中的&也不是地址运算符,而是类型标识符的一部分,表示这是一个引用变量
  现在让我们来通过一个例程看看变量的引用和变量之间的关系吧:

#include <iostream>using std::cout;using std::endl;int main() {  int dad = 88;  int& father = dad;  //这里的&是类型标识符的一部分,表示声明一个int的引用  cout << "dad's value : " << dad << endl;  cout << "dad's address : " << &dad << endl;  //这里的&是地址运算符  cout << "father's value : " << father << endl;  cout << "father's address : " << &father << endl;   //这里的&是地址运算符  dad = 888;  cout << "dad's new value : " << dad << endl;  cout << "father's new value : " << father << endl;}

  程序的输出是(具体的地址和格式与运行环境相关):

dad’s value : 88
dad’s address : 0x7ffd010490fc
father’s value : 88
father’s address : 0x7ffd010490fc
dad’s new value : 888
father’s new value : 888

  所以我们可以发现,dadfather的值和地址完全一样,尝试对dad进行赋值也会影响到father的值。
  现在我们可能很自然地把引用和指针联系起来,但是它们之间的差别我们也要清楚,例如:

int dad;int& father = dad;int* papa = &dad;

  那么dadfather*papa是等价的,&dad&fatherpapa是等价的。从这一点来说,引用很像包装了的指针,解引用操作被捆绑。但是引用除了表示方法不同于指针以外还有一点很重要的是,必须在声明时将其初始化,没有对象的引用变量是没有意义的。例如:

int dad;int& father;father = dad;

  像上面这样写是错误的,不会通过编译,必须在声明的时候对引用变量初始化
  引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就无法再与其他变量关联了(“忠心耿耿”,“从一而终”)。可以说,引用就像是被包装了的const指针并且被捆绑解引用操作,因而我们也常说,指向某某某的引用,引用的出现使得程序员在编写程序时不被大量的*搞混。

三、怎么将引用作为函数参数?

  引用最大的用途便是作为函数的参数,避免复制时消耗空间和时间,使得函数中的变量名成为调用程序中的原始变量的别名,这种传递参数的方法称为按引用传递。按引用传递允许被调函数直接访问传入变量的原始数据。还记得在C中应该如何写一个swap()函数来交换两个int变量的值吗?我们只能用指针来对原始数据进行修改。如:

void swap_by_pointer(int* a, int* b) {  int temp = *a;  *a = *b;  *b = temp;}int main() {  int x = 1, y = 2;  swap_by_pointer(&x, &y);}

  而通过引用,我们可以直接这么写(注意调用方式的差异):

void swap_by_reference(int& a, int& b) {  int temp = a;  a = b;  b = temp;}int main() {  int x = 1, y = 2;  swap_by_reference(x, y);}

四、什么是临时变量和左值?

  经过先前的了解,我们知道:对于一个引用变量而言,必须要有它引用的东西,否则它就没有意义,那么对于按引用传递的函数来说,如果传入的数据不是一个可引用的变量呢,比如swap_by_reference(x + 1, 2)?在当前的C++标准下,这是错误的,大多数编译器都将指出这一点,因为我们知道x + 1 = 2的赋值语句是不正确的。但是有些情况下还是允许特例的,具体情况是这样:因为x + 1并不是一个变量,而是一个表达式,于是程序将创建一个临时的无名变量并将x + 1的值赋给它,再让函数形参中的a成为这个临时变量的引用,现在让我们看看什么时候会生成临时变量,什么时候不会呢?
  在当前的C++标准下,仅当参数被声明为const引用,并且在实参属于以下两种情况时生成临时变量:
    1. 实参的类型正确,但不是左值。
    2. 实参的类型错误,但可以转换为正确的类型。
  这里可能会出现新的疑问:什么是左值呢?
  左值是可被引用的数据对象,所有有名字的,比如变量、数组元素、结构成员、指针和被解引用的指针等。非左值包括字面常量(双引号”“下的常量字符串除外,它们实质上是一个地址)和包含多项的表达式(如上述的x + 1)。在C语言中,左值最初指的是可出现在赋值语句左侧的实体,但当引入const关键字后,常规变量和const都被视作左值,因为可通过地址访问它们,尽管const修饰的变量不能被赋值,因而常规变量就被称为可修改的左值,而const变量则属于不可修改的左值
  了解了临时变量以后,我们应当意识到,在不影响函数功能时将引用参数设置为const是推荐的,有这么几个理由:
    1. 使用const可以避免无意中修改数据造成的错误;
    2. 使用const使函数能够同时处理const实参和非const实参,否则只能接受非const数据;
    3. 使用const使函数能够正确生成并使用临时变量。

五、怎么将引用作为函数返回值?

  在了解如何返回引用之前,先来思考一个问题:为什么要返回引用?
  要回答这个问题,我们需要先分辨出返回引用传统返回的区别,传统返回的机制按值传递函数参数类似:首先计算return后语句的值,然后将结果返回给调用函数,这个值被复制到一个临时位置,而调用程序将使用这个值,这是一个非左值。而返回引用实质上是返回一个左值,可用于相关的操作。比如在STLvector容器定义中有一个at(int)函数,返回特定位置元素的引用,便于调用函数对其进行赋值等操作。
  实现起来并不麻烦,只需要将函数返回值声明为引用类型即可,来看个例子:

#include <iostream>using std::cout;using std::endl;int& first_by_reference(int num[]) {  return num[0];}int first_by_value(int num[]) {  return num[0];}int main() {  int numArr[] = {0, 1, 2, 3, 4};  cout << "Reference : " << first_by_reference(numArr) << endl;  cout << "Value : " << first_by_value(numArr) << endl;  first_by_reference(numArr) = 5;  cout << "Edited Value : " << first_by_value(numArr) << endl;}

  上面有两个函数,一个返回引用一个传统返回,两者差异只不过是函数返回值类型中前者为int&,后者为int而已,对return语句无须作任何修改。那么这个程序的输出是:

Reference : 0
Value : 0
Edited Value : 5

  从中可以看见如果返回引用就相当于直接指向了原始数据。而如果我们尝试写出first_by_value(numArr) = 5;编译时就会报错,在笔者环境中的提示如下:error: lvalue required as left operand of assignment.
  大意是赋值语句的左操作数需要为左值。所以从这里可以看到返回引用和传统返回的一个差异。
  再来看另一个差异,前面提到过,引用作为参数传递时可以减少空间和时间的消耗,因为按值传递程序会生成一个原始数据的拷贝,在函数返回值的问题中也是一样,如果要返回一个大型结构,程序会生成一个副本,同样会耗费空间和时间,所以返回引用就也能解决这个问题,减少了程序构建副本时造成的开销。

六、返回引用需要注意的问题

  返回引用时最重要的就是避免返回一个不再存在的内存单元的引用,比如:

string& givePapa() {  string papa = "papa";  return papa;}

  当函数结束时,papa的生命周期结束,它已经被销毁,这时候再使用函数返回的引用将导致程序崩溃。一个可以采用的方法是在堆上开辟内存,如下:

string& givePapa() {  string *papa = new string{"papa"};  return *papa;}

  但是这样又带来了新的问题,堆上内存需要释放,不然会造成内存泄漏,这是要务必小心的。还有另一个优雅的解决方法就是使用智能指针。

七、总结

  使用引用的目的主要有两个:
    1.程序员能够修改调用函数中的数据对象
    2.通过传递引用而不是整个数据对象(不论是传入函数还是从函数传回),可以提高程序的运行速度。
  使用引用时的问题或许没有使用指针时那么多,但是同样要提防会发生的问题。将引用当成const修饰的捆绑解引用操作的指针来理解会十分精准而自然。这么去理解也会减少很多可能会误用导致的程序错误。

1 0
原创粉丝点击