指標

在前面的章節中,變數被解釋為計算機記憶體中的位置,可以透過其識別符號(即名稱)進行訪問。這樣,程式就不需要關心資料在記憶體中的物理地址;它只需要在需要引用變數時使用識別符號。

對於C++程式而言,計算機的記憶體就像一系列記憶體單元,每個單元大小為一位元組,並具有唯一的地址。這些單位元組記憶體單元的排列方式使得大於一位元組的資料表示可以佔據具有連續地址的記憶體單元。

這樣,每個單元都可以透過其唯一的地址輕鬆定位在記憶體中。例如,地址為1776的記憶體單元緊跟在地址為1775的單元之後,並位於地址為1777的單元之前,它比地址776晚一千個單元,比地址2776早一千個單元。

當宣告一個變數時,儲存其值所需的記憶體會被分配到記憶體中的一個特定位置(其記憶體地址)。通常,C++程式不會主動決定其變數儲存的確切記憶體地址。幸運的是,這項任務留給了程式執行的環境——通常是作業系統,它會在執行時決定特定的記憶體位置。然而,程式在執行時能夠獲取變數的地址以訪問相對於其位置的某些資料單元可能會很有用。

取地址運算子(&)

可以透過在變數名前加上一個名為取地址運算子的“&”符號來獲取變數的地址。例如:

1
foo = &myvar;

這將把變數myvar的地址分配給foo;透過在變數myvar名前加上取地址運算子&),我們不再是將變數本身的內容分配給foo,而是其地址。

變數在記憶體中的實際地址在執行時是無法得知的,但為了幫助闡明一些概念,我們假設在執行時myvar被放置在記憶體地址1776

在這種情況下,請考慮以下程式碼片段:

1
2
3
myvar = 25;
foo = &myvar;
bar = myvar;

下面這張圖顯示了執行此操作後每個變數中包含的值:



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

第二個語句將myvar的地址(我們假設為1776)賦給了foo

最後,第三個語句將myvar中包含的值賦給了bar。這是一個標準的賦值操作,正如在前面的章節中已經多次完成的那樣。

第二個和第三個語句之間的主要區別在於取地址運算子&)的出現。

在C++中,儲存另一個變數地址的變數被稱為指標。指標是語言中一個非常強大的特性,在底層程式設計中有許多用途。稍後,我們將學習如何宣告和使用指標。

解引用運算子(*)

如前所述,儲存另一個變數地址的變數被稱為指標。指標“指向”它們所儲存地址的變數。

指標的一個有趣特性是它們可以用來直接訪問它們所指向的變數。這是透過在指標名稱前加上解引用運算子*)來實現的。該運算子本身可以讀作“指向的值”。

因此,根據上一個示例中的值,以下語句:

1
baz = *foo;

可以讀作:“baz等於foo指向的值”,並且該語句實際上會將值25賦給baz,因為foo1776,而1776指向的值(根據上面的示例)是25


明確區分foo指的是值1776,而*foo(在識別符號前有一個星號*)指的是儲存在地址1776的值(在本例中為25)非常重要。請注意包含或不包含解引用運算子的區別(我已經添加了關於這兩個表示式如何讀取的解釋性註釋)。

1
2
baz = foo;   // baz equal to foo (1776)
baz = *foo;  // baz equal to value pointed to by foo (25)  

取地址和解引用運算子因此是互補的。
  • &取地址運算子,可以簡單地讀作“地址”。
  • *解引用運算子,可以讀作“指向的值”。

因此,它們的意思是相反的:用&獲得的地址可以用*解引用。

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

1
2
myvar = 25;
foo = &myvar;

在這兩條語句之後,所有以下表達式的結果都將為真:

1
2
3
4
myvar == 25
&myvar == 1776
foo == 1776
*foo ==

第一個表示式很清楚,考慮到對myvar執行的賦值操作是myvar=25。第二個表示式使用了取地址運算子(&),它返回myvar的地址,我們假設其值為1776。第三個表示式有些顯而易見,因為第二個表示式為真,並且對foo執行的賦值操作是foo=&myvar。第四個表示式使用了解引用運算子*),可以讀作“指向的值”,而foo指向的值確實是25

因此,在這一切之後,您還可以推斷,只要foo指向的地址保持不變,以下表達式也將為真:

1
*foo ==

宣告指標

