关闭

C++中引用、指针与const之间的爱恨情愁

1181人阅读 评论(5) 收藏 举报
分类:

学过C语言基础的肯定都知道变量和数据类型是再简单不过的知识,然而这个基础中确有几个泥潭,稍有不慎就粉身碎骨——编程受阻,面试被刷。其中一个就是引用、指针和const,以及相互之间剪不断理还乱的关系。今天我们就来理一理。

1.引用是个什么鬼

1.1引用的概念

引用是为对象另外起的一个名字,也就是别名而已。那什么是对象呢?注意这里说的对象和面向对象里的对象不是一回事。这里的对象是内存的一块区域,它具有某种类型,变量是命名了的对象。可以这么认为,引用与对象简单的关系就像姓名和本人。姓名可以多换几个,但是必须和一个实在的人对应起来,不管它是是男是女,是死是生,甚至可以是某个小说里杜撰的,但是一定要有个显示的人与之对应。

一般在初始化变量时,初始值会被拷贝到新建的对象中,然而在定义引用时,程序把引用和它的初始值绑定在一起了,而不是将初始值直接拷贝给引用,也就是所,引用指向对象,但数据就一份。一旦初始化完成,引用将和它的初初识值对象绑定在一起,因为引用不能重新绑定另一个对象,所以引用必须初始化(要点一)

看下面的程序:

int ival=1024;
int &refVal=ival;
int &refval2;
第二行的定义了ival的引用refVal。第三行的就不对了,原因就是上面的要点一:未初始化。

需要注意的是,不能解绑、必须初始化不代表着引用类型的值就不能变了,事实上其改变更加多样。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的(要点二)。例如下面的程序:

int i,&ri=i;
i=5;
ri=10;

这里将ri与i绑定了,这里用i来初始化ri,虽然在第一行i也并未初始化,这没关系,以后给i赋值即可。就像新婚夫妻给自己的孩子取了名字,尽管孩子还没有,但是这没关系,名字属于未来出生的孩子。

中间两行是赋值,如果按照这个顺序,最后i和ri的值都为10。反过来,如果将中间两行反过来呢?那i和ri的值都是5.原因同样是要点二。

1.2引用的定义和使用

引用的符号是“&”,对于引用的定义,记住两个原则:引用的初始值必须是一个对象(要点三),引用类型的定义类型与初始值对象类型要一样(要点四)

例如:① int &refVal4=10;是错误的,原因是要点三。

② double dval=3.14;

    int &refVal5=dval; 是错误的,原因是要点四。

③ int ival=1.01;int &rval2=ival; 这个代码是正确的,原因是ival的值是整数类型1,而不是1.01 ,但是这种代码有隐患,容易产生意想不到的问题。


再看一个比较特殊的例子:

     int i,  &ri=i,  &r2=ri;

     i=5;

在这里,&r2=r1,在gcc下编译是正确的,但是否是说可以为引用定义引用呢?按照《C++ Primer》的说法,引用本身不是一个对象,因此不能定义引用的引用,但是最后输出的结果是三个变量值都为5。我感觉这里ri应该指向了i,这里其实还是为i定义的引用。

我测试上面的功能用的两个完整代码:

#include<iostream>
 using namespace std;
	int  main()
	{
	int i=1,&r1=i;
	double d=2.1,&r2=d;
	//r2=3.1415;
	//cout<<"r2:"<<r2<<";d:"<<d<<endl;
	//	r2=r1;
	//	cout<<"r1:"<<r1<<";r2:"<<r2<<endl;
	// i=r2;
	// cout<<"i:"<<i<<";r2:"<<r2<<endl;
	  r1=d;
	 cout<<"r1:"<<r1<<";d:"<<d<<endl;


	return 0;
	} 
第二个:

#include<iostream>
 using namespace std;
	int  main()
	{
	int i,&ri=i,&r2=ri;
	 
	ri=10;
	i=5;	
	cout<<i<<" "<<ri<<";"<<r2<<endl;
	return 0;
	} 

2.指针是个什么鬼

指针可不是闹着玩的,一本书都解释不完(还真有本大牛的书,叫《C与指针》)。可以简单认为,指针就是某个对象的地址。指针的细节非常多,大部分人应该都知道,这里我们重点分析与引用的区别。

指针定义也很简单:

int ival=42;

int *p=&ival。

比较要命的就是这里有个“&”,指针里表示某个对象的地址,初学时容易与引用混在一起。上面的代码可以写成下面的形式,更容易理解:

int ival=42;

int *p;

p=&ival;

通过指针可以访问对象,例如

cout<<*p;就可以输出42.效果与cout<<ival一样的。

其实指针和引用容易混淆的关键之一就是“&”和“*”这种符号可以有多种含义。

 在等号左边,符号随着类型名出现,是声明的一部分(要点5.1)。如果在等号右边,符号在一个表达式中,因此是取地址(&)或者解析的(*)。(要点5.2)

int i=42;
int &r=i;//要点5.1
int *p;//要点5.1
p=&i;//要点5.2
*p=i;//这里其实就是用常量i给*p赋值,《C++ Primer》里说这是解引用,不懂,请高手指点
int &r2=*p;//这里与int &r2=42;是一样的,从地址P所指向的对象给引用r2赋值。</span>

 指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针,但是指针是对象,所以存在对指针的引用。
例如
 int i=42;
