C++是在C语言的基础上发展来的。C++除了有C语言的指针外,还增加一个新的概念——引用,初学者容易把引用和指针混淆一起,面试或者笔试经常被考到。
要弄清楚这两个概念,先从变量说起。
一:变量的形式
什么是变量呢?变量(variable)的定义在计算机科学中到底是如何定义的?然后variable到底是在内存中如何存储值的呢?那么跟着上面的问题,我们来一一的解答。
首先最重要的,变量的定义,当你申明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:int x = 5; 这是一句最简单的变量赋值语句了, 我们常说“x等于5”,其实这种说法是错误的,x仅仅是变量的一个名字而已,它本身不等于任何值的。这条语句的正确翻译应该是:“将5赋值于名字叫做x的内存空间”,其本质是将值5赋值到一块内存空间,而这个内存空间名叫做x。切记:x只是简单的一个别名而已,x不等于任何值。其图示如下:
变量在内存中的操作其实是需要经过2个步骤的:
1)找出与变量名相对应的内存地址。
2)根据找到的地址,取出该地址对应的内存空间里面的值进行操作。
二:指针
首先介绍到底什么是指针?指针变量和任何变量一样,也有变量名,和这个变量名对应的内存空间,只是指针的特殊之处在于:指针变量相对应的内存空间存储的值恰好是某个内存地址。这也是指针变量区别去其他变量的特征之一。例如某个指针的定义如下:
intx=5;
int*ptr=&x;
ptr即是一个指正变量名。通过指针获取这个指针指向的内存中的值称为dereference,间接引用。
其相对于内存空间的表示如下:
使用指针的优点和必要性:
指针能够有效的表示数据结构;
能动态分配内存,实现内存的自由管理;
能较方便的使用字符串;
便捷高效地使用数组
指针直接与数据的储存地址有关,比如:值传递不如地址传递高效,因为值传递先从实参的地址中取出值,再赋值给形参代入函数计算;而指针则把形参的地址直接指向实参地址,使用时直接取出数据,效率提高,特别在频繁赋值等情况下(注意:形参的改变会影响实参的值!)
三:引用
引用是C++引入的新语言特性,是C++常用的一个重要内容之一。
引用(reference)在C++中也是经常被用到,尤其是在作为函数参数的时候,需要在函数内部修改更新函数外部的值的时候,可以说是引用场景非常丰富。正确、灵活地使用引用,可以使程序简洁、高效。
我在工作中发现,许多人使用它仅仅是想当然,只是知道怎么应用而已,而不去具体分析这个reference。
在某些微妙的场合,很容易出错,究其原由,大多因为没有搞清本源。
下面我就来简单的分析一下这个reference。首先我们必须明确的一点就是:reference是一种特殊的pointer。从这可以看出reference在内存中的存储结构应该跟上面的指针是一样的,也是存储的一块内存的地址。例如reference的定义如下:
intx=5;
int&y=x;
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
上面的代码,定义了引用y,它是变量x的引用,别名,这样子,目标变量有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
四、引用和指针有什么区别?
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
inta=1;int*p=&a;
inta=1;int&b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2) 引用不可以为空,当被创建的时候,必须初始化,初始化后就不会再改变了;而指针可以是空值,可以在任何时候被初始化,指针的值在初始化后可以改变,即指向其它的存储单元。
(3)可以有const指针,但是没有const引用;
(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(5)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(6)指针和引用的自增(++)运算意义不一样;
(7)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;
(8)从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域,因为引用声明时必须初始化,从而指向一个已经存在的对象。引用不能指向空值。
注:标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体随编译器 http://bbs.csdn.net/topics/320095541
(9)从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针。
(10)不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
下面用通俗易懂的话来概述一下:
指针-对于一个类型T,T就是指向T的指针类型,也即一个T类型的变量能够保存一个T对象的地址,而类型T是可以加一些限定词的,如const、volatile等等。见下图,所示指针的含义:
引用-引用是一个对象的别名,主要用于函数参数和返回值类型,符号X&表示X类型的引用。见下图,所示引用的含义:
总之,可以归结为”指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。”
五、指针传递和引用传递
在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
六、返回引用和返回指针
C++返回引用类型
A& a(){ return *this;} 就生成了一个固定地址的指针,并把指针带给你。
但A a() { return *this;}会生成一个临时对象变量,并把这个临时变量给你,这样就多了一步操作。
当返回一个变量时,会产生拷贝。当返回一个引用时,不会发生拷贝,你可以将引用看作是一个变量的别名,就是其他的名字,引用和被引用的变量其实是一个东西,只是有了两个名字而已。
问题的关键是,当你想要返回一个引用而不是一个拷贝时,你要确保这个引用的有效性,比如:
int & fun() { int a; a=10; return a; }
这样是不行的,因为a会在fun退出时被销毁,这时返回的a的引用是无效的。
这种情况下,如果fun的返回类型不是int & 而是int就没有问题了。
返回指针的话,谁调用该函数,谁负责接触返回的指针。
全局变量,局部静态变量,局部动态分配变量 都可以作为函数返回值。
局部自动变量不行
函数内部等局部变量,存储在栈中的变量是不能作为返回值的,虽然可以读取正确的值,但是这是一块未分配的内存,当别的进程用到时就会出错,这个指针相当于野指针。返回值可以是局部动态分配的内存空间,这一部分分配在堆上,在主动释放之前别的进程是无法使用的内存区域。
不管是指针还是引用都是如此。
七、特别之处const
为什么要提到const关键字呢?因为const对指针和引用的限定是有差别的:
常量指针VS常量引用
★常量指针:指向常量的指针,在指针定义语句的类型前加const,表示指向的对象是常量。
定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性。
常量指针定义”const int* pointer=&a”告诉编译器,pointer是常量,不能将pointer作为左值进行操作。
★常量引用:指向常量的引用,在引用定义语句的类型前加const,表示指向的对象是常量。也跟指针一样不能对引用指向的变量进行重新赋值操作。
指针常量VS引用常量
在指针定义语句的指针名前加const,表示指针本身是常量。在定义指针常量时必须初始化!而这是引用与生俱来的属性,无需使用const。
指针常量定义”int* const pointer=&b”告诉编译器,pointer(地址)是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pointer(地址所指向内存的值)可以修改。
常量指针常量VS常量引用常量
常量指针常量:指向常量的指针常量,可以定义一个指向常量的指针常量,它必须在定义时初始化。
定义 const int* const pointer=&c
告诉编译器,pointer和*pointer都是常量,他们都不能作为左值进行操作。
而不存在所谓的”常量引用常量”,因为引用变量就是引用常量。C++不区分变量的const引用和const变量的引用。程序决不能给引用本身重新赋值,使他指向另一个变量,因此引用总是const的。如果对引用应用关键字const,起作用就是使其目标称为const变量。即
没有
const double const& a=1;
只有 const double& a=1;
1 | double b=1; |
总结:有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身就是常量,即不可以改变指向。