天天看點

C++ 進階資料類型(三)—— 指針

我們已經明白變量其實是可以由辨別來存取的記憶體單元。但這些變量實際上是存儲在記憶體中具體的位置上的。對我們的程式來說,計算機記憶體隻是一串連續的單位元組單元(1byte cell),即最小資料機關,每一個單元有一個唯一位址。

計算機記憶體就好像城市中的街道。在一條街上,所有的房子被順序編号,每所房子有唯一編号。是以如果我們說芝麻街27号,我們很容易找到它,因為隻有一所房子會是這個編号,而且我們知道它會在26号和28号之間。

同房屋按街道位址編号一樣,作業系統(operating system)也按照唯一順序編号來組織記憶體。是以,當我們說記憶體中的位置1776,我們知道記憶體中隻有一個位置是這個位址,而且它在位址1775和1777之間。

位址操作符/去引操作符 Address/dereference operator (&)

當我們聲明一個變量的同時,它必須被存儲到記憶體中一個具體的單元中。通常我們并不會指定變量被存儲到哪個具體的單元中—幸虧這通常是由編譯器和作業系統自動完成的,但一旦作業系統指定了一個位址,有些時候我們可能會想知道變量被存儲在哪裡了。

這可以通過在變量辨別前面加與符号ampersand sign (&)來實作,它表示"...的位址" ("address of"),是以稱為位址操作符(adress operator),又稱去引操作符(dereference operator)。例如:

ted = &andy;

将變量andy的位址賦給變量ted,因為當在變量名稱andy 前面加ampersand (&) 符号,我們指的将不再是該變量的内容,而是它在記憶體中的位址。

假設andy 被放在了記憶體中位址1776的單元中,然後我們有下列代碼:

andy = 25;

fred = andy;

ted = &andy;

其結果顯示在下面的圖檔中:

C++ 進階資料類型(三)—— 指針

我們将變量andy 的值賦給變量fred,這與以前我們看到很多例子都相同,但對于ted,我們把作業系統存儲andy的記憶體位址賦給它,我們想像該位址為1776 (它可以是任何位址,這裡隻是一個假設的位址),原因是當給ted 指派的時候,我們在andy 前面加了ampersand (&) 符号。

存儲其它變量位址的變量(如上面例子中的ted ),我們稱之為指針(pointer)。在C++ 中,指針pointers 有其特定的優點,是以經常被使用。在後面我們将會看到這種變量如何被聲明。

引用操作符Reference operator (*)

使用指針的時候,我們可以通過在指針辨別的前面加星号asterisk (*)來存儲該指針指向的變量所存儲的數值,它可以被翻譯為“所指向的數值”("value pointed by")。是以,仍用前面例子中的數值,如果我們寫: beth = *ted; (我們可以讀作:"beth 等與ted所指向的數值") beth 将會獲得數值25,因為ted 是1776,而1776 所指向的數值為25。

C++ 進階資料類型(三)—— 指針

你必須清楚的區分ted 存儲的是1776,但*ted (前面加asterisk * ) 指的是位址1776中存儲的數值,即25。注意加或不加星号*的不同(下面代碼中注釋顯示了如何讀這兩個不同的表達式):

beth = ted;  // beth 等于 ted ( 1776 )

beth = *ted;  // beth 等于 ted 所指向的數值 ( 25 )

位址或反引用操作符Operator of address or dereference (&)

它被用作一個變量字首,可以被翻譯為“…的位址”("address of"),是以:&variable1 可以被讀作 variable1的位址("address of variable1" )。

引用操作符Operator of reference (*)

它表示要取的是表達式所表示的位址指向的内容。它可以被翻譯為“…指向的數值” ("value pointed by")。

* mypointer 可以被讀作 "mypointer指向的數值"。

繼續使用上面開始的例子,看下面的代碼:

andy = 25;

ted = &andy;

現在你應該可以清楚的看到以下等式全部成立:

andy == 25

&andy == 1776

ted == 1776

*ted == 25

