关于本文
我相信竞争能带来进步。
除了我的文章和
Moschops'的文章之外,还有
三篇 其他关于指针及其与数组关系的
文章。
然后,《
文档》中有专门介绍指针的部分。
因此,我将尽量让本文简短且直击要点。
(本文假设您了解 C++ 编程基础。)
指针事实
指针是一个变量。它存储一个数字。这个数字代表一个内存地址。
因此,我们说它指向某个数据。
指针可以有类型(例如
int
、
char
)或者可以是
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
|
NULL
和 nullptr
初始化变量的规则也适用于指针。
惯例是使用
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 = π // 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
,这是一个优秀的替代品。