指標

我們已經看到,變數可以被視為記憶體單元,透過其識別符號進行訪問。這樣,我們就不必關心資料在記憶體中的物理位置,只需在需要引用變數時使用其識別符號即可。

你的計算機記憶體可以想象成一連串的記憶體單元,每個單元都是計算機能管理的最小尺寸(一個位元組)。這些單位元組記憶體單元按連續方式編號,因此在任何記憶體塊中,每個單元的編號都比前一個單元的編號多一。

這樣,每個單元都可以很容易地在記憶體中定位,因為它有一個唯一的地址,並且所有記憶體單元都遵循連續的模式。例如,如果我們要查詢單元格1776,我們知道它會正好在單元格1775和1777之間,正好在776之後一千個單元,正好在2776之前一千個單元。

引用運算子 (&)

一旦我們宣告一個變數,所需記憶體量就會在記憶體中的特定位置(其記憶體地址)為其分配。我們通常不會主動決定變數在我們想象的記憶體單元面板中的確切位置——幸運的是,這是作業系統在執行時自動執行的任務。然而,在某些情況下,我們可能需要知道變數在執行時儲存的地址,以便對其進行相對位置的操作。

定位變數在記憶體中的地址,我們稱之為對該變數的引用。透過在變數識別符號前加上一個“與”號(&),即可獲得對變數的引用,這被稱為引用運算子,字面意思可以翻譯為“……的地址”。例如:

1
ted = &andy;

這將把變數ted的地址賦給andy,因為當變數名andy前面加上引用運算子(&)時,我們不再談論變數本身的內容,而是談論它的引用(即它在記憶體中的地址)。

從現在起,我們將假設andy在執行時被放置在記憶體地址1776中。這個數字(1776)只是我們現在為了幫助闡明本教程中的一些概念而隨意假設的,但實際上,我們無法在執行時之前知道變數在記憶體中地址的真實值。

考慮以下程式碼片段:

1
2
3
andy = 25;
fred = andy;
ted = &andy;

執行後,每個變數中包含的值如下圖所示:


首先,我們將值25賦給andy(一個我們假設其記憶體地址為1776的變數)。

第二條語句將變數fred的內容(即25)複製到andy。這是一個標準的賦值操作,我們之前已經做過很多次了。

最後,第三條語句複製到ted的不是andy中包含的值,而是它的引用(即它的地址,我們假設是1776)。原因是,在這次第三個賦值操作中,我們在識別符號andy前面加上引用運算子(&之前加上了),因此我們不再引用andy的值,而是引用它的引用(它在記憶體中的地址)。

儲存另一個變數引用的變數(例如上例中的ted)就是我們所說的指標。指標是C++語言一個非常強大的特性,在高階程式設計中有很多用途。稍後,我們將看到這種型別的變數是如何使用和宣告的。

解引用運算子 (*)


我們剛剛看到,儲存另一個變數引用的變數稱為指標。指標被認為“指向”它們儲存引用的變數。

使用指標,我們可以直接訪問它所指向的變數中儲存的值。為此,我們只需在指標的識別符號前加上一個星號(*),它充當解引用運算子,字面意思可以翻譯為“由……指向的值”。

因此,繼續使用上一個示例的值,如果我們寫

1
beth = *ted;

(我們可以讀作:“beth等於由ted") beth指向的值”),25將取值ted的 C++ 等效檔案是1776,因為


你必須清楚地區分表示式ted指的是值1776指向的是一個型別為,而*ted*(識別符號前帶有星號1776)指的是儲存在地址25的值,在本例中是

1
2
beth = ted;   // beth equal to ted ( 1776 )
beth = *ted;  // beth equal to value pointed by ted ( 25 ) 

注意引用和解引用運算子的區別:
  • &是引用運算子,可以讀作“...的地址”
  • *是解引用運算子,可以讀作“...指向的值”
因此,它們具有互補(或相反)的含義。用&引用的變數可以用*.

前面我們執行了以下兩個賦值操作:

1
2
andy = 25;
ted = &andy;

緊隨這兩個語句之後,所有以下表達式的結果都為真:

1
2
3
4
andy == 25
&andy == 1776
ted == 1776
*ted ==

第一個表示式非常清楚,考慮到對andy執行的賦值操作是andy=25。第二個表示式使用了引用運算子(&),它返回變數andy的地址,我們假設它的值為1776。第三個表示式有些明顯,因為第二個表示式為真,並且對ted執行的賦值操作是執行的賦值操作是ted=&andy*。第四個表示式使用瞭解引用運算子(ted),正如我們剛剛看到的,它可以讀作“指向的值”,而指向的值25.

確實是ted。所以,經過這一切,你也可以推斷,只要

1
*ted ==

宣告指標型別的變數

由於指標能夠直接引用它所指向的值,因此在宣告時有必要指定指標將指向的資料型別。指向char與指向intfloat.

指標的宣告遵循以下格式:

type * name;

,其中型別是指標預期指向的值的資料型別。此型別不是指標本身的型別!而是指標指向的資料的型別。例如:

1
2
3
int * number;
char * character;
float * greatnumber;

這是三個指標的宣告。每個都旨在指向不同的資料型別,但實際上它們都是指標,並且它們都將佔用相同的記憶體空間(指標的記憶體大小取決於程式碼將在其上執行的平臺)。然而,它們所指向的資料不佔用相同的記憶體空間,也不是相同的型別:第一個指向一個int,第二個指向一個char,最後一個指向一個float。因此,儘管這三個示例變數都是指標,佔用相同的記憶體大小,但它們被認為具有不同的型別int*, char*float*,分別取決於它們指向的型別。

我想強調的是,我們在宣告指標時使用的星號(*)僅僅表示它是一個指標(它是其型別複合說明符的一部分),不應與我們稍早看到的解引用運算子混淆,解引用運算子也用星號(*)表示。它們只是用同一個符號表示的兩個不同事物。

現在看看這段程式碼:

// my first pointer
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue, secondvalue;
  int * mypointer;

  mypointer = &firstvalue;
  *mypointer = 10;
  mypointer = &secondvalue;
  *mypointer = 20;
  cout << "firstvalue is " << firstvalue << endl;
  cout << "secondvalue is " << secondvalue << endl;
  return 0;
}
firstvalue is 10
secondvalue is 20

請注意,儘管我們從未直接設定firstvaluesecondvalue的值,但兩者最終都透過使用mypointer間接設定了值。這就是過程:

首先,我們將mypointer的引用賦值給firstvalue,使用引用運算子(&)。然後我們將值10賦值給由mypointer指向的記憶體位置,因為此時它指向firstvalue的記憶體位置,這實際上修改了firstvalue.

為了證明一個指標在同一個程式中可以取幾個不同的值,我用secondvalue和那個相同的指標mypointer.

這是一個更詳細的例子:

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue = 5, secondvalue = 15;
  int * p1, * p2;

  p1 = &firstvalue;  // p1 = address of firstvalue
  p2 = &secondvalue; // p2 = address of secondvalue
  *p1 = 10;          // value pointed by p1 = 10
  *p2 = *p1;         // value pointed by p2 = value pointed by p1
  p1 = p2;           // p1 = p2 (value of pointer is copied)
  *p1 = 20;          // value pointed by p1 = 20
  
  cout << "firstvalue is " << firstvalue << endl;
  cout << "secondvalue is " << secondvalue << endl;
  return 0;
}
firstvalue is 10
secondvalue is 20

我已將每行程式碼的閱讀方式作為註釋包含在內:&(&)表示“……的地址”,*(*)表示“……指向的值”。

請注意,有包含指標的表示式p1p2*,既有帶解引用運算子(*)的,也有不帶解引用運算子的。使用解引用運算子(

另一件可能引起你注意的是這一行:

1
int * p1, * p2;

這聲明瞭上一個例子中使用的兩個指標。但請注意,每個指標都有一個星號(*),以便兩者都具有int*型別(指向int).

)。否則,在該行中宣告的第二個變數的型別將是int(而不是int*),因為優先順序關係。如果我們寫成:

1
int * p1, p2;

p1確實會是int*型別,但會是int型別(為此目的,空格完全不重要)。這是由於運算子優先順序規則。但無論如何,對於大多數指標使用者來說,只需記住每個指標都必須放一個星號就足夠了。

指標和陣列

