文章目錄
- 復合型別
- 參考
- 概念與使用
- 參考的定義
- 注意
- 指標
- 概念
- 宣告方式
- 取地址符
- 指標值
- 空指標
- 利用指標訪問物件
- 賦值和指標
- 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則與指標和參考等復合型別的基本型別部分有關,指標型別既可以是頂層也可以是底層,這一點和其他型別相比區別明顯:
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/249435.html
標籤:其他
下一篇:關于經濟學家的笑話