int *p;
int *&r=p;//r是一个对指针的引用。这种代码很难理解,要点六:复杂的表达式从右向左一个个阅读。离变量名最近的符号对变量的类型有直接影响,这里*(&r),因此r是一个引用,然后(&r)是一个指针。
r=&i;//,上面要点5.2,r是一个引用,指向的是i的地址,因此给r赋值就是另P指向i。
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0。小心上面这两种代码。个人感觉理解大型软件的主线就是理清楚数据从头到尾是怎么走的,如果遇到这种代码有时候会发现程序突然断掉了,大伤脑筋。

3 const限定符

const是一种类型,用于说明永不改变的对象,这也意味着const对象一旦定义就不能改变了,自然定义的时候就必须初始化。const的定义和使用啥的就不提了,重点介绍const与引用和const与指针。

 当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层(指针所指向的对象)const资格,或者两个对象的数据类型必须能够转换。这里只有单向转换:非常量可以转换成常量,反之不可以。

3.1const与引用

将引用绑定到const上,就叫对常量的引用了。考虑到const的特征,对常量的引用不能用作修改它所绑定的对象。

const int ci=1024;
const int &r1=ci;//正确,引用及其对应的对象都是常量
int &r2=ci;//c错误,试图让一个非常量引用指向常量引用。注意这两行代码的区别

r1=42;//错误,r1是对常量的引用


const的要求比较严,不能用对常量的引用给普通的引用初始化。例如:

int i42;
const int &r1=i;
const int &r2=42;
const int &r3=r1*2;
int &r4=r1*2;//r4是非常量引用,不能用常量引用来初始化。  </span></span>


在普通的引用中,引用的类型必须与其所引用对象的类型一致。但是在初始化的时候只要类型能转换过去就可以用任意表达式作为初始值。嘛意思呢?还是看代码:

double dval1=3.14;
const int &r1=dval;//正确

int r2=4;
const double dval2=r2;//错误
其实,这里可以结合强制类型转换来理解。

由于ri引用了一个int型的数,但是dval是一个双精度浮点数,所以这里其实进行了强制转换:

const int temp=dval;

const int &ri=dval;

也即这里的ri绑定了一个临时量对象。不过这种方式虽然能编译过去,但是没什么意义,也存在隐患。C++代码还是严谨一点好。

3.2 const与指针

const指针理解起来就容易多了,就是把一个指针定义成了不可改变的常量嘛。我们知道一个指针能够表示一个对象以及对象的地址,很显然,const指针就可以表示地址本身是常量还是表示的数据是常量,《C++ Primer》中前者叫顶层const,后者叫底层const。

首先看一个代码:

const double pi=3.14;
double *ptr=&pi;//错误,用常量的地址给普通指针初始化,类似于用白马代替所有马了,以偏概全了。</span>

关于const指针,指向常量的指针不能用于改变其所指对象的值。要存放常量对象的地址,只能使用指向常量的指针。

例如:

const double pi=3.14;</span>
const double *cptr=&pi;
*cptr=42;//错误。不能给常量地址赋值。这里不管使用*p=42还是其它方式,都不可以了。

一般而言,指针的类型与其所指对象的类型要一致。但是一个指向常量的指针可以指向一个非常量对象(反之不行),从而间接的改变常量指针的值。例如:

const double pi=3.14;
onst double *cptr=&pi;
double dval=3.333;
cptr=&dval;
这里cptr指向的就是dval的地址,*cptr就是3.333了。但是不能通过cptr改变dval的值,指向常量的指针仅仅要求不能通过当前指针改变对象的值,但是可以通过其他途径间接的改变。

假如将*放在const前面,会怎么样呢?那就是const指针了,作用是说明指针是一个常量,其深层含义是地址本身是常量而所指向的值可能被改变了。

例如:

int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb的地址
int errNumb2=2;
curErr=&errNumb2;//错误,不能再绑定其他值了

如何分清上述两种声明呢?根据前面的要点六,从右向左阅读。这里离currErr最近的符号是const,意味着curErr本身是一个常量对象,也就是地址不会变,然后再看*就容易多了。

再来看一个更复杂一点的代码:

const double *const pip=&pi;
首先pip是一个常量指针,其次,其指向的对象也是常量,类型为双精度浮点型。

4.小结

引用、指针和const之间的关系是C++基础语法里绕不过的关键主题。解决问题的思路就是先记住每个点的特征,也就是上面总结的几个要点,然后按照从右向左的顺序逐步拆分。如果有等号,记得符号在等号左右表示的含义不一样就能解决大部分的问题。

将上述几个要点稍作完善:

要点一:引用和const定义对象时必须初始化,而指针不必。

要点二:定义的引用,对其进行的所有操作都是在与之绑定的对象上进行的。而指针其实就是某个对象的地址,可以通过指针来访问对象,也可以通过某个对象来放问指针(地址)。

要点三:引用的初始值必须是一个对象,引用类型的定义类型与初始值对象类型要一样。

要点四:在等号左边,符号随着类型名出现,是声明的一部分。如果在等号右边,符号在一个表达式中,因此是取地址(&)或者解析的(*)。

要点五:复杂的表达式从右向左一个个阅读。离变量名最近的符号对变量的类型有直接影响。


上面大部分素材都来自《C++ Primer》第二章的2.3和2.4。不过感觉我还是没彻底将问题说清楚,甚至有些地方有漏洞。虽然博客写了大半天,感觉还是挺失败。以后找几个面试中遇到的例子整理再慢慢打磨这篇“爱恨情愁”。

吐舌头

 






3
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:105857次
    • 积分:1682
    • 等级:
    • 排名:千里之外
    • 原创:58篇
    • 转载:19篇
    • 译文:0篇
    • 评论:53条
    最新评论