C++ 語言指針 (pointer)

Yongqiang Cheng 2021-08-16 01:22:57 阅读数:798

本文一共[544]字,预计阅读时长:1分钟~
c++ pointer

C++ 語言指針 (pointer)

指針 (pointer) 是指向 (point to) 另外一種類型的複合類型。與引用類似,指針也實現了對其他對象的間接訪問。然而指針與引用相比又有很多不同點。其一,指針本身就是一個對象,允許對指針賦值和拷貝,而且在指針的生命周期內它可以先後指向幾個不同的對象。其二,指針無須在定義時賦初值。和其他內置類型一樣,在塊作用域內定義的指針如果沒有被初始化,也將擁有一個不確定的值。

定義指針類型的方法將聲明符寫成 *d 的形式,其中 d 是變量名。如果在一條語句中定義了幾個指針變量,每個變量前面都必須有符號 *

1. 獲取對象的地址

指針存放某個對象的地址,要想獲取該地址,需要使用取地址符 (操作符 &)。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* ipl 和 ip2 都是指向 int 型對象的指針。 */
int *ipl, *ip2;
/* dp2 是指向 double 型對象的指針,dp1 是 double 型對象。 */
double dp1, *dp2;
int val = 42;
/* pval 存放變量 val 的地址,或者說 pval 是指向變量 val 的指針。 */
int *pval = &val;
return 0;
}

在這裏插入圖片描述

第四條語句把 pval 定義為一個指向 int 的指針,隨後初始化 pval 令其指向名為 val 的 int 對象。因為引用不是對象,沒有實際地址,所以不能定義指向引用的指針。

2. 指針值

指針的值 (即地址) 應屬下列 4 種狀態之一:

  1. 指向一個對象。
  2. 指向緊鄰對象所占空間的下一個比特置。
  3. 空指針,意味著指針沒有指向任何對象。
  4. 無效指針,也就是上述情况之外的其他值。

試圖拷貝或以其他方式訪問無效指針的值都將引發錯誤。編譯器並不負責檢查此類錯誤,這一點和試圖使用未經初始化的變量是一樣的。

盡管第 2 種和第 3 種形式的指針是有效的,但其使用同樣受到限制。顯然這些指針沒有指向任何具體對象,所以試圖訪問此類指針 (假定的) 對象的行為不被允許。

3. 利用指針訪問對象

如果指針指向了一個對象,則允許使用解引用符 (操作符 *) 來訪問該對象,解引用操作僅適用於那些確實指向了某個對象的有效指針。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* pval 存放著變量 val 的地址,或者說 pval 是指向變量 val。 */
int val = 99;
int *pval = &val;
/* 由符號 * 得到指針 pval 所指的對象,輸出 99。 */
std::cout << *pval << std::endl;
/* 由符號 * 得到指針 pval 所指的對象,可經由 pval 為變量 val 賦值。 */
*pval = 22;
std::cout << *pval << std::endl;
int i = 42;
/* & 緊隨類型名出現,因此是聲明的一部分,ref 是一個引用。 */
int &ref = i;
/* * 緊隨類型名出現,因此是聲明的一部分,p 是一個指針。 */
int *p;
/* & 出現在錶達式中,是一個取地址符。 */
p = &i;
/* * 出現在錶達式中,是一個解引用符。 */
*p = i;
/* & 是聲明的一部分,* 是一個解引用符。 */
int &r = *p;
return 0;
}
99
22
請按任意鍵繼續. . .

對指針解引用會得出所指的對象,因此如果給解引用的結果賦值,實際上也就是給指針所指的對象賦值。如上述程序所示,為 *pval 賦值實際上是為 *pval 所指的對象賦值。

&* 這樣的符號,既能用作錶達式裏的運算符,也能作為聲明的一部分出現,符號的上下文决定了符號的意義。在聲明語句中,&* 用於組成複合類型;在錶達式中,它們的角色又轉變成運算符。

4. 空指針

空指針 (null pointer) 不指向任何對象,在試圖使用一個指針之前代碼可以首先檢查它是否為空。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int *pl = nullptr;
int *p2 = 0;
int *p3 = NULL;
return 0;
}

在這裏插入圖片描述

得到空指針最直接的辦法就是用字面值 nullptr 來初始化指針,這也是 C++11 新標准引入的一種方法。nullptr 是一種特殊類型的字面值,它可以被轉換成任意其他的指針類型。也可以通過將指針初始化為字面值 0 來生成空指針。

