让你彻底弄懂指针、引用与const

今天重温了一下C++ Primer,对上面三个概念有了更清晰的认识,自我认为已经有了比较全面的理解了,所以赶紧记录下来,也请大家批评指正。

1.引用

引用,简单来说就是为对象起了一个别名,可以用别名来等同于操作对象,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名,即引用变量的别名:

int i =1; int &r = i; //r指向i(r是i的别名,可以通过操作r来改变i的值)、 r=2; cout<<r<<" "<<i<<" "<<endl; //r和i的值都为2 int j=r; //j被初始化为i的值,即为2

通俗的讲,为引用赋值,实际上就是将值赋给了与引用绑定的对象。获取引用的值,实际上就是获取了与引用绑定的对象的值。但是引用本身不是一个对象,所以不能定义引用的引用。

另外引用必须要被初始化,刚才也说了,在定义引用的时候,程序是吧引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起,因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

int &r; //错误,引用必须被初始化 int &r=10; //错误,引用类型的初始值必须是一个对象 int i=10,&r=i; //正确,r是一个引用,与i绑定在了一起,i是一个int int j=0; int& r=j,p=j; //正确,r是一个引用,与j绑定,p是一个int

在这里强调一个可能有一些人会忽视的问题,为什么上面的最后一段代码不是应该r和p都是引用吗?
这种想法是错误的,上面我故意把引用符号&写到了int后面,而没有写到变量的旁边,两种写法的结果都是一样的,r是引用,p是int;
这里我们要搞清楚一条生命语句是由一个基本数据类型和紧随其后的一个声明符列表组成。对应到上面那条代码就是基本数据类型是int,声明符是&,声明符只对其后的第一个变量有效,所以不管我们在int后面加多少个声明符,只对第一个变量有效,与后面的变量的无关,例如:

int *&p,q; //p是一个引用,绑定一个指针变量,q是一个int int* p,i,&r=i; //p是指针,i是int,r是引用

引用的还有一个比较重要的点就是引用的类型与之要绑定的对象严格匹配。(有两种例外的情况,在const部分会介绍一种)

int i=0,&r1=i; double d=3.14,&r2=d; double &r3=i; //错误,引用类型为double,对象类型为int r1=r2; //double型向int型转换,会丢失精度 cout<<i<<" "<<r1<<" "<<endl;//i和r1的值都为3

2.指针

指针与引用类似,也实现了对其他对象的间接访问。但是,与引用对比,又有很多不同点,其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且还可以先后指向不同的对象(引用一旦绑定就不能修改了);其二,指针无需再定义的时候赋初值。

指针存放的是某个对象的地址,要想获得该地址,需要使用取地址符&,并且一般情况下(有两种例外情况,再次暂时不谈),指针的类型都要与它所指向的对象严格匹配:

int a=1; int *p=&a; //p存放变量a的地址,或者说p是指向变量a的指针 double b=3.14; double *q; q=&b; //q是指向p的指针 int *r=&b //错误,指针类型与对象类型不匹配

指针的值(即地址)有四种状态:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置(int *p=&i,p++)
  3. 空指针,意味着指针没有指向任何对象
  4. 无效指针,也就是上述情况之外的其他值

访问无效指针将引发错误,但是编译的时候并不会报错,运行的时候才会报错,访问无效指针的后果无法预计,因此程序必须清楚给定的指针是否有效。

尽管上述第2和第3中形式的指针是有效的,显然这些指针没有指向任何对象,所以试图访问此类指针对象的行为也是不允许的,如果这样做了,后果也无法预计。

如果指针指向了一个对象,就可以用解引用符(操作符*)来访问对象,下面看一个引用和指针结合的例子:

int i = 42; int &r = i; //r为i的引用 int *p; p = &i; //p是指向i的指针 cout<<*p<<endl; //输出42 *p = 32; //相当于给i赋值 cout<<i<<endl; //输出32 int &r2 = *p; //相当于int &r2 = i; cout<<r2<<endl; //输出32 r2=22; //改变i的值 cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都为22 *p=12; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都为12 i=2; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都为2 int j = 52; *p=j; //相当于将j的值赋给i,i=j; cout<<i<<" "<<r2<<" "<<*p<<endl; //三者相等,都为52 p=&j; //将p指向j, j=62; cout<<i<<" "<<r2<<" "<<*p<<endl; //i=r2=52,*p=62 r=j; 相当于i=j, cout<<i<<" "<<r2<<" "<<*p<<endl; //都为62

可能有些同学对&和*的含义理解的很模糊,为什么一会表示这样,一会又是另外一个意思。确实,像&和*这样的符号,既可以用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:

int i=2; int &r=i; //&紧随类型名出现,因此是声明的一部分,r是一个引用 int *p; //*紧随类型吗出现,因此是声明的一部分,p是一个指针 p=&i; //&出现在表达式中,是一个取地址符 *p=i; //*出现在表达式中,是一个解引用符 int &r2=*p;//&是声明的一部分,是引用,*是一个解引用符

空指针不指向任何对象,定义空指针的方法:

int *p = nullptr;//推荐方法  int *p2=0; int *p3=NULL;//NULL的值就是0,等价int *p3=0

得到空指针最好的方法是第一种,它是C++11新标准新引入的方法,nullptr是一种特殊类型的值,它可以被转换成任意其它类型的指针。NULL是一个预处理变量,预处理器会自动将它替换为0。新标准下,最好使用nullptr,同时尽量避免使用NULL。

另外不能直接把int变量直接赋给指针,就算int变量为0也不行

int z=0; int *p=z;//错误,不能把int变量直接赋给指针

前面已经说过,指针和引用都能提供对其它对象的间接访问,然后在实现细节上二者差别很大,引用本身不是一个对象,一旦绑定一个对象就不能更改。而指针则不同,它只要对指针赋值存放一个新的地址,就指向了一个新的对象:

int i=2; int *pi=0; //pi是空指针 int *pi2=&i; //pi2指向i int *pi3; //pi3的值无法确定 pi3=pi2; //pi3和pi2都指向同一个对象i pi2=0; //现在pi2不指向任何对象,但pi3还是指向i

指向指针的指针

一般来说声明符中修饰符的个数并没有限制,譬如可以有用**来表示指向指针的指针,***表示指向指针的指针的指针,依次类推:

int i=1024; int *pi=&i; //pi指向int型的数 int **ppi=&pi; //ppi指向一个int型的指针

输出i,*pi和**ppi的值都是一样,都为1024.

指向指针的引用

如前所述,引用不是一个对象,所以就不会存在指向引用的指针,但指针是对象,所以存在指向指针的引用:

int i=42; int *p; //p是一个未被初始化的指针 int *&r=p; //r是一个指向指针p的引用 r=&i; //相当于p=&i; cout<<i<<" "<<*p<<" "<<*r<<endl;//三者的值相等

对于上面r的类型到底是什么,教给大家一个最简单的方法,离变量名最近的符号(此处是&)决定变量的类型,所以此处r肯定是一个引用,声明符的部分确定r引用的类型是什么,此处是*,就说明引用的是一个指针,最后,声明的数据类型是int,那么,也就是说,r引用的是一个int型的指针。以后这样复杂的定义都是这样理解。

3.const限定符

const是用作定义常量,一旦某个对象定义为一个常量,那么就不能改变这个对象的值。

const int buff = 512; buff=1024//错误,试图向const对象赋值 const int i=get_size(); //正确:运行时初始化 const int j = 42; //正确:编译时初始化 const int k; //错误:k是一个未经初始化的变量 int h=12; const int m=h; //正确:h的值拷贝给了m int n=m; //正确:m的值拷贝了n

可以把引用绑定到一个const对象上,称为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象,另外允许一个常量引用绑定一个非常量的对象、子面值,甚至是一个表达式:

const int ci=1024; const int &r1=ci; //正确:引用及其对应的对象都是常量 r1=42; //错误:r1是对常量的引用,不能修改 int &r2 = ci; //错误,非常量引用不能指向常量对象 int i=42; const int &r1=i; //正确:允许常量引用绑定到普通int对象上 const int &r2=42; //正确:r2为常量引用 const int &r3=r1*2; //正确:r3为常量引用 i=2; cout<<r1<<" "<<i<<" "<<r3<<endl; //r1=i=2,r3=84; double d = 3.14; const int &r4 = d; //正确:r4为一个绑定到int对象的常量引用 cout<<d<<" "<<r4<<endl; //d=3.14,r4=3 d=5.15; cout<<d<<" "<<r4<<endl; //d=5.15,r4=3

大家一定很奇怪,为什么在将引用的时候说两边的类型要一致,并且要指向一个对象,但是在这里又存在这样的一种情况,就算指向不同类型和一个具体的数值都可以。要想理解者是为什么,就要弄清楚当一个常量引用绑定到另外一种类型上时到底发生了什么:

在最后的那一段代码中,其实编译器为了确保r4绑定一个整型对象,编译器引入了一个临时变量,将代码变成了如下形式:

double d=3.14; const int temp=d; const int &r4=temp;

写成这样以后,大家就好理解了,那么我们回到之前所说的,如果r4不是const,那么为什么就引用的类型和对象的类型必须一致,你可以这样想,如果r4不是常量,那么就允许对r4赋值,也就会改变r4所引用对象的值,但是注意,r4绑定的是一个临时变量而不是对象d,所以r4改变不了d的值,如此看来r4引用d,但是不能用来改变d的值,那么就没有什么意义了,C++也就将这种行为归为非法了。

与引用一样,也可以令指针指向常量或非常量,类似于常量引用,指向常量的指针不能用于改变其所指对象的值,并且,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

const double pi=3.14; //pi是一个常量,它的值不能改变 double *ptr=&pi; //错误:ptr不能指向常量 const double *cptr = &pi; //正确 *cptr = 2.1; //错误,不能给*cptr赋值 double d = 3.14; cptr = &d; //正确 *cptr=4.1; //错误:不能直接修改*cptr的值 d=4.1 //正确,*cptr=4.1

指针本身是一个对象,所以可以把*放在const之前来说明指针本身是一个常量,这样写的意思是,不变的是指针本身的值而不是指向的那个值:

int errNumb = 0; int c=1; int *const curErr = &errNumb; //curErr将一直指向errNumb,不能修改 curErr=&c; //错误,curErr不能修改 *curErr = c; //正确:curErr不能修改,但是*curErr可以修改 cout<<*curErr<<" "<<errNumb<<endl; //都等于1 const double pi = 3.14; double p=4.5; const double *const pip = &pi; //pip是一个指向常量对象的常量指针 pip=&p; //错误 *pip=p; //错误 const int ci=42; int bi = 32; const int *p2=&ci; //正确:p2指向ci const int *const p3=p2; //正确:p3和p2同时指向ci cout<<*p2<<" "<<*p3<<endl; //二者相等 p2=&bi; //p2指向bi,p3指向ci cout<<*p2<<" "<<*p3<<endl; //*p2=32,*p3=42 p2=p3; //正确 int &r=ci; //错误:普通int不能绑定到int常量上

在这里,教给大家一个方法,判断向上面这种赋值语句,你只需要看假设能够赋值成功,那么改变左边的值,右边的对象会不会出现错误,例如:p2=p3,不能改变*p2,但是可以改变p2,但是改变p2的值,对p3的值没有造成影响,所以可以p2=p3,为什么int &r=ci就错了,假设可以赋值成功,那么就可以改变r的值来改变ci的值了,但是ci是const型,不能改变,所以上式就有问题。像这样的赋值语句都可以这样考虑。

猜你喜欢

转载自www.cnblogs.com/maxwelldzl/p/11639410.html