文章目錄
- 復合類型
- 引用
- 概念與使用
- 引用的定義
- 注意
- 指針
- 概念
- 聲明方式
- 取地址符
- 指針值
- 空指針
- 利用指針訪問對象
- 賦值和指針
- void* 指針
- 指向指針的指針
- 指向指針的引用
- 初始化所有指針
- 有多重含義的某些符號
- const限定符
- 概念
- const的引用
- 指針和const
- 頂層const和底層const
- constexpr和常量表達式
- constexpr 變量
- 字面值類型
- 指針和constexpr
復合類型
復合類型是指基于其他類型定義的類型。引用和指針都是復合類型。
引用
概念與使用
引用: 引用并非對象,只是為一個已經存在的對象起了另一個名字,引用即別名。
一般在初始化變量時,初始值會被拷貝到新建的對象中。定義引用時,程序把引用和它的初始值綁定(bind) 在一起,而非將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值對象一直綁定在一起。
因為無法令引用重新綁定到另外一個對象,因此引用必須初始化。
int ival = 1024;
int &refVal = ival; // refVal指向ival(是ival的另一個名字)
int &refVal2; // 報錯:引用必須被初始化
定義了一個引用之后,對其進行的所有操作都是在與之綁定的對象上進行的:
為引用賦值,實際上是把值賦給了與引用綁定的對象:
refVal = 2; // 把2賦給refVal綁定的對象,即賦給了ival
獲取引用的值,實際上是獲取了與引用綁定的對象的值:
int ii = refVal; // 等價于 ii = ival
以引用作為初始值,實際上是以與引用綁定的對象作為初始值:
int &refVal3 = refVal; // 正確:refVal3綁定到了那個與refVal綁定的對象——ival上
因為引用本身不是一個對象,所以不能定義引用的引用。
引用的定義
允許在一條語句中定義多個引用,其中每個引用標識符都必須以符號&開頭:
int i = 1024, i2 = 2048; // i和i2都是int
int &r = i, r2 = i2; // r是一個引用,與i綁定在一起,r2是int
int i3 = 1024, &ri = i3; // i3是int,ri是一個引用,與i3綁定在一起
int &r3 = i3, &r4 = i2; // 一條語句定義多個引用
大多數情況下(詳情見下文注意), 引用的類型都要和與之綁定的對象嚴格匹配。而且引用只能綁定在對象上,不能與字面值霍某個表達式的計算結果綁定在一起。
int &refVal4 = 10; // warning:引用類型的初始值必須是一個對象
double dval = 3.14;
int &refVal5 = dval;
// warning:引用類型要和與之綁定的對象嚴格匹配,此處引用類型的初始值必須是int型對象
注意
上文說的大多數情況是指除了:
- 初始化常量引用時允許用任意表達式作為初始值
- 基類引用可以綁定到派生類對象上
第一點將在下文說明,第二點將在別的博文中說明。
指針
概念
指針: 與引用類似,指針也實現了對其他對象的間接訪問。
不同點在于:
- 指針本身就是一個對象,允許對指針賦值和拷貝,而且在指針的生命周期內它可以先后指向幾個不同的對象。
- 指針無須在定義時賦初值。和其他內置類型一樣,在塊作用域內定義的指針如果沒有被初始化,也將擁有一個不確定的值。
聲明方式
經常有一種觀點會誤認為,在定義語句中,類型修飾符(*和&)作用于本次定義的全部變量。
int* p1, p2; // p1是指向int的指針,p2是int
上述代碼中,基本數據類型是int而非int*。*僅僅是修飾了p1而已,對該聲明語句中的其他變量,例如p2并不產生任何作用。
取地址符
指針存放某個對象的地址,想獲取該地址,需要使用取地址符(操作符&):
int ival = 1024;
int *p = &ival; // p存放變量ival的地址,或者說p是指向變量ival的指針
因為引用不是對象,沒有實際地址,所以不能定義指向引用的指針。
同樣的,**大部分情況下,**指針的類型都要和它所指向的對象嚴格匹配:
double dval;
double *pd = &dval; // 正確:初始值是double型對象的地址
double *pd2 = pd; // 正確:初始值是指向double對象的指針int *pi = pd; // warning:指針pi的類型和pd的類型不匹配
pi = &dval; // warning:double型對象的地址賦給int型指針
因為在聲明語句中指針的類型實際上被用于指定它所指向對象的類型,所以二者必須匹配。如果指針指向了一個其他類型的對象,對該對象的操作將發生錯誤。
指針值
指針的值(即所指向的地址),應屬于下列4種狀態之一:
- 指向一個對象。
- 指向緊鄰對象所占空間的下一個位置。
- 空指針,意味著指針沒有指向任何對象。
- 無效指針,也就是上述情況之外的其他值。
試圖拷貝或以其他方式訪問無效指針的值都將引發錯誤。 編譯器并不負責檢查此類錯誤,這一點和試圖使用未經初始化的變量是一樣的。訪問無效指針的后果無法預計,因此程序員必須清楚任意給定的指針是否有效。
盡管第2種和第3種形式的指針是有效的,但其使用同樣受到限制。顯然這些指針沒有指向任何具體對象, 所以試圖訪問此類指針(假定的)對象的行為是不被允許的。如果這樣做了,后果也無法預計。
空指針
生成空指針的方法:
int *p1 = nullptr; // 等價于int *p1 = 0;
int *p2 = 0; // 直接將p2初始化為字面常量0
int *p3 = NULL; // 需要#include cstdlib
得到空指針最直接的辦法就是用字面值nullptr 來初始化指針。nullptr 是一種特殊類型的字面值,可以被轉換成任意其他的指針類型。
用名為NULL的預處理變量來給指針賦值,NULL在頭文件cstdlib中定義,它的值就是0。
在C++11新標準下,程序最好使用nullptr,同時盡量避免使用NULL。
注意:不能將int變量的直接賦給指針是錯誤的操作,即使int變量的值恰好等于0也不行。
int zero = 0;
pi = zero; // warning:不能把int變量直接賦給指針
利用指針訪問對象
允許使用解引用符(操作符*)來訪問該對象:
int ival = 42;
int *p = &ival; // p存放著變量ival的地址,或者說p是指向變量ival的指針
cout << *p; //由符號*得到指針p所指的對象,輸出42
cout << p; //得到指針p所指對象的地址,輸出ival的地址
對指針解引用會得出所指的對象,因此如果給解引用的結果賦值,實際上也就是給指針所指的對象賦值:
*p = 0; //由符號*得到指針p所指的對象,即可經由p為變量ival賦值
cout << *p; // 輸出0
解引用操作僅適用于那些確實指向了某個對象的有效指針。
賦值和指針
想要搞清楚一條賦值語句到底改變了指針的值還是改變了指針所指對象的值,最好的辦法就是記住賦值永遠改變的是等號左側的對象。
pi = &ival; // pi的值被改變,現在pi指向了ival
上述代碼的意思是為指針pi賦一個新的值,也就是改變了那個存放在pi內的地址值。
*pi = 0; // ival的值被改變,指針pi并沒有改變
上述代碼的意思是為指針pi所指對象賦一個新的值,也就是改變了pi所指對象的值。
void* 指針
void*是一種特殊的指針類型,可用于存放任意對象的地址。但是我們對該地址中到底是個什么類型的對象并不了解:
double obj = 3.14,*pd = &obj;void *pv = &obj; // 正確:void*能存放任意類型對象的地址,obj可以是任意類型的對象
pv = pd; // pv可以存放任意類型的指針
利用void指針能做的事兒比較有限:拿它和別的指針比較、作為函數的輸入或輸出,或者賦給另外一個void指針。不能直接操作void*指針所指的對象,因為我們并不知道這個對象到底是什么類型,也就無法確定能在這個對象上作哪寫操作。
概括來說,以void*的視角來看內存空間也就僅僅是內存空間,沒辦法訪問內存空間中所存的對象。
指向指針的指針
一般來說,聲明符中修飾符的個數并沒有限制。當有多個修飾符連寫在一起時,按照其邏輯關系詳加解釋即可。以指針為例,指針式內存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中。
通過*的個數可以區分指針的級別。也就是說,** 表示指向指針的指針, ***表示指向指針的指針的指針,以此類推:
int ival = 1024;
int *pi = &ival; // pi指向一個int型的數
int **ppi = π // ppi指向一個int型的指針
解引用int型指針會得到一個int型的數,同樣,解引用指向指針的指針會得到一個指針。為了訪問最原始的那個對象,需要對指針的指針做兩次解引用。
ival
*pi
**ppi
上述三種方式輸出的都是ival的值。
指向指針的引用
引用本身不是一個對象,因此不能定義指向引用的指針。但指針是對象,所以存在對指針的引用:
int i = 42;
int *p; // p是一個int型指針
int *&r = p; // r是一個對指針p的引用r = &i; // r引用了一個指針,因此給r賦值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的對象,將i的值改為0
要理解r的類型到底是什么,最簡單的辦法是從右向左閱讀r的定義。離變量名最近的符號對變量的類型有最直接的影響。對r來講就是(int *&r)中的&,因此r是一個引用。聲明符的其余部分用以確定r引用的類型是什么,此例中的符號 * 說明r引用的是一個指針。最后,聲明的基本數據類型部分指出r引用的是一個int指針。
初始化所有指針
在大多數編譯器環境下,如果使用了未經初始化的指針,則該指針所占的內存空間的當前內容將被看作一個地址值。訪問該指針,相當于去訪問一個本不存在的位置上的本不存在的對象。如果指針所占內存空間中恰好有內容,而這些內容又被當作了某個地址,我們就很難分清它到底是合法的還是非法的了。
良好的編程習慣應該是初始化所有指針,并且在可能的情況下,盡量等定義了對象之后再定義指向它的指針。如果實在不清楚指針應該指向何處,就把它初始化為nullptr或者0,這樣程序就能檢測并知道它沒有指向任何具體的對象了。
有多重含義的某些符號
像&和*這樣的符號,既能用作表達式里的運算符,也能作為聲明的一部分出現,符號的上下文決定了符號的意義:
int i = 42;
int &r = i; // &緊隨類型名出現,因此是聲明的一部分,r是個引用
int *p; // *緊隨類型名出現,因此是聲明的一部分,p是個指針
p = &i; // &出現在表達式中,是一個取地址符
*p = i; // *出現在表達式中,是一個解引用符
int &r2 = *p; // &是生命的一部分,*是一個解引用符
在聲明語句中, &和*用于組成復合類型;在表達式中, 又作為運算符。在不同場景下出現的雖然是同一個符號,但是由于含義截然不同,所以我們完全可以把它們當作不同的符號看待。
const限定符
概念
const可以對變量的類型加以限定,使得它的值不能被改變。
由于const對象一旦創建后其值就不能再改變,因此const對象必須初始化。
const int i = get_size(); // 正確,運行時初始化
const int j = 42; // 正確,編譯時初始化
const int k; // 錯誤,k是一個未經初始化的常量
const的引用
像其他對象一樣,可以把引用綁定到const對象上,稱之為對常量的引用。與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象。
const int ci = 1024; //
const int &r1 = ci; // 正確:引用及其對應的對象都是常量
r1 = 42; // 錯誤:r1是對常量的引用
int &r2 = ci; // 錯誤:試圖讓一個非常量的引用指向一個常量對象
因為不允許直接為ci賦值,當然也就不能通過引用去改變ci。因此,對r2的初始化是錯誤的。換種方式理解:假設該初始化合法,則可以通過r2來改變它引用對象的值,這顯然是不正確的。
通常引用的類型必須與其所引用對象的類型一致, 但有兩個例外,一種即在初始化常量時允許用任意表達式作為初始值,只要該表達式的結果能轉換成引用的類型即可。例如,允許為一個常量引用綁定非常量的對象、字面值,甚至是個一般表達式:
int i = 42;
const int &r1 = i; // 允許將const int&綁定到一個普通int對象上
const int &r2 = 42; // 正確:r2是一個常量引用
const int &r3 = r1 * 2; // 正確:r3是一個常量引用
int &r4 = r1 * 2; // 錯誤:r4是一個普通的非常量引用
r3 和 r4采用了同樣的初始化方法卻出現了不同的結果。為什么呢?這需要弄清楚當一個常量引用被綁定到另外一種類型上時到底發生了什么:
double dval = 3.14;
const int &ri = dval; // 正確
此處ri引用了一個int型的數。對ri的操作應該是整數運算,但dval卻是一個雙精度浮點數而非整數。因此為了確保讓ri綁定一個整數,編譯器把上述代碼變成了如下形式:
const int temp = dval; // 由雙精度浮點數生成一個臨時的整型常量
const int &ri = temp; // 讓ri綁定這個臨時量
這里簡單介紹臨時量(temporary)對象。所謂臨時量對象就是當編譯器需要一個空間來暫存表達式的求值結果時臨時創建的一個未命名的對象。
接下來簡單探討當 ri 不是常量時,如果執行了類似上面的初始化過程將帶來什么樣的后果。如果 ri 不是常量,則允許對 ri 賦值,這樣就會改變 ri 所引用對象的值。注意,此時綁定的對象是一個臨時量而非dval。因此賦值不會修改dval,而是修改了temp。程序員既然讓 ri 引用dval,是想讓 ri 改變dval,不會想著把引用綁定到臨時量上,C++自然也就把這種行為歸為非法。 ri 是常量的話不允許對其賦值,自然也就沒有修改temp而不是dval的隱患啦~
常量引用僅對引用可參與的操作做出了限定,對于引用的對象本身是不是一個常量未作限定。對象若是非常量,允許通過其他途徑改變它的值:
int i = 42;
int &r1 = i; // 引用ri綁定對象i
const int &r2 = i; // r2也綁定對象i,但是不允許通過r2修改i的值
r1 = 0; // r1并非常量,i的值修改為0
r2 = 0; // 錯誤:r2是一個常量引用
r2 綁定非常量整數 i 是合法行為,但是不允許通過 r2 修改 i 的值。 但 i 的值仍然允許通過其他途徑修改,既可以直接給 i 賦值,也可以通過 r1 一樣綁定到 i 的其他引用來修改。
PS:有時候經常會遇到將“對const的引用”稱作“常量引用”的情況,但嚴格來說并不存在常量引用,因為引用本身不是一個對象, 所以我們沒法讓引用本身恒定不變。但事實上,由于C++并不允許隨意改變引用所綁定的對象,所以從這層意義上理解所有的引用又都算常量。引用的對象是常量還是非常量可以決定其所能參與的操作,卻無論如何都不會影響到引用和對象的綁定關系本身。
指針和const
指針亦可指向常量或非常量。指向常量的指針(pointer to const)不能用于改變其所指對象的值,想要存放常量對象的地址,只能使用指向常量的指針:
const double pi = 3.14; // pi是個常量,其值不可更改
double *ptr = π // 錯誤:ptr是個普通指針
const double *cptr = π // 正確:cptr可以指向一個雙精度常量
*cptr = 42; // 錯誤:不能給*cptr賦值
通常來講指針的類型必須和其所指對象的類型一致。但是有兩種例外情況,其中之一便是允許一個指向常量的指針指向一個非常量對象:
double dval = 3.14;
cptr = &dval; // 正確:但是不能通過cptr改變dval的值
和常量引用一樣,指向常量的指針也沒有規定其所指的對象必須是一個常量。 所謂指向常量的指針,僅僅要求不能通過該指針改變對象的值,而沒有規定那個對象的值不能通過其他途徑改變。
指針是對象而引用不是,因此允許將指針本身定為常量。常量指針 (const pointer)必須初始化。而且一旦初始化完成,則它的值(也就是存放在指針中的那個地址)就不能再改變了。把 * 放在const關鍵字之前用以說明指針是一個常量,即不變的是指針本身的值而非指向的那個值:
int errNumb = 0;
int *const curErr = &errNumb; // curErr將一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一個指向常量對象的常量指針
還是重提一下如何明晰聲明的含義,拿curErr來講,離curErr最近的符號是const,意味著curErr本身是一個常量對象,聲明符中下一個符號是*,意思是curErr是一個常量指針。最后,該聲明語句的基本數據類型部分確定了常量指針指向的是一個int對象。同理可得,pip是一個常量指針,指向的對象是一個雙精度浮點型常量。
指針本身是一個常量并不意味著不能通過指針修改其所指對象的值,能否這樣做完全依賴于所指對象的類型。例如,pip是要給指向常量的常量指針,則不論是pip所指的對象值,還是pip自己存儲的那個地址都不能改變。相反的,curErr指向的是一個一般的非常量整型,那么就完全可以用curErr去修改errNumb的值:
*pip = 2.72; // 錯誤:pip是一個指向常量的指針
*curErr = 3; // 正確:將curErr所指的對象的值改為3
頂層const和底層const
由于指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針所指的是不是一個常量就是兩個相互獨立的問題。用名詞頂層 const(top-level const)表示指針本身是個常量,而用名詞底層 const(low-level const)表示指針所指的對象是一個常量。
引申來講,頂層const可以表示任意的對象是常量,這一點對任何數據類型都適用,如算術類型、類、指針等。底層const則與指針和引用等復合類型的基本類型部分有關。指針類型既可以是頂層也可以是底層,這一點和其他類型相比區別明顯。
const
在 */&
左邊的是頂層,在右邊的是底層:
int i = 0;
int *const pi = &i; // 不能改變p1的值,這是一個頂層const
const int ci = 42; // 不能改變ci的值,這是一個頂層的const
const int *p2 = &ci; // 允許改變p2的值,這是一個底層const
const int *const p3 = p2; // 靠右的const是頂層const,靠左的const是底層const
const int &r = ci; // 用于聲明引用的const都是底層const
當執行對象的拷貝操作時,常量是頂層const還是底層const區別明顯。頂層const不受什么影響:
i = ci; // 正確:拷貝ci的值,ci是一個頂層const,拷貝操作不會更改ci的值
p2 = p3; // 正確:p2和p3指向的對象類型相同,都是指向常量的指針
拷貝操作并不改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什么影響。
但底層const的限制卻不容忽視。當執行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠轉換。一般來說,非常量可以轉換成常量,反之則不行:
int *p = p3; // 錯誤:p3包含底層const含義,指向一個常量對象,賦值給p的話有可能會更改指向對象的值
p2 = p3; // 正確:p2和p3都是底層const
p2 = &i; // 正確:int*能轉換成const int*
int &r = ci; // 錯誤:普通的int&不能綁定到int常量上
const int &r2 = i; // 正確:const int&可以綁定到一個普通的int上
指向常量的指針和對const的引用,不過是指針或引用覺得自己指向了常量,所以自覺地不去改變所指對象的值。所以指向(綁定)常量,也可以指向(綁定)非常量。
constexpr和常量表達式
定義:常量表達式(const expression)是指值不會改變并且在編譯過程就能得到計算結果的表達式。
顯然,字面值屬于常量表達式,用常量表達式初始化的const對象也是常量表達式。
一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如:
const int max_files = 20; // max_files是常量表達式
const int limit = max_files; // limit是常量表達式
int i = 30; // i不是常量表達式
const int sz = get_size(); //要在運行階段才能初始化,sz不是常量表達式
從定義我們可以知道常量表達式必須具備兩個特征:
- 值不會改變
- 編譯過程就能得到計算結果
因此盡管 i 的初始值是個字面值常量,滿足第二點,但是它的數據類型只是一個普通的int而非const int,所以它不屬于常量表達式。
constexpr 變量
在一個復雜系統中,幾乎肯定不能分辯一個初始值到底是不是常量表達式。當然可以定義一個const變量并把它初始值設為我們認為的某個常量表達式,但在實際使用時,盡管如此要求卻常常發現初始值并非常量表達式的情況。在此種情況下,對象的定義和使用根本就是兩碼事兒。
C++11標準規定,允許將變量聲明為constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明為constexpr的變量一定是一個常量,而且必須用常量表達式初始化:
constexpr int mf = 20; // 20是常量表達式
constexpr int limit = mf + 1; // mf + 1 是常量表達式
constexpr int sz = size(); // 只有當size是一個constexpr函數時// 才是一條正確的聲明語句
盡管不能使用普通函數作為constexpr變量的初始值,但是允許定義一種特殊的constexpr函數。
一般來說,如果你認定變量是一個常量表達式,那就把它聲明成constexpr類型。
字面值類型
常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為 “字面值類型”(literal type)。
到目前為止接觸過的數據類型中,算術類型、引用和指針都屬于字面值類型。自定義類、IO庫、string類型則不屬于字面值類型,也就不能被定義成constexpr。
盡管指針和引用都能定義成constexpr,但他們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲于某個固定地址中的對象。
函數體內定義的變量一般來說并非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義于所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。同時,C++允許函數定義一類有效范圍超出函數本身的變量,這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用可以綁定到這樣的變量上,constexpr指針也可以指向這樣的變量。
指針和constexpr
constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關:
const int *p = nullptr; // p是一個指向整型常量的指針
constexpr int *q = nullptr; // q是一個指向整數的常量指針
q和p類型相差甚遠,p是一個指向常量的指針,而q是一個常量指針,其中的關鍵在于constexpr把它所定義的對象中置為了頂層const。
與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個非常量:
constexpr int *np = nullptr; // np是一個指向整數的常量指針,其值為空
int j = 0;
constexpr int i = 42; // i的類型是整型常量
// i和j都必須定義在函數體之外
constexpr const int *p = &i; // p是指向整性常量的常量指針,指向整型常量i
constexpr int *p1 = &j; // p1是常量指針,指向整數j