釋出
2011年11月14日 (最後更新: 2011年11月19日)

指標工藝

評分: 4.2/5 (120票)
*****

關於本文

我相信競爭能帶來進步。
除了我的文章和Moschops'的文章之外,還有三篇 其他關於指標及其與陣列關係的文章
然後,《文件》中有專門介紹指標的部分。
因此,我將盡量讓本文簡短且直擊要點。
(本文假設您瞭解 C++ 程式設計基礎。)

指標事實

指標是一個變數。它儲存一個數字。這個數字代表一個記憶體地址。
因此,我們說它指向某個資料。
指標可以有型別(例如 intchar)或者可以是 void
型別將提示您希望如何解釋被指向的資料。
如果您使用 void,您可能需要在以後指定一個型別。

宣告指標

宣告指標就像宣告任何變數一樣,只是在型別和名稱之間新增一個星號(*)。

示例
1
2
3
4
5
6
void * function(int *i)
{
    void *v;     // we don't know what type of data v will point to
    v = i + 500; // pointer arithmetic
    return v;    // return the resulting memory address
}


上面的 function() 以指標作為引數。
i 的值是它包含的記憶體地址。
進行指標算術運算後,我們將得到一個新的記憶體地址。
我們使用 void 作為型別,因為我們不確定 v 指向的資料應該如何處理。

指標算術

指標算術是指指標與整數之間的加法或減法。
指標的值是它所持有的記憶體地址。它以位元組為單位表示。
大多數型別在記憶體中佔用不止一個位元組。(例如,float 使用四個位元組。)
該整數表示我們將透過指標型別的元素數量來移動地址。
最後,地址將按儲存該數量元素所需的位元組數進行偏移。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float *pf = reinterpret_cast<float *> (100);
// force pf to contain the value 100 (0x64 in hexadecimal)
// notice that (sizeof (float) == 4) bytes

pf += 1; // shift pf forward by one float
// pf is now 104 (0x68)
pf -= 2; // shift pf backward by two floats
// pf is now 96 (0x60)

void *pv = reinterpret_cast<void *> (100); // pv is 100 (0x64)
// notice that (sizeof (void) == 1) byte

pv += 1; // pv is now 101 (0x65)
pv -= 2; // pv is now 99 (0x63)

// caution, you should never assign a custom address to a pointer 


NULLnullptr

初始化變數的規則也適用於指標。
慣例是使用 NULL(或 C++11 中的 nullptr)為指標提供一箇中性值。

示例
1
2
3
int *i1;        // caution, i1 has a junk value
int *i2 = NULL; // we mark i2 as unused
i1 = NULL;      // same for i1 


NULL 最常見的值是 0
設計良好的函式在使用給定指標之前,應該檢查它是否為 NULL
在 C++ 的最新標準(稱為 C++11)中,nullptr 取代了 NULL

引用事實

指標是 C 語言繼承的概念,而引用是 C++ 引入的。
引用可以描述為同一型別現有變數的別名。
引用不包含您可以更改的記憶體地址。
引用不能被重新別名到另一個變數。

宣告引用

宣告引用就像宣告指標一樣,但使用 ampersand(&)而不是星號(*)。

示例
1
2
3
4
int a;       // regular variable a
int &ra = a; // reference, must be initialized at declaration
ra = -1;     // now a is -1, too
a = 55;      // now ra is 55, too 


引用有什麼用?

它可以作為更好的指標。引用不像指標那樣容易失效。
引用的典型用法是作為函式引數中指標的更安全替代品。

示例
1
2
3
4
5
6
void die_string_die(std::string &s)
{
    s.clear();
}
// notice that the real string is not copied as a local variable,
// so when we change s inside our function, the real string changes as well 


使用引用是誘人的,因為不必複製可以節省記憶體和時間。
因此,為了防止對原始變數的任何意外更改,程式設計師會將引用宣告為 const

老派 C 程式設計師也會對指標做同樣的事情,但他們仍然需要檢查指標是否為 NULL
即使它不是,他們仍然無法保證它有效。

示例
1
2
3
4
5
6
7
8
void safe(const std::string &s) {}

void still_unsafe(const std::string *s)
{
    if (s == NULL); // we surely can't use s now

    else; // but what if it's still invalid?
}


解引用(*)和引用(&)運算子

我寫前面這些部分的原因是,C 和 C++ 都選擇了不加思考地重複使用星號(*)和 ampersand(&)作為運算子。
因此,在繼續討論操作之前,我想先闡明它們在宣告中的作用。

