C++——引用简介
来源:互联网 发布:浙江大学知乎 编辑:程序博客网 时间:2024/05/19 16:23
为获得更好观感,可访问https://www.zybuluo.com/HolyCipher/note/682545
一、什么是引用?
C++相比于C新增了一种复合类型——引用变量。引用是已定义的变量的一个别名,例如将dad
作为father
的引用,则dad
和father
是指同一个对象。那么这种别名有什么作用呢?可以让程序变得像一篇文章吗,避免重复词汇的出现?那真是太蠢了。其实引用变量的主要用途是用作函数的形参。通过将引用变量作为参数传给函数,函数将使用原始数据,而不是其副本(如果不清楚形参和实参的区别,请补充相关知识)。
这意味着,在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
所以我们可以发现,dad
和father
的值和地址完全一样,尝试对dad
进行赋值也会影响到father
的值。
现在我们可能很自然地把引用和指针联系起来,但是它们之间的差别我们也要清楚,例如:
int dad;int& father = dad;int* papa = &dad;
那么dad
、father
、*papa
是等价的,&dad
、&father
、papa
是等价的。从这一点来说,引用很像包装了的指针,解引用操作被捆绑。但是引用除了表示方法不同于指针以外还有一点很重要的是,必须在声明时将其初始化,没有对象的引用变量是没有意义的。例如:
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
后语句的值,然后将结果返回给调用函数,这个值被复制到一个临时位置,而调用程序将使用这个值,这是一个非左值。而返回引用实质上是返回一个左值,可用于相关的操作。比如在STL
的vector
容器定义中有一个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
修饰的捆绑解引用操作的指针来理解会十分精准而自然。这么去理解也会减少很多可能会误用导致的程序错误。
- C++——引用简介
- 引用——C++:bitset类的使用与简介
- c++—引用。。。
- 【C++】基础知识—引用和指针引用
- C/C++——引用和指针
- 初学c/c++——浅谈引用
- C++——左值引用和右值引用
- C++——左值引用和右值引用
- C++——迭代器简介
- css基础———简介、语法、引用方式
- 引用简介
- C#——引用参数的操作
- 与C不同之处——引用
- C++——指针和引用
- C++——传引用调用
- C++Primer——变量、引用、指针
- C和C++区别——引用
- C#—特殊引用类型string
- ps如何调出参考线?
- js的一些常用方法
- iOS回顾笔记(04) -- UIScrollView的基本使用详解
- Standby Redo Log 的设定原则、创建、删除、查看、归档位置
- 从零开始学贪心算法
- C++——引用简介
- 中文诗词翻译成英文
- The connection to adb is down, and a severe error has occured.问题解决方法小结
- 编译MatConvNet(仅CPU版本)
- 算法训练 未名湖边的烦恼
- Threes.js入门篇之7
- 重载和覆盖
- gradle简单模板
- 升级高版本的struts2.2.3.32 无法访问Action解决办法