過去的程序還會用到一個名為 NULL 的預處理變量 (preprocessor variable) 來給指針賦值,這個變量在頭文件 cstdlib 中定義,它的值就是 0。預處理變量不屬於命名空間 std,它由預處理器負責管理,因此我們可以直接使用預處理變量而無須在前面加上 std::

當用到一個預處理變量時,預處理器會自動地將它替換為實際值,因此用 NULL 初始化指針和用 0 初始化指針是一樣的。在新標准下,現在的 C++ 程序最好使用 nullptr,同時盡量避免使用 NULL

把 int 變量直接賦給指針是錯誤的操作,即使 int 變量的值恰好等於 0 也不行。

// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int *pl = nullptr;
int *p2 = 0;
int *p3 = NULL;
int zero = 0;
int *pi = zero;
return 0;
}
1>d:\visual_studio_workspace\...\yongqiang.cpp(18): error C2440: 'initializing': cannot convert from 'int' to 'int *'
1> d:\visual_studio_workspace\...\yongqiang.cpp(18): note: Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or function-style cast

建議初始化所有的指針,並且在可能的情况下,盡量等定義了對象之後再定義指向它的指針。如果實在不清楚指針應該指向何處,就把它初始化為 nullptr 或者 0,這樣程序就能檢測並知道它沒有指向任何具體的對象了。

5. 賦值和指針

指針和引用都能提供對其他對象的間接訪問,引用本身並非一個對象。一旦定義了引用,就無法令其再綁定到另外的對象,之後每次使用這個引用都是訪問它最初綁定的那個對象。指針和它存放的地址之間就沒有這種限制,和其他任何變量 (只要不是引用) 一樣,給指針賦值就是令它存放一個新的地址,從而指向一個新的對象。

記住賦值永遠改變的是等號左側的對象。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int val = 66;
/* p1 被初始化,但沒有指向任何對象。 */
int *p1 = 0;
/* p2 被初始化,存有 val 的地址。 */
int *p2 = &val;
int *pi = nullptr;
/* pi 的值被改變,pi 指向 val。 */
pi = &val;
/* val 的值被改變,指針 pi 並沒有改變。 */
*pi = 1;
return 0;
}

在這裏插入圖片描述

pi = &val;

pi 賦一個新的值,也就是改變了那個存放在 pi 內的地址值。

*pi = 1;

*pi (指針 pi 指向的對象) 發生改變。

6. 其他指針操作

只要指針擁有一個合法值,就能將它用在條件錶達式中。如果指針的值是 0,條件值為 false,任何非 0 指針對應的條件值都是 true。

對於兩個類型相同的合法指針,可以用相等操作符 (==) 或不相等操作符 (!=) 來比較它們,比較的結果是布爾類型。如果兩個指針存放的地址值相同,則它們相等;反之它們不相等。這裏兩個指針存放的地址值相同 (兩個指針相等) 有三種可能:它們都為空、都指向同一個對象,或者都指向了同一個對象的下一地址。需要注意的是,一個指針指向某對象,同時另一個指針指向另外對象的下一地址,此時也有可能出現這兩個指針值相同的情况,即指針相等。

7. void* 指針

void* 是一種特殊的指針類型,可用於存放任意對象的地址。一個 void* 指針存放著一個地址,這一點和其他指針類似。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
double obj = 3.1415926, *pobj = &obj;
/* void* 能存放任意類型對象的地址,obj 可以是任意類型的對象。 */
void *pv = &obj;
pv = pobj;
return 0;
}

在這裏插入圖片描述

利用 void* 指針能做的事兒比較有限:拿它和別的指針比較、作為函數的輸入或輸出,或者賦給另外一個 void* 指針。不能直接操作 void* 指針所指的對象,因為我們並不知道這個對象到底是什麼類型,也就無法確定能在這個對象上做哪些操作。概括說來,以 void* 的視角來看內存空間也就僅僅是內存空間,沒辦法訪問內存空間中所存的對象。

8. 定義多個變量