第一個表達式很容易了解,因為我們有指派語句andy=25;。第二個表達式使用了位址(或反引用)操作符(&) 來傳回變量andy的位址,即 1776。第三個表達式很明顯成立,因為第二個表達式為真,而我們給ted指派的語句為ted = &andy;。第四個表達式使用了引用操作符 (*),相當于ted指向的位址中存儲的數值,即25。

由此你也可以推斷出,隻要ted 所指向的位址中存儲的數值不變,以下表達式也為真:

*ted == andy

聲明指針型變量Declaring variables of type pointer

由于指針可以直接引用它所指向的數值,是以有必要在聲明指針的時候指明它所指向的資料類型。指向一個整型int或浮點型float資料的指針與指向一個字元型char資料的指針并不相同。

是以,聲明指針的格式如下:

type * pointer_name;

這裡,type 是指針所指向的資料的類型,而不是指針自己的類型。例如:

int * number;

char * character;

float * greatnumber;

它們是3個指針的聲明,每一個指針指向一種不同資料類型。這三個指針本身其實在記憶體中占用同樣大小的記憶體空間(指針的大小取決于不同的作業系統),但它們所指向的資料是不同的類型,并占用不同大小的記憶體空間,一個是整型int,一個是字元型char ,還有一個是浮點型float。

需要強調的一點是,在指針聲明時的星号asterisk (*) 僅表示這裡聲明的是一個指針,不要把它和前面我們用過的引用操作符混淆,雖然那也是寫成一個星号 (*)。它們隻是用同一符号表示的兩個不同任務。

// my first pointer

#include <iostream.h>

int main ( ) {

int value1 = 5, value2 = 15;

int * mypointer;

mypointer = &value1;

*mypointer = 10;

mypointer = &value2;

*mypointer = 20;

cout << "value1==" << value1 << "/ value2==" << value2;

return 0;

}

value1==10 / value2==20

注意變量value1 和 value2 是怎樣間接的被改變數值的。首先我們使用 ampersand sign (&) 将value1的位址賦給mypointer 。然後我們将10 賦給 mypointer所指向的數值,它其實指向value1的位址,是以,我們間接的修改了value1的數值。

為了讓你了解在同一個程式中一個指針可以被用作不同的數值,我們在這個程式中用value2 和同一個指針重複了上面的過程。

下面是一個更複雜一些的例子:

// more pointers

#include <iostream.h>

int main () {

int value1 = 5, value2 = 15;

int *p1, *p2;

p1 = &value1; // p1 = address of value1

p2 = &value2; // p2 = address of value2

*p1 = 10; // value pointed by p1 = 10

*p2 = *p1; // value pointed by p2 = value pointed by p1

p1 = p2; // p1 = p2 (value of pointer copied)

*p1 = 20; // value pointed by p1 = 20

cout << "value1==" << value1 << "/ value2==" << value2;

return 0;

}

value1==10 / value2==20

上面每一行都有注釋說明代碼的意思:ampersand (&) 為"address of",asterisk (*) 為 "value pointed by"。注意有些包含p1 和p2 的表達式不帶星号。加不加星号的含義十分不同:星号(*)後面跟指針名稱表示指針所指向的地方,而指針名稱不加星号(*)表示指針本身的數值,即它所指向的地方的位址。

另一個需要注意的地方是這一行:

int *p1, *p2;

聲明了上例用到的兩個指針,每個帶一個星号(*),因為是這一行定義的所有指針都是整型int (而不是 int*)。原因是引用操作符(*) 的優先級順序與類型聲明的相同,是以,由于它們都是向右結合的操作,星号被優先計算。我們在 section 1.3: Operators 中已經讨論過這些。注意在聲明每一個指針的時候前面加上星号asterisk (*)。

指針和數組Pointers and arrays

數組的概念與指針的概念聯系非常解密。其實數組的辨別相當于它的第一個元素的位址,就像一個指針相當于它所指向的第一個元素的位址,是以其實它們是同一個東西。例如,假設我們有以下聲明:

int numbers [20];

int * p;

下面的指派為合法的:

p = numbers;

