指针

我们已经看到,变量可以被视为内存单元,通过其标识符进行访问。这样,我们就不必关心数据在内存中的物理位置,只需在需要引用变量时使用其标识符即可。

你的计算机内存可以想象成一连串的内存单元,每个单元都是计算机能管理的最小尺寸(一个字节)。这些单字节内存单元按连续方式编号,因此在任何内存块中,每个单元的编号都比前一个单元的编号多一。

这样,每个单元都可以很容易地在内存中定位,因为它有一个唯一的地址,并且所有内存单元都遵循连续的模式。例如,如果我们要查找单元格1776,我们知道它会正好在单元格1775和1777之间,正好在776之后一千个单元,正好在2776之前一千个单元。

引用运算符 (&)

一旦我们声明一个变量,所需内存量就会在内存中的特定位置(其内存地址)为其分配。我们通常不会主动决定变量在我们想象的内存单元面板中的确切位置——幸运的是,这是操作系统在运行时自动执行的任务。然而,在某些情况下,我们可能需要知道变量在运行时存储的地址,以便对其进行相对位置的操作。

定位变量在内存中的地址,我们称之为对该变量的引用。通过在变量标识符前加上一个“与”号(&),即可获得对变量的引用,这被称为引用运算符,字面意思可以翻译为“……的地址”。例如:

1
ted = &andy;

这将把变量ted的地址赋给andy,因为当变量名andy前面加上引用运算符(&)时,我们不再谈论变量本身的内容,而是谈论它的引用(即它在内存中的地址)。

从现在起,我们将假设andy在运行时被放置在内存地址1776中。这个数字(1776)只是我们现在为了帮助阐明本教程中的一些概念而随意假设的,但实际上,我们无法在运行时之前知道变量在内存中地址的真实值。

考虑以下代码片段:

1
2
3
andy = 25;
fred = andy;
ted = &andy;

执行后,每个变量中包含的值如下图所示:


首先,我们将值25赋给andy(一个我们假设其内存地址为1776的变量)。

第二条语句将变量fred的内容(即25)复制到andy。这是一个标准的赋值操作,我们之前已经做过很多次了。

最后,第三条语句复制到ted的不是andy中包含的值,而是它的引用(即它的地址,我们假设是1776)。原因是,在这次第三个赋值操作中,我们在标识符andy前面加上引用运算符(&之前加上了),因此我们不再引用andy的值,而是引用它的引用(它在内存中的地址)。

存储另一个变量引用的变量(例如上例中的ted)就是我们所说的指针。指针是C++语言一个非常强大的特性,在高级编程中有很多用途。稍后,我们将看到这种类型的变量是如何使用和声明的。

解引用运算符 (*)


我们刚刚看到,存储另一个变量引用的变量称为指针。指针被认为“指向”它们存储引用的变量。

使用指针,我们可以直接访问它所指向的变量中存储的值。为此,我们只需在指针的标识符前加上一个星号(*),它充当解引用运算符,字面意思可以翻译为“由……指向的值”。

因此,继续使用上一个示例的值,如果我们写

1
beth = *ted;