由於指標能夠直接引用它所指向的值,指標指向char時與指向intfloat時具有不同的屬性。一旦解引用,就需要知道型別。為此,指標的宣告需要包含指標將指向的資料型別。

指標的宣告遵循以下語法:

type * name;

其中type是該指標指向的資料型別。此型別不是指標本身的型別,而是指標指向的資料的型別。例如:

1
2
3
int * number;
char * character;
double * decimals;

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

請注意,宣告指標時使用的星號(*)僅表示它是一個指標(它是其型別複合說明符的一部分),不應與前面看到的解引用運算子混淆,後者也用星號(*)表示。它們只是用同一個符號表示的兩個不同的東西。

讓我們看一個關於指標的例子:

// 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 << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}
firstvalue is 10
secondvalue is 20

請注意,即使在程式中沒有直接為firstvaluesecondvalue設定任何值,它們最終都透過使用mypointer間接設定了值。它是這樣發生的:

首先,使用取地址運算子(&)將firstvalue的地址賦給mypointer。然後,將mypointer指向的值賦為10。由於此刻mypointer指向firstvalue的記憶體位置,這實際上會修改firstvalue的值。

為了證明指標在程式生命週期中可以指向不同的變數,該示例使用相同的指標mypointer重複了對secondvalue的過程。

這是一個稍微更詳細的例子:

// 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 to by p1 = 10
  *p2 = *p1;         // value pointed to by p2 = value pointed to by p1
  p1 = p2;           // p1 = p2 (value of pointer is copied)
  *p1 = 20;          // value pointed to by p1 = 20
  
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}
firstvalue is 10
secondvalue is 20

每個賦值操作都包含一個關於如何讀取該行的註釋:即用“地址”替換“&”,用“指向的值”替換星號(*)。

請注意,存在帶有指標p1p2的表示式,其中包含和不包含解引用運算子*)。使用解引用運算子(*)的表示式的含義與不使用該運算子的表示式截然不同。當該運算子出現在指標名稱之前時,表示式引用的是所指向的值;而當指標名稱沒有該運算子時,它引用的是指標本身的值(即,指標指向的地址)。

另外可能引起您注意的是這一行:

1
int * p1, * p2;

這聲明瞭前面示例中使用的兩個指標。但請注意,每個指標都有一個星號(*),以便兩者都能擁有型別int*(指向int的指標)。這是必需的,因為優先順序規則。請注意,如果程式碼是:

1
int * p1, p2;

p1確實是int*型別,但p2int型別。在這種情況下,空格完全無關緊要。但無論如何,對於對宣告多個指標感興趣的大多數指標使用者而言,記住每個語句有一個星號就足夠了。甚至更好:為每個變數使用不同的語句。

指標和陣列

陣列的概念與指標的概念相關。事實上,陣列的工作方式非常類似於指向其第一個元素的指標,並且實際上,陣列總是可以隱式轉換為適當型別的指標。例如,考慮這兩個宣告:

1
2
int myarray [20];
int * mypointer;

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

1
mypointer = myarray;

之後,mypointermyarray將是等效的,並且將具有非常相似的屬性。主要區別在於mypointer可以被賦以不同的地址,而myarray永遠不能被賦以任何值,並且將始終代表相同大小的20個int型別元素的塊。因此,以下賦值無效:

1
myarray = mypointer;

讓我們看一個混合了陣列和指標的例子:

// 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 to by (a+5) = 0  

這兩個表示式是等效且有效的,不僅當a是指標時,當a是陣列時也是如此。請記住,如果是一個數組,它的名稱可以用作指向其第一個元素的指標。

指標初始化

指標可以在定義時初始化為指向特定位置:

1
2
int myvar;
int * myptr = &myvar;

此程式碼執行後的變數狀態與以下程式碼執行後的狀態相同:

1
2
3
int myvar;
int * myptr;
myptr = &myvar;

當初始化指標時,初始化的是它們指向的地址(即myptr),而不是所指向的值(即*myptr)。因此,上面的程式碼不應與以下程式碼混淆:

1
2
3
int myvar;
int * myptr;
*myptr = &myvar;

無論如何,這都沒有多大意義(並且不是有效的程式碼)。

指標宣告中的星號(*)(第2行)僅表示它是一個指標(它是其型別複合說明符的一部分),不應與解引用運算子(第3行)混淆。這兩者碰巧都使用相同的符號:*。一如既往,空格無關緊要,並且從不改變表示式的含義。