複合類型 (compound type) 是指基於其他類型定義的類型,例如引用和指針。一條聲明語句由一個基本數據類型 (base type) 和緊隨其後的一個聲明符 (declarator) 列錶組成。每個聲明符命名了一個變量並指定該變量為與基本數據類型有關的某種類型。在同一條定義語句中,雖然基本數據類型只有一個,但是聲明符的形式卻可以不同。一條定義語句可能定義出不同類型的變量。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* i 是一個 int 數,p 是一個 int 指針,r 是一個 int 引用。 */
int i = 1024, *p = &i, &r = i;
return 0;
}

在這裏插入圖片描述

有一種觀點會誤以為,在定義語句中,類型修飾符 (*&) 作用於本次定義的全部變量。造成這種錯誤看法的原因有很多,其中之一是我們可以把空格寫在類型修飾符和變量名中間。

int* p; // 合法,但是容易產生誤導

這種寫法可能產生誤導是因為 int* 放在一起好像是這條語句中所有變量共同的類型一樣。其實恰恰相反,基本數據類型是 int 而非 int** 僅僅是修飾了 p 而已,對該聲明語句中的其他變量,它並不產生任何作用。

int* p1, p2; // p1 是指向 int 的指針,p2 是 int

涉及指針或引用的聲明,一般有兩種寫法。第一種把修飾符和變量標識符寫在一起:

int *p1, *p2; // p1 和 p2 都是指向 int 的指針

這種形式著重强調變量具有的複合類型。第二種把修飾符和類型名寫在一起,並且每條語句只定義一個變量。

int* p1; // p1 是指向 int 的指針
int* p2; // p2 是指向 int 的指針

推薦采用第一種寫法,將 * (或是 &) 與變量名連在一起。

9. 指向指針的指針

一般來說,聲明符中修飾符的個數並沒有限制。當有多個修飾符連寫在一起時,按照其邏輯關系詳加解釋即可。以指針為例,指針是內存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中。

通過 * 的個數可以區分指針的級別。也就是說,** 錶示指向指針的指針,*** 錶示指向指針的指針的指針。

此處 pi 是指向 int 的指針,而 ppi 是指向 int 指針的指針。

在這裏插入圖片描述

解引用 int 指針會得到一個 int 數,解引用指向指針的指針會得到一個指針。此時為了訪問最原始的那個對象,需要對指針的指針做兩次解引用。

#include <iostream>
using std::cout;
using std::endl;
int main()
{
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = &pi; // ppi points to a pointer to an int
cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **ppi
<< endl;
int i = 2;
int *p1 = &i; // p1 points to i
*p1 = *p1 * *p1; // equivalent to i = i * i
cout << "i = " << i << endl;
*p1 *= *p1; // equivalent to i *= i
cout << "i = " << i << endl;
return 0;
}
The value of ival
direct value: 1024
indirect value: 1024
doubly indirect value: 1024
i = 4
i = 16
請按任意鍵繼續. . .

該程序使用三種不同的方式輸出了變量 ival 的值:第一種直接輸出;第二種通過 int 指針 pi 輸出;第三種兩次解引用 ppi,取得 ival 的值。

10. 指向指針的引用

引用本身不是一個對象,因此不能定義指向引用的指針。但指針是對象,所以存在對指針的引用。

//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int ival = 1024;
int *p; // p 是一個 int 指針
int *&r = p; // r 是一個對指針 p 的引用
r = &ival; // r 引用了一個指針,給 r 賦值 &ival 就是令 p 指向 ival
*r = 512; // 解引用 r 得到 ival,也就是 p 指向的對象,將 ival 的值改為 512
return 0;
}

在這裏插入圖片描述

面對一條比較複雜的指針或引用的聲明語句時,從右向左閱讀有助於弄清楚它的真實含義。離變量名最近的符號 (此例中是 &r 的符號 &) 對變量的類型有最直接的影響,因此 r 是一個引用。聲明符的其餘部分用以確定 r 引用的類型是什麼,此例中的符號 * 說明 r 引用的是一個指針。最後,聲明的基本數據類型部分指出 r 引用的是一個 int 指針。

* - 解引用
& - 取地址

References

(美) Stanley B. Lippman, (美) Josée Lajoie, (美) Barbara E. Moo 著, 王剛, 楊巨峰 譯. C++ Primer 中文版[M]. 第 5 版. 電子工業出版社, 2013.
https://www.informit.com/store/c-plus-plus-primer-9780321714114

版权声明:本文为[Yongqiang Cheng]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210816012233583v.html