這裡指針p 和numbers 是等價的,它們有相同的屬性,唯一的不同是我們可以給指針p賦其它的數值,而numbers 總是指向被定義的20個整數組中的第一個。是以,p隻是一個普通的指針變量,而與之不同,numbers 是一個指針常量(constant pointer),數組名的确是一個指針常量。是以雖然前面的指派表達式是合法的,但下面的不是:

numbers = p;

因為numbers 是一個數組(指針常量),常量辨別不可以被賦其它數值。

由于變量的特性,以下例子中所有包含指針的表達式都是合法的:

// more pointers

#include <iostream.h>

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,

在數組一章中我們使用了括号[]來指明我們要引用的數組元素的索引(index)。中括号[]也叫位移(offset)操作符,它相當于在指針中的位址上加上括号中的數字。例如,下面兩個表達式互相等價:

a[5] = 0;  // a [offset of 5] = 0

*(a+5) = 0;  // pointed by (a+5) = 0

不管a 是一個指針還是一個數組名, 這兩個表達式都是合法的。

指針初始化Pointer initialization

當聲明一個指針的時候我們可能需要同時指定它們指向哪個變量,

int number;

int *tommy = &number;

這相當于:

int number;

int *tommy;

tommy = &number;

當給一個指針指派的時候,我們總是賦給它一個位址值,而不是它所指向資料的值。你必須考慮到在聲明一個指針的時候,星号 (*) 隻是用來指明它是指針,而從不表示引用操作符reference operator (*)。記住,它們是兩種不同操作,雖然它們寫成同樣的符号。是以,我們要注意不要将以上的代碼與下面的代碼混淆:

int number;

int *tommy;

*tommy = &number;

這些代碼也沒有什麼實際意義。

在定義數組指針的時候,編譯器允許我們在聲明變量指針的同時對數組進行初始化,初始化的内容需要是常量,例如:

char * terry = "hello";

在這個例子中,記憶體中預留了存儲"hello" 的空間,并且terry被賦予了向這個記憶體塊的第一個字元(對應’h’)的指針。假設"hello"存儲在位址1702,下圖顯示了上面的定義在記憶體中狀态:

C++ 進階資料類型(三)—— 指針

這裡需要強調,terry 存儲的是數值1702 ,而不是'h' 或 "hello",雖然1702 指向這些字元。

指針terry 指向一個字元串,可以被當作數組一樣使用(數組隻是一個常量指針)。例如,如果我們的心情變了,而想把terry指向的内容中的字元'o' 變為符号'!' ,我們可以用以下兩種方式的任何一種來實作:

terry[4] = '!';

*(terry+4) = '!';

記住寫 terry[4] 與*(terry+4)是一樣的,雖然第一種表達方式更常用一些。以上兩個表達式都會實作以下改變:

C++ 進階資料類型(三)—— 指針

指針的數學運算Arithmetic of pointers

對指針進行數學運算與其他整型資料類型進行數學運算稍有不同。首先,對指針隻有加法和減法運算,其它運算在指針世界裡沒有意義。但是指針的加法和減法的具體運算根據它所指向的資料的類型的大小的不同而有所不同。

我們知道不同的資料類型在記憶體中占用的存儲空間是不一樣的。例如,對于整型資料,字元char 占用1 的位元組(1 byte),短整型short 占用2 個位元組,長整型long 占用4個位元組。

假設我們有3個指針:

char *mychar;

short *myshort;

long *mylong;

而且我們知道他們分别指向記憶體位址1000, 2000 和3000。

是以如果我們有以下代碼:

mychar++;

myshort++;

mylong++;

就像你可能想到的,mychar的值将會變為1001。而myshort 的值将會變為2002,mylong的值将會變為3004。原因是當我們給指針加1時,我們實際是讓該指針指向下一個與它被定義的資料類型的相同的元素。是以,它所指向的資料類型的長度位元組數将會被加到指針的數值上。以上過程可以由下圖表示:

C++ 進階資料類型(三)—— 指針

這一點對指針的加法和減法運算都适用。如果我們寫以下代碼,它們與上面例子的作用一抹一樣: mychar = mychar + 1;

myshort = myshort + 1;

mylong = mylong + 1;

