寫在前面:
與其說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了。相信通過前面的講解,這里不用在下多說了。
后記:
在下能力有限,盡管盡最大努力做到嚴謹,但錯誤疏漏之處仍在所難免,懇請大家批評指正。您的幫助,就是在下前進的不竭動力。