TOP

理清C++常量指针和指针常量这团乱麻

写在前面:

     与其说C++中的常量指针和指针常量是一块很有嚼头的语法糖,不如说它是一块相当难啃的骨头。其实本来没什么,这无非是const int *p与int* const p的区别, 但一涉及到起名字,特别是给他们戴上“常量指针”和“指针常量”的中文帽子,由于作者和译者(针对外文书)的不同,就出现了“张冠李戴”和“李冠张戴”的乱像,不知道谁是谁了,弄得人一头雾水,尤其是对于初学者。本文的目的就是针对这一细节,为大家将两者理清楚,同时说明在使用上的区别。

注意:1.const int *p也可写成int const *p,即C++中const int和int const无区别,这使得本来就很乱的局面更加麻烦,本文中我只使用const int,以后不再说明。

        2.如果您讨厌啰嗦,只想学“干货”,您可以直接跳到“安能辨我是雄雌——判断方法”一节(在下技术有限,就不设置页面内跳转了)。

在理清楚之前,让 我们先简单看看当前“乱象丛生”的现状吧。

一、const遇上指针——一团乱麻

同样的问题,相反的解释

1.标新立异的少数派

      C++ Primer第五版的提法可谓与其它C++书籍背道而驰,它的提

                                                                                            常量指针——int* const p

指向常量的指针——const int *p

      在英文版中,int* const p被称为const pointer,于是中文版将其译为常量指针;而const int *p被称为pointer to const,但对应的中文版并未将其译为指针常量,而是译为指向常量的指针。

      无独有偶,我们国内的书籍《零起点学通C++》(范磊 著 清华大学出版社 2010年版)的提法和C++ Primer完全一致,将int* const p称为常量指针,而将const int *p称为指向常量的指针。

2.一边倒的大众说辞

      更广为流传的提法是

常量指针——const int *p

指针常量——int* const p

      明确写有“常量指针”和“指针常量”的书籍在下就见过一本——《C++面向对象程序设计》(郭有强 著 清华大学出版社 2009年版)(从出版年上我们可以大胆推测,相对较老的书会这样提)。更多的书籍的做法是——将int* const p称为指针常量,将const int *p称为指向常量的指针。(可见问题的关键在于int* const p,有的书中将其称为常量指针,如1,有的书中将其称为指针常量,如2)

      这里的“一边倒”更主要的是针对网络而言。如果您百度“常量指针和指针常量”,将会看到几乎所有的材料中都采用了2中的提法。如果一个初学者在解决“常量指针和指针常量”的困惑时首先选择了网络途径,那么2的提法应该在他的脑海里根深蒂固了吧。

      如果您已经接受了2中的提法,建议您将“常量指针”扩展为“指向常量的指针”,将“指针常量”扩展为“该指针是常量”来帮助您记忆和区别两者的本质。

作者们的拯救

      大概一些IT教育者和从业者们也注意到了这一问题,近年来的一些书籍作者们开始用自己的方式来纠正,让我们来看看他们是怎么做的。

1.这样的改变好吗?

     《C++程序设计基础》(管建和 著 清华大学出版社 2013年版)中将const int *p称为“常量化指针目标表达式”,而将int* const p称为“常量化指针变量”,试问,这么长的名字,能不能记住?记住了,能不能搞清楚?

2.两个弄不清,干脆叫一个

      比较著名的外文书籍《C++高级编程》(这里就中文第二版讲)中在“const指针”的标题下同时介绍了const int *p和int* const p,而没有给他们具体起名字。

      在国内,《新标准C++程序设计教程》(郭炜 著 清华大学出版社 2012年版)将const int *p称为常量指针,而在介绍int* const p时写道“还有一种常量指针”。

      但这样做仍然不能称之为行之有效,因为这样并没有通过名字将两者从本质上区分,同时“不同的事物,同一个名字”也不便于交流。

3.一种值得推荐的命名法

《C++程序设计教程》(方超昆 著 北京邮电大学出版社 2009年版)和《C++从入门到精通》(人民邮电出版社 2015年版)是这样提的