解引用運算子(*)用於指標,以操作它們所包含記憶體地址中的資料。
引用運算子(&)用於普通變數,以獲取它們的記憶體地址。
您可以引用一個指標來獲取它自身的記憶體地址。這就是為什麼您會有指向指標的指標。
但是解引用一個普通變數很可能會導致崩潰。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i;       // regular variable i
int *pi;     // pointer to int
int **ppi;   // pointer to pointer to int
int ***pppi; // this is ridiculous, avoid doing things like this

pi = &i;     // apply reference to i, to get i's memory address
ppi = &pi;   // apply reference to pi, to get pi's own memory address
pppi = &ppi; // apply reference to ppi, to get ppi's own memory address

*pi = 5;     // apply dereference to pi, to change the data pointed to by pi

// i has the value 5

**ppi = -17; // apply dereference to ppi twice, i is now -17
***pppi = 9; // apply dereference to pppi three times, i is now 9 


C 陣列事實

陣列可以被描述為具有已知元素數量、相同型別的鏈。
它們有時被描述為“常量指標”,因為使用它們的名稱會返回第一個元素的記憶體地址,但該地址不能更改。
陣列的大小也不能更改。

使用陣列的舊限制是其大小必須在編譯時已知。
在最新的 C 標準(稱為 C99)中,這不再是問題,但 C++ 的設計者決定不在 C++ 中實現 VLA(可變長度陣列)。
VLA 中的“可變”意味著大小是 *一個變數*,而不是大小 *是可變的*。

宣告陣列

可以使用方括號宣告一個簡單的二維陣列。
如果您提供了初始化列表,則可以推斷大小,否則需要自己指定大小。

示例
1
2
3
4
5
6
7
8
9
10
11
int ia1[] = {0, 1, 2, 3};     // size deduced to be 4
int ia2[4] = {5};             // size is 4, contents are {5, 0, 0, 0}
int ia3[40];                  // caution, size is 40 but elements are junk
int ia4[40] = {};             // size is 40, all elements are 0
char ca1[] = "car";           // caution, a '\0' character is added to the end, size is 4
char ca2[] = {'c', 'a', 'r'}; // size is 3
// and so on...

char *pc = ca1; // no need to reference ca1, because it returns a memory address

ia1[1] = -3; // changes second element in ia1 (counting starts from 0) 


動態記憶體分配

在沒有 VLA 的情況下,並且如果出於某種原因我們不想使用 STL 容器,我們可以動態分配記憶體。
在編譯時不知道需要儲存多少元素的情況下,我們會這樣做。

指標的首選用途仍然是指向一個給定的變數。
但它們也可以用來構建包含任意數量元素的鏈。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstddef>
// for size_t (which is an unsigned integral type, like unsigned int)

size_t ne=0; // number of elements

std::cin >> ne; // let the user input desired length

double *pd; // declare a pointer to double

pd = new double[ne]; // new[] allocates memory to store ne doubles,
                     // and returns the starting memory address

// ... pd now acts as a doubles array of size ne ...
// caution, the memory address contained in pd must not be changed

delete[] pd; // delete[] frees the memory new[] allocated
             // caution, omitting this step can cause a memory leak 


函式指標

由於函式也有地址,我們可以擁有一個指向函式的指標。
這是一種實現多型性的原始方法。
下面的示例突出了分派表的用法。

示例
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstdlib>
#include <cstddef>

void good(int i)
{
    std::cout << "I fed " << i << " little kittens today." << std::endl;
}

void neutral(int i)
{
    std::cout << "I drove " << i << " miles yesterday." << std::endl;
}

void evil(int i)
{
    std::cout << "I steal public toilet paper rolls every day." << std::endl;
}

// notice that the "type" of a function is its signature,
// and all the functions above have the same signature: void name(int )

int main()
{
    void (*wondering[])(int ) = {good, neutral, evil};
    // on the left we have an array of pointers to a function of signature: void name(int )
    // on the right we have the initializer list with the three functions

    size_t user_input = 0;

    std::cout << "GOOD\t== 0\nNEUTRAL\t== 1\nEVIL\t== 2\n\nYour choice is:" << std::endl;
    std::cin >> user_input;

    if (user_input > 2)
        user_input = 2; // just in case...

    (*wondering[user_input])(10);
    // notice how we don't call a specific function for the user

    system("PAUSE"); // you may remove this line if on Linux
    return EXIT_SUCCESS;
}


結論

如果您是 C 程式設計師,指標和陣列可以是有用的工具。

但是,由於您很可能是 C++ 程式設計師,所以應該避免進行指標的“技巧性”操作。
使用指標指向現有變數(物件),僅為提高速度和降低記憶體使用量。
請記住,在某些情況下,您可以使用引用而不是指標。

至於 C 陣列,您也應該避免使用它們。C++11 提供了 std::array,這是一個優秀的替代品。