這裡需要提醒你的是,遞增 (++) 和遞減 (--) 操作符比引用操作符reference operator (*)有更高的優先級,是以,以下的表達式有可能引起歧義:

*p++;

*p++ = *q++;

第一個表達式等同于*(p++) ,它所作的是增加p (它所指向的位址,而不是它存儲的數值)。

在第二個表達式中,因為兩個遞增操作(++) 都是在整個表達式被計算之後進行而不是在之前,是以*q 的值首先被賦予*p ,然後q 和p 都增加1。它相當于:

*p = *q;

p++;

q++;

像通常一樣,我們建議使用括号()以避免意想不到的結果。

指針的指針Pointers to pointers

C++ 允許使用指向指針的指針。要做到這一點,我們隻需要在每一層引用之前加星号(*)即可:

char a;

char * b;

char ** c;

a = 'z';

b = &a;

c = &b;

假設随機選擇記憶體位址為7230, 8092 和10502,以上例子可以用下圖表示:

C++ 進階資料類型(三)—— 指針

(方框内為變量的内容;方框下面為記憶體位址)

這個例子中新的元素是變量c,關于它我們可以從3個方面來讨論,每一個方面對應了不同的數值:

c 是一個(char **)類型的變量,它的值是8092

*c 是一個(char*)類型的變量,它的值是7230

**c 是一個(char)類型的變量,它的值是'z'

空指針void pointers

指針void 是一種特殊類型的指針。void 指針可以指向任意類型的資料,可以是整數,浮點數甚至字元串。唯一個限制是被指向的數值不可以被直接引用(不可以對它們使用引用星号*),因為它的長度是不定的,是以,必須使用類型轉換操作或指派操作來把void 指針指向一個具體的資料類型。

它的應用之一是被用來給函數傳遞通用參數:

// integer increaser

#include <iostream.h>

void increase (void* data, int type) {

switch (type) {

case sizeof(char) : (*((char*)data))++; break;

case sizeof(short): (*((short*)data))++; break;

case sizeof(long) : (*((long*)data))++; break;

}

}

int main () {

char a = 5;

short b = 9;

long c = 12;

increase (&a,sizeof(a));

increase (&b,sizeof(b));

increase (&c,sizeof(c));

cout << (int) a << ", " << b << ", " << c;

return 0;

}

6, 10, 13

sizeof 是C++的一個操作符,用來傳回其參數的長度位元組數常量。例如,sizeof(char) 傳回 1,因為 char 類型是1位元組長資料類型。

函數指針Pointers to functions

C++ 允許對指向函數的指針進行操作。它最大的作用是把一個函數作為參數傳遞給另外一個函數。聲明一個函數指針像聲明一個函數原型一樣,除了函數的名字需要被括在括号内并在前面加星号asterisk (*)。例如:

// pointer to functions

#include <iostream.h>

int addition (int a, int b) { 

    return (a+b); 

}

int subtraction (int a, int b) { 

    return (a-b); 

}

int (*minus)(int,int) = subtraction;

int operation (int x, int y, int (*functocall)(int,int)) {

    int g;

    g = (*functocall)(x,y);

    return (g);

}

int main () {

    int m,n;

    m = operation (7, 5, addition);

    n = operation (20, m, minus);

    cout <<n;

    return 0;

}

8

在這個例子裡, minus 是一個全局指針,指向一個有兩個整型參數的函數,它被指派指向函數subtraction,所有這些由一行代碼實作:

int (* minus)(int,int) = subtraction;

這裡似乎解釋的不太清楚,有問題問為什麼(int int)隻有類型,沒有參數,就再多說兩句。 這裡 int (*minus)(int int)實際是在定義一個指針變量,這個指針的名字叫做minus,這個指針的類型是指向一個函數,函數的類型是有兩個整型參數并傳回一個整型值。

整句話“int (*minus)(int,int) = subtraction;”是定義了這樣一個指針并把函數subtraction的值賦給它,也就是說有了這個定義後minus就代表了函數subtraction。是以括号中的兩個int int實際隻是一種變量類型的聲明,也就是說是一種形式參數而不是實際參數。

繼續閱讀