指向常量的指针变量——const int* p

指向变量的指针常量——int* const p

                                  指向常量的指针常量——const int* const p   //后面会详细说到

      这样,谁常谁变就一目了然了,而且名字反应了本质。从第一本书的出版年可以看出,其实这样比较清晰的命名方式好几年前就有了,但可惜的是,直到现在,在众多的C++书籍中,这般清晰的命名方式也并不多见。

在下的用意

      一定会有人说:“你说了这么多,并没有给我们带来什么实质性的知识。”在下不反对,其实在下在这一部分的开头已经清醒过大家,可以略过。在下写这部分的用意有四:

1.向读者介绍一下现状和常见的提法。

2.告诉读者该问题上存在的不一致现象,当读者阅读不同的书籍时遇到不一致,不要过于困惑。

3.劝读者不要记忆网上的“常量指针”和“指针常量”,因为这样的命名并不好,许多书籍上也并没有同时提到这两者。

4.表达一下自己希望C++的相关命名实现术语化、标准化、大一统的愿望,尽管在下知道这一点很难实现。

在下怎么看

1.不要使用“常量指针”和“指针常量”,特别是后者,因为它连“表述对象是指针”这层意思都没有清晰地表达出来。

2.建议采用“一种值得推荐的命名法”下的名称来记忆。

3.或者采用在下的建议。现在说明在下的提法

指向常量的指针——const int *p

   本身是常量的指针——int* const p

                       所指和本身都是常量的指针——const int* const p

       这里采用“定语+主体”的结构,主体“指针”很好地表现了所描述对象是指针这一本质,而定语部分则描述了特性,同时也将“自己”和“他人”区别开,整体通俗易懂。(在下一直认为,有时一个高大上的名称就是故作高深,而通俗易懂本身就是一种真正的高大上。面向初学者,把话说明白,一直是在下文章的宗旨与追求。)唯一的不足可能是名称有点长,但在下相信,只要足够通俗易懂,长不是障碍。

       从现在开始,我们在后面的讲述中将一直采用这种提法。

二、安能辨我是雄雌——判断方法

      相信有些初学者会有这样的困惑:单提名字知道怎么回事,但一看到代码就不知道叫什么名字了,也就无法想起代码的语法特性。这里,在下给大家介绍一种简单易用的判断方法,一看代码就能分析出语法特性。

      相信有些读者看到过所谓的“从右往左读”的办法,在下接下来要说的方法和“从右往左读”本质上一样,只不过无论是认知还是操作上都更加简单(起码在下认为是这样,这里就不谦虚了)。方法表述如下:

先找到*,然后看*的两边,右边是对指针p本身的限定,左边是对p所指向的东西的限定。

 注:本文中我们将p的所指笼统地称为“东西”。

例如

1.const int *p

      *的右边没有限定成分,表明p就是我们熟悉的普通指针,p的内容(即值,也就是它指向的东西的地址)是可以改变的;*的左边是const int,表明p指向的东西是一个const的int,我们不能通过p来修改这个int,因为它是const的。(关于“不能通过p来修改”,我们后边还会详细解释)

2.int* const p

      *的右边是const,表明p本身是const的,我们不能对p的内容进行修改(例如++p;是不可以的),*的左边是int,即p指向的东西是普通的int,我们可以通过p来修改它(例如*p=100;是可以的)。

3.const int* const p

      *的右边是const,表明指针p本身是const的,*的左边是const int,表明p指向的int也是const的。即这种情况下,p本身不能修改,同时也不能通过p修改它所指向的那个int。

三、天生我材必有用——语法特性

      其实在讲判断方法的时候,已经涉及到了各自的语法特性。这里再针对一些细节深入说一下。

1.const int *p

      就是所谓的“指向常量的指针”。这里注意,所谓“指向常量”只是这个指针的“一厢情愿”,只是一种效果上的等价。事实上,const int *p=&a;a既可以是常量(const int a=10;)又可以是变量(int a=10;),但p一厢情愿地认为它所指的就是一个常量,所以它不允许通过自己来修改所指,这就造成一种效果上的等价——从p的角度看,它所指的“的确”是常量。所以,对“指向常量的指针”的最佳理解应为:我们不能通过该指针修改它所指向的东西(常量或者变量)。

      注意,const int *p=&a;只是说不能通过p来修改a,如果a本身不是const的,通过其它方式修改a自然是可以的(例如直接++a)。

      另外一点,由于p本身只是一个普通的指针,所以允许在声明时不初始化。但需要注意的是,我们只是说可以,但并不提倡这样做。在任何时候都不应该让指针无所指,如果在声明一个指针时还不知道让它指向谁,就先将其初始化为nullptr或NULL(nullptr是C++11新特性,用它比用NULL更安全些,这里不详细介绍)。

2.int* const p

      就是所谓的“本身是常量的指针”。关于“p本身不能修改但可以通过p修改其所指”这一点,我们在讲判断方法时已经说过,这里主要再说一下p的初始化。

      由于p本身是const的,所以在编译的时候必须知道p的值(即p所指向的东西的地址),所以在声明p的同时必须初始化p。但要注意,对于 int* const p=&a,我们只要求a的地址是确定的,但a的值可以不确定。比如下面的代码是可行的

复制代码
#include<iostream>

using namespace std;

int GetData(int num)
{
    return num;
}

int main()
{
    int a;
    cin >> a;
    int b = GetData(a);
    int* const p = &b;
    cout << *p << endl;
    return 0;
}
复制代码

由于声明了int b,所以b的地址在编译时是确定的,但很显然,b的值只要在程序运行时才能确定。

      另外注意,用nullptr或NULL初始化int* const p没有问题,因为nullptr和NULL都代表有效地址。

3.const int* const p

     就是所谓的“所指和本身都是常量的指针”。它的语法特性就是前两者的结合,这里不再赘述。

四、番外篇——说说引用和const引用

      文章至此本该结束了,但就C++而言,谈到了指针,似乎自然应该再谈谈引用。于是,在下接着再来简单说说引用和const引用。所谓“简单说说”,是指在下只是做一个简明扼要的介绍,对一些原理和细节不做深入探讨。为什么要在这里说?因为我们的讲述要会用到上面的概念。另外还要注意,我们这里只讨论左值引用,即大家所熟悉的一般引用,对于C++11新特性右值引用,不在讨论之列。

相信大家都知道引用的这些特性

1.如果一个引用和一个东西(这里再次使用“东西”这个词)绑定,那么它就永远只能是这个东西的“别名”,不能再说“其他人”的“别名”,即引用本身不能修改。但是,我们可以通过引用来修改它所引用的东西的值。

2.引用声明时必须同时初始化 ,且必须用左值初始化。(左值:就是可以用&求地址的量,换言之,就是有确定地址的量,而不是所谓的临时量)

       对这些特性大家是不是似曾相识呢?没错,这些特性和“本身是常量的指针”(int* const p)的特性一样。事实上,我们完全可以借用“本身是常量的指针”来理解甚至定义引用:

引用是一个指向不可变的、被编译器自动解引用的指针,即,引用是一个被编译器自动解引用的“本身是常量的指针”。

看下面的代码

int a = 10;
int &ra = a;
ra = 11;

      在上面的代码中,编译器将int &ra=a转化为int* const ra=&a,而将ra=11转化为*ra=11,将ra自动转化为*ra的过程,就是上面定义中所说的“自动解引用”。

      那么,什么是const引用(即我们说的常量引用,但我希望大家称其为const引用而不是常量引用)呢?很显然,const int &ra=a就相当于const int* const ra=&a了。相信通过前面的讲解,这里不用在下多说了。

后记:

      在下能力有限,尽管尽最大努力做到严谨,但错误疏漏之处仍在所难免,恳请大家批评指正。您的帮助,就是在下前进的不竭动力。

posted @ 2015-10-22 20:45  同勉共进  阅读(5798)  评论(9编辑  收藏  举报