发布
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,这是一个优秀的替代品。