(我们可以读作:“beth等于由ted") beth指向的值”),25将取值ted的 C++ 等效文件是1776,因为


你必须清楚地区分表达式ted指的是值1776指向的是一个类型为,而*ted*(标识符前带有星号1776)指的是存储在地址25的值,在本例中是

1
2
beth = ted;   // beth equal to ted ( 1776 )
beth = *ted;  // beth equal to value pointed by ted ( 25 ) 

注意引用和解引用运算符的区别:
  • &是引用运算符,可以读作“...的地址”
  • *是解引用运算符,可以读作“...指向的值”
因此,它们具有互补(或相反)的含义。用&引用的变量可以用*.

前面我们执行了以下两个赋值操作:

1
2
andy = 25;
ted = &andy;

紧随这两个语句之后,所有以下表达式的结果都为真:

1
2
3
4
andy == 25
&andy == 1776
ted == 1776
*ted ==

第一个表达式非常清楚,考虑到对andy执行的赋值操作是andy=25。第二个表达式使用了引用运算符(&),它返回变量andy的地址,我们假设它的值为1776。第三个表达式有些明显,因为第二个表达式为真,并且对ted执行的赋值操作是执行的赋值操作是ted=&andy*。第四个表达式使用了解引用运算符(ted),正如我们刚刚看到的,它可以读作“指向的值”,而指向的值25.

确实是ted。所以,经过这一切,你也可以推断,只要

1
*ted ==

声明指针类型的变量

由于指针能够直接引用它所指向的值,因此在声明时有必要指定指针将指向的数据类型。指向char与指向intfloat.

指针的声明遵循以下格式:

type * name;

,其中类型是指针预期指向的值的数据类型。此类型不是指针本身的类型!而是指针指向的数据的类型。例如:

1
2
3
int * number;
char * character;
float * greatnumber;

这是三个指针的声明。每个都旨在指向不同的数据类型,但实际上它们都是指针,并且它们都将占用相同的内存空间(指针的内存大小取决于代码将在其上运行的平台)。然而,它们所指向的数据不占用相同的内存空间,也不是相同的类型:第一个指向一个int,第二个指向一个char,最后一个指向一个float。因此,尽管这三个示例变量都是指针,占用相同的内存大小,但它们被认为具有不同的类型int*, char*float*,分别取决于它们指向的类型。

我想强调的是,我们在声明指针时使用的星号(*)仅仅表示它是一个指针(它是其类型复合说明符的一部分),不应与我们稍早看到的解引用运算符混淆,解引用运算符也用星号(*)表示。它们只是用同一个符号表示的两个不同事物。

现在看看这段代码:

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

请注意,尽管我们从未直接设置firstvaluesecondvalue的值,但两者最终都通过使用mypointer间接设置了值。这就是过程:

首先,我们将mypointer的引用赋值给firstvalue,使用引用运算符(&)。然后我们将值10赋值给由mypointer指向的内存位置,因为此时它指向firstvalue的内存位置,这实际上修改了firstvalue.

为了证明一个指针在同一个程序中可以取几个不同的值,我用secondvalue和那个相同的指针mypointer.

这是一个更详细的例子:

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

我已将每行代码的阅读方式作为注释包含在内:&(&)表示“……的地址”,*(*)表示“……指向的值”。

请注意,有包含指针的表达式p1p2*,既有带解引用运算符(*)的,也有不带解引用运算符的。使用解引用运算符(

另一件可能引起你注意的是这一行:

1
int * p1, * p2;

这声明了上一个例子中使用的两个指针。但请注意,每个指针都有一个星号(*),以便两者都具有int*类型(指向int).

)。否则,在该行中声明的第二个变量的类型将是int(而不是int*),因为优先级关系。如果我们写成:

1
int * p1, p2;

p1确实会是int*类型,但会是int类型(为此目的,空格完全不重要)。这是由于运算符优先级规则。但无论如何,对于大多数指针用户来说,只需记住每个指针都必须放一个星号就足够了。

指针和数组

数组的概念与指针的概念紧密相连。实际上,数组的标识符等同于其第一个元素的地址,就像指针等同于它所指向的第一个元素的地址一样,因此它们实际上是相同的概念。例如,假设有以下两个声明:

1
2
int numbers [20];
int * p;

以下赋值操作将是有效的:

1
p = numbers;

之后,pnumbersp将是等效的,并具有相同的属性。唯一的区别是我们可以改变指针的值,而int将始终指向其定义的20个p类型元素的第一个。因此,与是一个普通指针不同,

1
numbers = p;

因为是一个数组,所以它作为常量指针操作,我们不能给常量赋值。

由于变量的特性,以下示例中所有包含指针的表达式都是完全有效的:

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

这两个表达式是等效的且有效,无论a是指针还是a是数组。

指针初始化

声明指针时,我们可能希望明确指定它们要指向的变量:

1
2
int number;
int *tommy = &number;

此代码的行为等同于:

1
2
3
int number;
int *tommy;
tommy = &number;

当指针初始化时,我们总是将指针指向的引用值(tommy)赋值,而不是被指向的值(*tommy)。你必须考虑到,在声明指针时,星号(*)仅表示它是一个指针,它不是解引用运算符(尽管两者使用相同的符号:*)。记住,它们是同一个符号的两种不同功能。因此,我们必须注意不要将前面的代码与

1
2
3
int number;
int *tommy;
*tommy = &number;

混淆,这是不正确的,而且如果你仔细想想,在这种情况下也没有太大意义。

与数组的情况一样,编译器允许一种特殊情况,即我们希望在声明指针的同时用常量初始化指针所指向的内容:

1
const char * terry = "hello";

在这种情况下,内存空间被保留以包含"hello",然后将此内存块第一个字符的指针分配给terry。如果我们假设"hello"存储在从地址1702开始的内存位置,我们可以将之前的声明表示为:


重要的是要指出terry包含值1702,而不是'h'也不是"hello",尽管1702确实是这两者的地址。

指针terry指向一串字符,可以像数组一样读取(记住数组就像一个常量指针)。例如,我们可以用以下两种表达式中的任何一种访问数组的第五个元素:

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

两个表达式的值都是'o'(数组的第五个元素)。

指针算术


对指针执行算术运算与对常规整数数据类型执行算术运算略有不同。首先,只允许对它们进行加法和减法运算,其他运算在指针世界中没有意义。但是,加法和减法对指针的行为根据它们指向的数据类型的大小而不同。

当我们了解不同的基本数据类型时,我们看到有些在内存中占用空间多于其他。例如,我们假设在某个特定机器的给定编译器中,char占用1字节,short占用2字节,long占用4字节。

假设我们在此编译器中定义了三个指针:

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

并且我们知道它们分别指向内存位置1000, 20003000

所以如果我们写:

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

mychar,正如你所期望的,将包含值1001。但并不那么明显,myshort将包含值2002,而将包含3004mylong


这适用于对指针进行任何数字的加法和减法。如果我们写:

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

增加(++)和减少(--)运算符的优先级都高于解引用运算符(*),但两者在使用后缀形式时都有特殊的行为(表达式会用增加前的值进行计算)。因此,以下表达式可能会导致混淆:

1
*p++

因为++优先级高于*,因此此表达式等价于*(p++)。因此,它所做的是增加p的值(所以它现在指向下一个元素),但由于++用作后缀,整个表达式被评估为原始引用指向的值(指针在增加之前指向的地址)。

注意与

(*p)++
的区别。在这里,表达式将被评估为p指向的值加一。而p的值(指针本身)不会被修改(被修改的是这个指针所指向的内容)。

如果我们写:

1
*p++ = *q++;

因为++的优先级高于*,那么pq都会增加,但是由于两个增加运算符(++)都是用作后缀而不是前缀,所以赋给*p的 C++ 等效文件是 *qpq的值是它们在两者增加之前的值。然后两者都被增加。这大致相当于:

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

一如既往,我建议你使用括号(()),以避免意想不到的结果并提高代码的可读性。

指向指针的指针

C++ 允许使用指向指针的指针,这些指针又指向数据(甚至指向其他指针)。为此,我们只需要在声明中为每个引用级别添加一个星号(*):

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

假设为7230, 809210502的每个变量随机选择内存位置,这可以表示为:


每个变量的值写在每个单元格内;单元格下方是它们各自在内存中的地址。

这个例子中的新事物是变量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 << endl;
  return 0;
}
y, 1603

sizeof是 C++ 语言中一个内置运算符,它返回其参数的字节大小。对于非动态数据类型,这个值是一个常量。因此,例如,sizeof(char)的 C++ 等效文件是1,因为char类型长度为1字节。

空指针

空指针是任何指针类型的常规指针,它具有一个特殊值,表示它不指向任何有效的引用或内存地址。此值是将整数值零类型转换为任何指针类型的结果。

1
2
int * p;
p = 0;     // p has a null pointer value 

不要将空指针与 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
目录