指標可以初始化為指向變數的地址(如上面的情況),或者指向另一個指標(或陣列)的值:

1
2
3
int myvar;
int *foo = &myvar;
int *bar = foo;

指標算術

對指標進行算術運算與對普通整數型別進行運算略有不同。首先,只允許加法和減法運算;其他運算在指標的世界中沒有意義。但是,加法和減法對指標的行為略有不同,具體取決於它們指向的資料型別的大小。

當介紹基本資料型別時,我們看到型別具有不同的尺寸。例如:char的大小始終為1位元組,short通常比char大,而intlong更大;這些的確切大小取決於系統。例如,讓我們假設在一個給定的系統中,char佔用1位元組,short佔用2位元組,long佔用4位元組。

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

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

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

因此,如果我們寫:

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

不出所料,mychar將包含值1001。但並非顯而易見的是,myshort將包含值2002,而mylong將包含3004,儘管它們都只被遞增了一次。原因是,當向指標加一時,指標被指向相同型別的下一個元素,因此,它指向的型別的大小(以位元組為單位)會被新增到指標中。


這既適用於向指標加任何數字,也適用於從指標減去任何數字。如果我們寫:

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

關於遞增(++)和遞減(--)運算子,它們都可以作為表示式的字首或字尾使用,行為略有不同:作為字首時,遞增發生在表示式求值之前;作為字尾時,遞增發生在表示式求值之後。這也適用於遞增和遞減指標的表示式,這些表示式可以成為更復雜的表示式的一部分,而這些表示式還包括解引用運算子(*)。回想一下運算子優先順序規則,我們可以記得字尾運算子(如遞增和遞減)的優先順序高於字首運算子(如解引用運算子(*))。因此,表示式:

1
*p++

等同於*(p++)。它的作用是增加p的值(因此它現在指向下一個元素),但由於++被用作字尾而不是字首,因此整個表示式的求值結果是先前由指標指向的值(在遞增之前它指向的地址)。

本質上,這是解引用運算子與遞增運算子的字首和字尾版本的所有四種可能組合(同樣適用於遞減運算子):

1
2
3
4
*p++   // same as *(p++): increment pointer, and dereference unincremented address
*++p   // same as *(++p): increment pointer, and dereference incremented address
++*p   // same as ++(*p): dereference pointer, and increment the value it points to
(*p)++ // dereference pointer, and post-increment the value it points to 

一個典型的(但並非那麼簡單的)涉及這些運算子的語句是:

1
*p++ = *q++;

由於++的優先順序高於*,因此pq都會遞增,但由於兩個遞增運算子(++)都用作字尾而不是字首,因此賦給*p的值是*q,然後在pq都遞增之前。然後兩者都遞增。這大致相當於:

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

一如既往,括號透過增加表示式的可讀性來減少混淆。

指標和const

指標可以用於透過地址訪問變數,並且這種訪問可能包括修改所指向的值。但也可以宣告指標,這些指標可以訪問所指向的值進行讀取,但不能進行修改。為此,只需將指標指向的型別限定為const即可。例如:

1
2
3
4
5
int x;
int y = 10;
const int * p = &y;
x = *p;          // ok: reading p
*p = x;          // error: modifying p, which is const-qualified 

在這裡,p指向一個變數,但以const限定的方式指向它,這意味著它可以讀取所指向的值,但不能修改它。另請注意,表示式&y的型別是int*,但這被賦給了一個型別為const int*的指標。這是允許的:指向非const的指標可以隱式轉換為指向const的指標。但反過來則不行!作為一種安全功能,指向const的指標不能隱式轉換為指向非const的指標。

指向const元素的指標的一個用途是作為函式引數:一個接受指向非const的指標作為引數的函式可以修改作為引數傳遞的值,而一個接受指向const的指標作為引數的函式則不能。

// pointers as arguments:
#include <iostream>
using namespace std;

void increment_all (int* start, int* stop)
{
  int * current = start;
  while (current != stop) {
    ++(*current);  // increment value pointed
    ++current;     // increment pointer
  }
}

void print_all (const int* start, const int* stop)
{
  const int * current = start;
  while (current != stop) {
    cout << *current << '\n';
    ++current;     // increment pointer
  }
}

int main ()
{
  int numbers[] = {10,20,30};
  increment_all (numbers,numbers+3);
  print_all (numbers,numbers+3);
  return 0;
}
11
21
31

請注意,print_all使用指向常量元素的指標。這些指標指向它們無法修改的常量內容,但它們本身不是常量:也就是說,指標仍然可以遞增或被賦以不同的地址,儘管它們不能修改它們所指向的內容。

在這裡,我們將const的第二維新增到指標:指標本身也可以是const的。這是透過將const附加到指向的型別(在星號之後)來指定的:

1
2
3
4
5
int x;
      int *       p1 = &x;  // non-const pointer to non-const int
const int *       p2 = &x;  // non-const pointer to const int
      int * const p3 = &x;  // const pointer to non-const int
const int * const p4 = &x;  // const pointer to const int 

const與指標的語法確實很棘手,並且識別最適合每種用途的案例通常需要一些經驗。無論如何,儘早正確掌握const與指標(和引用)的結合非常重要,但如果您是第一次接觸const和指標的混合,則不必過於擔心掌握所有內容。後續章節將出現更多用例。

為了增加const與指標語法的混淆度,const限定符可以放在指向型別的前面或後面,具有完全相同的含義:

1
2
const int * p2a = &x;  //      non-const pointer to const int
int const * p2b = &x;  // also non-const pointer to const int 

與星號周圍的空格一樣,此情況下的const順序只是風格問題。本章使用字首const,因為出於歷史原因,這似乎更為普遍,但兩者完全等效。各自風格的優點仍在網際網路上激烈爭論。

指標和字串字面量

如前所述,字串字面量是包含以 null 結尾的字元序列的陣列。在前面的章節中,字串字面量已被用於直接插入cout,初始化字串以及初始化字元陣列。

但它們也可以被直接訪問。字串字面量是具有正確陣列型別的陣列,可以包含所有字元加上終止的 null 字元,每個元素都是const char型別(作為字面量,它們永遠不能被修改)。例如:

1
const char * foo = "hello";

這聲明瞭一個具有"hello"字面量表示的陣列,然後將指向其第一個元素的指標賦給了foo。如果我們設想"hello"儲存在從地址1702開始的記憶體位置,我們可以將前面的宣告表示為:


請注意,此處foo是指標,其值為1702,而不是'h'"hello",儘管1702確實是這兩者的地址。

指標foo指向一個字元序列。由於指標和陣列在表示式中的行為基本相同,因此foo可以用來訪問字元,方式與以 null 結尾的字元序列的陣列相同。例如:

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

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

指向指標的指標

C++允許使用指向指標的指標,這些指標又指向資料(甚至是指向其他指標)。語法僅需要在宣告指標時為每個間接級別新增一個星號(*):

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

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


其中每個單元中都表示了變數的值,而它們各自在記憶體中的地址則由其下方的數值表示。

此示例中的新內容是變數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 << '\n';
  return 0;
}
y, 1603

sizeof是C++語言整合的一個運算子,它返回其引數的大小(以位元組為單位)。對於非動態資料型別,此值是常量。因此,例如,sizeof(char)為1,因為char的大小始終為1位元組。

無效指標和空指標

原則上,指標旨在指向有效地址,例如變數的地址或陣列中元素的地址。但指標實際上可以指向任何地址,包括不引用任何有效元素的地址。這方面的典型例子是未初始化的指標和指向陣列不存在元素的指標:

1
2
3
4
int * p;               // uninitialized pointer (local variable)

int myarray[10];
int * q = myarray+20;  // element out of bounds 

pq均未指向已知包含值的地址,但以上任何語句都不會導致錯誤。在C++中,指標可以取任何地址值,無論該地址處是否實際存在內容。可能導致錯誤的是解引用此類指標(即實際訪問它們指向的值)。訪問此類指標會導致未定義的行為,從執行時錯誤到訪問隨機值。

但是,有時指標確實需要顯式地指向“無”,而不僅僅是無效地址。對於這種情況,任何指標型別都可以採用一個特殊的值:空指標值。在C++中,此值可以透過兩種方式表示:整數值零,或關鍵字nullptr

1
2
int * p = 0;
int * q = nullptr;

在這裡,pq都是空指標,這意味著它們顯式地指向“無”,並且它們都相等比較:所有空指標都與其他空指標相等。在舊程式碼中,經常可以看到定義的常量NULL被用來表示空指標值:

1
int * r = NULL;

NULL在標準庫的幾個標頭檔案中定義,並被定義為某個空指標常量值(如0nullptr)的別名。

不要將空指標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
目錄