陣列的概念與指標的概念緊密相連。實際上,陣列的識別符號等同於其第一個元素的地址,就像指標等同於它所指向的第一個元素的地址一樣,因此它們實際上是相同的概念。例如,假設有以下兩個宣告:

1
2
int numbers [20];
int * p;

以下賦值操作將是有效的:

1
p = numbers;

之後,pnumbersp將是等效的,並具有相同的屬性。唯一的區別是我們可以改變指標的值,而int將始終指向其定義的20個p型別元素的第一個。因此,與是一個普通指標不同,

1
numbers = p;

因為是一個數組,所以它作為常量指標操作,我們不能給常量賦值。

由於變數的特性,以下示例中所有包含指標的表示式都是完全有效的:

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int numbers[5];
  int * p;
  p = numbers;  *p = 10;
  p++;  *p = 20;
  p = &numbers[2];  *p = 30;
  p = numbers + 3;  *p = 40;
  p = numbers;  *(p+4) = 50;
  for (int n=0; n<5; n++)
    cout << numbers[n] << ", ";
  return 0;
}
10, 20, 30, 40, 50, 

在關於陣列的章節中,我們多次使用方括號([])來指定我們想要引用的陣列元素的索引。這些方括號運算子[]也是一種解引用運算子,稱為偏移運算子。它們解引用它們所跟隨的變數,就像*一樣,但它們也將方括號內的數字新增到被解引用的地址中。例如:

1
2
a[5] = 0;       // a [offset of 5] = 0
*(a+5) = 0;     // pointed by (a+5) = 0 

這兩個表示式是等效的且有效,無論a是指標還是a是陣列。

指標初始化

宣告指標時,我們可能希望明確指定它們要指向的變數:

1
2
int number;
int *tommy = &number;

此程式碼的行為等同於:

1
2
3
int number;
int *tommy;
tommy = &number;

當指標初始化時,我們總是將指標指向的引用值(tommy)賦值,而不是被指向的值(*tommy)。你必須考慮到,在宣告指標時,星號(*)僅表示它是一個指標,它不是解引用運算子(儘管兩者使用相同的符號:*)。記住,它們是同一個符號的兩種不同功能。因此,我們必須注意不要將前面的程式碼與

1
2
3
int number;
int *tommy;
*tommy = &number;

混淆,這是不正確的,而且如果你仔細想想,在這種情況下也沒有太大意義。

與陣列的情況一樣,編譯器允許一種特殊情況,即我們希望在宣告指標的同時用常量初始化指標所指向的內容:

1
const char * terry = "hello";

在這種情況下,記憶體空間被保留以包含"hello",然後將此記憶體塊第一個字元的指標分配給terry。如果我們假設"hello"儲存在從地址1702開始的記憶體位置,我們可以將之前的宣告表示為:


重要的是要指出terry包含值1702,而不是'h'也不是"hello",儘管1702確實是這兩者的地址。

指標terry指向一串字元,可以像陣列一樣讀取(記住陣列就像一個常量指標)。例如,我們可以用以下兩種表示式中的任何一種訪問陣列的第五個元素:

1
2
*(terry+4)
terry[4]

兩個表示式的值都是'o'(陣列的第五個元素)。

指標算術


對指標執行算術運算與對常規整數資料型別執行算術運算略有不同。首先,只允許對它們進行加法和減法運算,其他運算在指標世界中沒有意義。但是,加法和減法對指標的行為根據它們指向的資料型別的大小而不同。

當我們瞭解不同的基本資料型別時,我們看到有些在記憶體中佔用空間多於其他。例如,我們假設在某個特定機器的給定編譯器中,char佔用1位元組,short佔用2位元組,long佔用4位元組。

假設我們在此編譯器中定義了三個指標:

1
2
3
char *mychar;
short *myshort;
long *mylong;

並且我們知道它們分別指向記憶體位置1000, 20003000

所以如果我們寫:

1
2
3
mychar++;
myshort++;
mylong++;

mychar,正如你所期望的,將包含值1001。但並不那麼明顯,myshort將包含值2002,而將包含3004mylong


這適用於對指標進行任何數字的加法和減法。如果我們寫:

1
2
3
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;

增加(++)和減少(--)運算子的優先順序都高於解引用運算子(*),但兩者在使用字尾形式時都有特殊的行為(表示式會用增加前的值進行計算)。因此,以下表達式可能會導致混淆:

1
*p++

因為++優先順序高於*,因此此表示式等價於*(p++)。因此,它所做的是增加p的值(所以它現在指向下一個元素),但由於++用作字尾,整個表示式被評估為原始引用指向的值(指標在增加之前指向的地址)。

注意與

(*p)++
的區別。在這裡,表示式將被評估為p指向的值加一。而p的值(指標本身)不會被修改(被修改的是這個指標所指向的內容)。

如果我們寫:

1
*p++ = *q++;

因為++的優先順序高於*,那麼pq都會增加,但是由於兩個增加運算子(++)都是用作字尾而不是字首,所以賦給*p的 C++ 等效檔案是 *qpq的值是它們在兩者增加之前的值。然後兩者都被增加。這大致相當於:

1
2
3
*p = *q;
++p;
++q;

一如既往,我建議你使用括號(()),以避免意想不到的結果並提高程式碼的可讀性。

指向指標的指標

C++ 允許使用指向指標的指標,這些指標又指向資料(甚至指向其他指標)。為此,我們只需要在宣告中為每個引用級別新增一個星號(*):

1
2
3
4
5
6
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;

假設為7230, 809210502的每個變數隨機選擇記憶體位置,這可以表示為:


每個變數的值寫在每個單元格內;單元格下方是它們各自在記憶體中的地址。

這個例子中的新事物是變數c,它可以在三個不同的間接級別中使用,每個級別對應一個不同的值:

  • c的型別是char**,值為8092
  • *c的型別是char*,值為7230
  • **c的型別是char,值為'z'

void 指標

要放回的字元的void指標是一種特殊型別的指標。在 C++ 中,void表示沒有型別,因此 void 指標是指向沒有型別(因此也具有不確定長度和不確定解引用屬性)的值的指標。

這使得 void 指標可以指向任何資料型別,從整數值或浮點數到字串。但作為交換,它們有一個很大的限制:它們指向的資料不能直接解引用(這是合乎邏輯的,因為我們沒有型別可以解引用),因此我們總是必須在解引用之前將 void 指標中的地址轉換為指向具體資料型別的其他指標型別。

它的一個用途可能是將通用引數傳遞給函式。

// increaser
#include <iostream>
using namespace std;

void increase (void* data, int psize)
{
  if ( psize == sizeof(char) )
  { char* pchar; pchar=(char*)data; ++(*pchar); }
  else if (psize == sizeof(int) )
  { int* pint; pint=(int*)data; ++(*pint); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  cout << a << ", " << b << endl;
  return 0;
}
y, 1603

sizeof是 C++ 語言中一個內建運算子,它返回其引數的位元組大小。對於非動態資料型別,這個值是一個常量。因此,例如,sizeof(char)的 C++ 等效檔案是1,因為char型別長度為1位元組。

空指標

空指標是任何指標型別的常規指標,它具有一個特殊值,表示它不指向任何有效的引用或記憶體地址。此值是將整數值零型別轉換為任何指標型別的結果。

1
2
int * p;
p = 0;     // p has a null pointer value 

不要將空指標與 void 指標混淆。空指標是任何指標都可能採用的值,表示它指向“無處”,而 void 指標是一種特殊型別的指標,可以指向某處但沒有特定型別。一個指的是指標本身儲存的值,另一個指的是它指向的資料型別。

函式指標

C++ 允許對函式指標進行操作。其典型用途是將函式作為引數傳遞給另一個函式,因為函式不能直接解引用地傳遞。為了宣告一個函式指標,我們必須像函式原型一樣宣告它,但函式名要用括號括起來:()並在函式名前插入一個星號(*)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pointer to functions
#include <iostream>
using namespace std;

int addition (int a, int b)
{ return (a+b); }

int subtraction (int a, int b)
{ return (a-b); }

int operation (int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*minus)(int,int) = subtraction;

  m = operation (7, 5, addition);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}
8

在示例中,minus是一個指向函式的指標,該函式有兩個int型別的引數。它立即被賦值指向函式subtraction (減法),所有這些都在一行中完成。

1
int (* minus)(int,int) = subtraction;
Index
目錄