条款7 常量指针与指向常量的指针
在日常交谈中,当一个C++(www.cppentry.com)程序员说“常量指针”(const pointer)时,其实他想表达的意思往往是“指向常量的指针”(pointer to const)。真不幸,它们是两个完全不同的概念。
- T *pt = new T; // 一个指向T的指针
- const T *pct = pt; // 一个指向const T的指针
- T *const cpt = pt; // 一个const指针,指向T
将const修饰符放到指针声明之前,应该想好,到底想叫什么东西变成常量,是指针?还是准备指向的那个对象?或兼而有之?在pct的声明中,指针不是const的,但它所指向的对象被认为是const的。换句话说,const修饰符修饰的是基础类型(base type)T而不是指针修饰符*。而对于cpt的声明来说,声明的是一个指向一个非常量对象的常量指针,即const修饰符修饰的是指针修饰符*而不是基础类型T。
声明中的修饰符(即指针声明中第一个*修饰符之前出现的任何东西)的顺序无关性加剧了围绕指针和常量的语法问题。例如,以下两行代码所声明的变量的类型完全相同:
- const T *p1; // 一个指向常量T的指针
- T const *p2; // 也是一个指向常量T的指针
第一种形式更传统一些,但如今许多C++(www.cppentry.com)专家推荐使用第二种形式。理由在于,第二种形式不太容易被误解,因为这种声明可以倒过来读,即“指向常量T的指针”。使用哪一种形式无关紧要,只要保持一致就行了。然而,务必小心一个常见的错误,那就是将常量指针的声明与指向常量的指针的声明混淆。
- T const *p3; // 一个指向常量的指针
- T *const p4 = pt; // 一个常量指针,指向非常量T
当然,可以声明一个指向常量的常量指针:
- const T *const cpct1 = pt; // 二者均为常量
- T const *const cpct2 = cpct1; // 同上
注意,使用一个引用通常比使用一个常量指针更简单:
- const T &rct = *pt; // 而不是const T *const
- T &rt = *pt; // 而不是T *const
注意在前面的一些例子中,我们能够将一个指向非常量的指针转换为一个指向常量的指针。例如,我们能够使用pt(类型为T *)初始化pct(类型为const T *)。从非技术的角度来说,这样做之所以合法,是因为不会产生任何不良后果。想想当一个非常量对象的地址被复制到一个指向常量的指针时的情形,如图3所示。
指向常量的指针pct现在指向一个非常量T,但这不会造成任何危害。实际上,指向常量的指针(或引用)去指向非常量的对象,是司空见惯的事情:
- void aFunc( const T *arg1, const T &arg2 );
- //...
- T *a = new T;
- T b;
- aFunc( a, b );
调用aFunc时,使用a初始化arg1,使用b初始化arg2。我们并没有宣称a要指向一个常量对象,或者b是一个常量引用,只是声明在aFunc函数中它们被视为常量,而不管它们实际上是否如此。这很有用,不是吗?
相反的转换,即从指向常量的指针转换为指向非常量的指针,则是非法的,因为可能会产生危险的后果,如图4所示。
在这个例子中,pct可能实际上指向一个被定义为常量的对象。如果我们能够将一个指向常量的指针转换为一个指向非常量的指针,那么pt就可用于改变acT的值。
- const T acT;
- pct = &acT;
- pt = pct; // 报错!很幸运……
- *pt = aT; // 试图修改常量对象!
C++(www.cppentry.com)标准告诉我们,这样的赋值会产生未定义的结果,也就是说,我们不知道究竟会发生什么,不过可以肯定的是,不会发生什么好事。当然,我们可以利用const_cast显式地执行类型转换。
- pt = const_cast<T *>(pct); // 没有错,但这种做法不妥
- *pt = aT; // 试图修改常量对象!
然而,如果pt指向一个被声明为常量的对象(例如acT),那么以上赋值行为仍然是未定义的(参见“新式转型操作符”[条款9])。