1. 指向常量的指针和常量指针
2. 指向指针的指针
往往有c++程序员说“常量指针”(const pointer)时,其想表达的意思往往是“指向常量的指针”(pointer to const),但实际上,这两者是两个完全不同的概念。
将const修饰符放到指针声明之前,应该想好,到底想让什么东西变成常量,是指针?还是准备指向的那个对象?或兼而有之?在pct的声明中,指针不是const的,但它所指向的对象被认为是const的。换句话说,const修饰符修饰的是基础类型T而不是指针修饰符*。而对于cpt的声明来说,声明的是一个指向一个非常量对象的常量指针,即const修饰符修饰的是指针修饰符*而不是基础类型T。
声明中的修饰符(即指针声明中第一个*修饰符之前出现的任何东西)的顺序无关性加剧了围绕指针和常量的语法问题。例如,以下两行代码所声明的变量的类型完全相同:
第一种形式更传统一些,但如今许多c++专家推荐使用第二种形式。理由在于,第二种形式不太容易被误解,因为这种声明可以倒过来读,即“指向T类型常量的指针”。使用哪一张形式无关紧要,只要保持一致就行了。然而,务必小心一个常见的错误,那就是将常量指针的声明与指向常量的指针的声明混淆。
当然,可以声明一个指向常量的常量指针:
注意,使用一个引用通常比使用一个常量指针更简单:
注意我们能够将一个指向非常量的指针转换成一个指向常量的指针。例如,我们能够使用pt(类型为T*)初始化pct(类型为const T*)。从非技术的角度来说,这样做之所以合法,是因为不会产生任何不良后果。想想当一个非常量对象的地址被复制到一个指向常量的指针时的情形,如图1所示。
图1 一个指向常量的指针可以指向一个非常量对象
指向常量的指针pct现在指向一个非常量T,但这不会造成任何危害。实际上,指向常量的指针(或引用)去指向非常量的对象,是司空见惯的事情:
调用aFunc时,使用a初始化arg1,使用b初始化arg2.我们并没有宣传a要指向一个常量对象,或者b是一个常量引用,只是声明在aFunc函数中它们被视为常量,而不管它们实际上是否如此。这很有用。
相反的转换,即从指向常量的指针转换为指向非常量的指针,则是非法的,因为可能会产生危险的后果,如图2所示。
图2 指向非常量的指针不可以指向常量对象
在这个例子中,pct可能实际上指向一个被定义为常量的对象。如果我们能够将一个指向常量的指针转换为一个指向非常量的指针,那么pt就可以用于改变act的值。
C++标准告诉我们,这样的赋值会产生未定义的结果,也就是说,我们不知道究竟会发生什么,不过可以肯定的是,不会发生什么好事情。当然,我们可以利用const_cast显示的指向类型转换。
然而,如果pt指向一个被声明为常量的对象(例如act),那么以上赋值行为仍然是未定义的。
指向指针的指针,这就是C++标准所说的“多级”指针。
尽管超过两级的多级指针很罕见,但在两种情况下,确实会看到指向指针的指针。第一种情形是当我们声明一个指针数组时:
由于数组的名字会退化为指向其首元素的指针,所以指针数组的名字也是一个指向指针的指针:
我们在管理指针缓冲区的类的实现中最常看到这种用法:
从PtrVector的实现可以看到,指向指针的指针可能会很复杂,最好将其隐藏起来。
多级指针的第二个常见应用情形,是当一个函数需要改变传递给它的指针的值时。考虑如下函数,它将一个指针移动到指向字符串中的下一个字符:
传递给scanTo的第一个参数是一个指向指针的指针,该指针值是我们希望改变的。这意味着我们必须传递指针的地址:
这种用法在C中时合理的,但在C++中,更习惯、更简单、更安全的做法是使用指向指针的引用作为函数参数,而不是指向指针的指针作为参数。
在C++中,几乎总是首选使用指向指针的引用作为函数参数,而不是指向指针的指针。
一个常见的误解是,适用于指针的转换同样适用于指向指针的指针。事实上并非如此。例如,我们知道一个指向派生类的指针可被转换为一个指向其公共基类的指针:
因为Circle是一个(is-a)Shape,因而一个指向Circle的指针也是一个Shape指针。然而,一个指向Circle指针的指针并不是一个指向Shape指针的指针:
当涉及const时也会发生同样的混淆。我们知道,将一个指向非常量的指针转换为一个指向常量的指针是合法的,但不可以将一个指向“指向非常量的指针”的指针转换为一个指向“指向常量的指针”的指针: