C++官方参考链接:Pointers - C++ Tutorials (cplusplus.com)
指针
在前面的章节中,变量被解释为计算机内存中的位置,可以通过它们的标识符(它们的名称)访问。这样,程序就不需要关心内存中数据的物理地址;它只是在需要引用变量时使用标识符。
对于C++程序来说,计算机的内存就像一连串的内存单元,每一个字节都有一个唯一的地址。这些单字节内存单元的排序方式允许大于一个字节的数据表示占用具有连续地址的内存单元。
通过这种方式,每个单元可以通过其唯一地址在内存中轻松定位。例如,地址为1776的存储单元总是紧跟在地址为1775的单元之后,在地址为1777的单元之前,并且在776之后正好是1000个单元,在2776之前正好是1000个单元。
当声明一个变量时,存储它的值所需的内存被分配到内存中的一个特定位置(它的内存地址)。通常,C++程序不会主动决定变量存储在哪里的确切内存地址。幸运的是,该任务由运行程序的环境决定——通常是在运行时决定特定内存位置的操作系统。然而,如果程序能够在运行时获得变量的地址,以便访问相对于变量的某个位置的数据单元,那么它可能是有用的。
取地址操作符(&)
变量的地址可以通过在变量名前面加&号来获得,称为取地址操作符。例如:
foo = &myvar;
这将把变量myvar的地址分配给foo;通过在变量myvar的名称前面加上取地址操作符(&),我们不再把变量本身的内容赋值给foo,而是赋值给它的地址。
在运行时之前无法知道内存中变量的实际地址,但是为了帮助阐明一些概念,让我们假设myvar在运行时期间被放置在内存地址1776中。
在这种情况下,考虑以下代码片段:
myvar = 25;
foo = &myvar;
bar = myvar;
执行这个函数后,每个变量所包含的值如下图所示:
首先,我们将值25赋给myvar(一个在内存中的地址我们假设为1776的变量)。
第二条语句将myvar的地址赋给foo,我们假设它是1776。
最后,第三条语句将myvar中包含的值赋值给bar。这是一个标准的赋值操作,在前面的章节中已经做过很多次了。
第二个语句和第三个语句之间的主要区别是取地址操作符(&)的出现。
存储另一个变量地址的变量(如上例中的foo)在C++中称为指针。指针是该语言的一个非常强大的特性,在较低级别的编程中有很多用途。稍后,我们将看到如何声明和使用指针。
解引用操作符(*)
正如刚才所见,存储另一个变量地址的变量称为指针。指针被称为“指向”其存储地址的变量。
指针的一个有趣的属性是,可以使用它们直接访问它们所指向的变量。这是通过在指针名称前面加上解引用操作符(*)来实现的。操作符本身可以理解为“被指向的值”。
因此,在前面例子的值后面加上下面的语句:
baz = *foo;
这可以理解为:“baz等于foo指向的值”,而该语句实际上会将值25赋给baz,因为foo是1776,而1776(按照上面的例子)指向的值将是25。
很重要的一点是要清楚地区分,foo指的是值1776,而*foo(标识符前面有星号*)指的是存储在地址1776中的值,在本例中是25。注意包含或不包含解引用操作符的区别(我已经添加了一个解释性注释,说明如何读取这两个表达式):
baz = foo; // baz equal to foo (1776)
baz = *foo; // baz equal to value pointed to by foo (25)
因此,引用和解引用操作符是互补的:
&是取地址操作符,可以简单地读为“取...地址”
*是解引用操作符,可以读为“被指向的值”
因此,它们具有某种相反的含义:用&获得的地址可以用*解引用。
之前,我们执行了以下两个赋值操作:
myvar = 25;
foo = &myvar;
在这两个语句之后,下面所有表达式的结果都是正确的:
myvar == 25
&myvar == 1776
foo == 1776
*foo == 25
第一个表达式非常清楚,考虑到对myvar执行的赋值操作是myvar=25。第二个使用的是取地址操作符(&),它返回myvar的地址,我们假设它的值为1776。第三个比较明显,因为第二个表达式为正确的,对foo执行的赋值操作为foo=&myvar。第四个表达式使用了解引用操作符(*),它可以被读为“被指向的值”,foo指向的值实际上是25。
所以,在所有这些之后,你也可以推断,只要foo指向的地址保持不变,下面的表达式也将是正确的:
*foo == myvar
声明指针
由于指针可以直接指向它所指向的值,因此指针在指向char类型时与指向int类型或float类型时具有不同的属性。一旦解引用,就需要知道类型。为此,指针的声明需要包含指针要指向的数据类型。
指针的声明遵循以下语法:
type * name;
其中type是指针所指向的数据类型。这种类型不是指针本身的类型,而是指针所指向的数据的类型。例如:
int * number;
char * character;
double * decimals;
这是指针的三种声明。每个指针都指向不同的数据类型,但实际上,它们都是指针,而且它们很可能会占用相同的内存空间(指针的内存大小取决于程序运行的平台)。然而,它们所指向的数据并不占用相同的空间,也不具有相同的类型:第一个指向int型,第二个指向char型,最后一个指向double型。因此,尽管这三个示例变量都是指针,但它们实际上有不同的类型:分别是int*、char*和double*,这取决于它们所指向的类型。
注意,在声明指针时使用星号(*)只表示它是一个指针(它是类型复合说明符的一部分),不应该与前面看到的解引用操作符混淆,但解引用操作符也是用星号(*)写的。它们只是用同一个符号表示的两个不同的东西。
让我们看一个关于指针的例子:
// 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 << '\n';
cout << "secondvalue is " << secondvalue << '\n';
return 0;
}
注意,尽管firstvalue和secondvalue都没有在程序中直接设置任何值,但它们都通过使用mypointer间接设置了一个值。事情是这样发生的:
首先,使用取地址操作符(&)为mypointer分配firstvalue的地址。然后,mypointer所指向的值被赋值为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 to by p1 = 10
*p2 = *p1; // value pointed to by p2 = value pointed to by p1
p1 = p2; // p1 = p2 (value of pointer is copied)
*p1 = 20; // value pointed to by p1 = 20
cout << "firstvalue is " << firstvalue << '\n';
cout << "secondvalue is " << secondvalue << '\n';
return 0;
}
每个赋值操作都包含关于如何读取每一行的注释:例如,将&号替换为“address of”,将*替换为“value pointed to by”。
注意,有些表达式带有指针p1和p2,其中有和没有解引用操作符(*)。使用解引用操作符(*)的表达式的含义与不使用解引用操作符的表达式的含义非常不同。当此操作符位于指针名称之前时,表达式引用被指向的值,而当指针名称出现时不包含此操作符时,表达式引用指针本身的值(即指针所指向的对象的地址)。
另一件可能会引起你注意的事情是这一行:
int * p1, * p2;
这声明了前一个示例中使用的两个指针。但是请注意,每个指针都有一个星号(*),以便两个指针都具有int*类型(指向int的指针)。
由于优先规则,这是必需的。注意,如果把上面的代码代替为:
int * p1, p2;
p1的类型确实是int*,但是p2的类型是int。对于这个目的来说,空白并不重要。但是无论如何,对于大多数想要在每个语句中声明多个指针的指针用户来说,只要记住在每个指针上加一个星号就足够了。或者更好的方法是:对每个变量使用不同的语句。
指针和数组
数组的概念与指针的概念有关。事实上,数组的工作方式非常类似于指向其第一个元素的指针,而且实际上,数组总是可以隐式转换为适当类型的指针。例如,考虑以下两个声明:
int myarray [20];
int * mypointer;
下面的赋值操作是有效的:
mypointer = myarray;
在那之后,mypointer和myarray将是等价的,并且具有非常相似的属性。主要的区别是mypointer可以被分配不同的地址,而myarray永远不能被分配任何东西,并且总是表示由20个int类型元素组成的相同块。因此,下面的赋值将无效:
myarray = mypointer;
让我们看一个混合了数组和指针的例子:
// 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;
}
指针和数组支持相同的操作集,具有相同的含义。主要的区别是指针可以被分配新地址,而数组不能。
在关于数组的章节中,括号([])被解释为指定数组元素的索引。实际上,这些括号是一个叫做偏移操作符的解引用操作符。它们像*一样解引用后面的变量,但它们还将括号之间的数字添加到被解引用的地址中。例如:
a[5] = 0; // a [offset of 5] = 0
*(a+5) = 0; // pointed to by (a+5) = 0
这两个表达式是等价且有效的,不仅当a是指针时,而且当a是数组时也是如此。记住,如果一个数组,它的名称可以像指向其第一个元素的指针一样使用。
指针初始化
指针可以在定义时初始化,以指向特定的位置:
int myvar;
int * myptr = &myvar;
这段代码之后变量的结果状态与下面相同:
int myvar;
int * myptr;
myptr = &myvar;
当指针初始化时,被初始化的是它们所指向的地址(即myptr),而不是被指向的值(即*myptr)。因此,以上代码不能与下面的表达式混淆:
int myvar;
int * myptr;
*myptr = &myvar;
这无论如何都没有多大意义(并且不是有效的代码)。
指针声明(第2行)中的星号(*)只表示它是一个指针,而不是解引用操作符(如第3行)。两者只是碰巧使用了相同的符号:*。和往常一样,空白是无关紧要的,也永远不会改变一个表达式的意思。
指针可以初始化为一个变量的地址(如上面的例子),也可以初始化为另一个指针(或数组)的值:
int myvar;
int *foo = &myvar;
int *bar = foo;
指针算术
在指针上执行算术操作与在普通整数类型上执行算术操作略有不同。首先,只允许进行加减操作;其他的在指针的世界里没有意义。但是,根据它们所指向的数据类型的大小,加减法在指针上的行为略有不同。
在介绍基本数据类型时,我们看到类型具有不同的大小。例如:char总是有1字节的大小,short通常比它大,而int和long甚至更大;它们的确切大小取决于系统。例如,假设在一个给定的系统中,char占用1个字节、short占用2个字节、long占用4个字节。
假设现在我们在编译器中定义了三个指针:
char *mychar;
short *myshort;
long *mylong;
我们知道它们分别指向1000、2000和3000的内存位置。
因此,如果我们这样写:
++mychar;
++myshort;
++mylong;
正如预期的那样,mychar将包含值1001。但不是很明显,myshort将包含值2002,而mylong将包含值3004,尽管它们各自只增加了一次。原因是,当向指针添加1时,该指针将指向下一个相同类型的元素,因此,它所指向的类型的字节大小将被添加到指针中。
这适用于对指针加减任何数字。它的结果完全相同如果我们写:
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;
关于递增(++)和递减(--)操作符,它们都可以用作表达式的前缀或后缀,只是行为略有不同:作为前缀,递增发生在表达式求值之前,作为后缀,递增发生在表达式求值之后。这也适用于递增和递减指针的表达式,它们可以成为更复杂的表达式的一部分,其中还包括解引用操作符(*)。记住操作符的优先级规则,我们可以回忆起后缀操作符(如递增和递减)的优先级高于前缀操作符(如解引用操作符(*))。
因此,下面的表达式:
*p++
相当于*(p++)。它所做的是增加p(因此它现在指向下一个元素)的值,但由于++被用作后缀,整个表达式被计算为指针(在被增加之前它所指向的地址)最初所指向的值。
本质上,下面是四种可能的解引用操作符与递增操作符的前缀和后缀版本的组合(同样也适用于自减操作符):
*p++ // same as *(p++): increment pointer, and dereference unincremented address
*++p // same as *(++p): increment pointer, and dereference incremented address
++*p // same as ++(*p): dereference pointer, and increment the value it points to
(*p)++ // dereference pointer, and post-increment the value it points to
包含这些操作符的一个典型但不那么简单的语句是:
*p++ = *q++;
因为++的优先级高于*,所以p和q都是递增了的,但是因为两个递增操作符(++)都用作后缀而不是前缀,所以在p和q都被递增之前,赋给*p的值是*q。然后两者都是递增了的。大致相当于:
*p = *q;
++p;
++q;
像往常一样,括号通过提高表达式的易读性来减少混乱。
指针和const
指针可用于通过变量的地址访问变量,这种访问可能包括修改所指向的值。但是也可以声明可以访问指向值的指针来读取它,但不能修改它。为此,只要将指针指向的类型限定为const就足够了。例如:
int x;
int y = 10;
const int * p = &y;
x = *p; // ok: reading p
*p = x; // error: modifying p, which is const-qualified
这里p指向一个变量,但是以const限定的方式指向它,这意味着它可以读取所指向的值,但不能修改它。还要注意,表达式&y是int*类型的,但它被赋值给const int*类型的指针。这是允许的:指向非const的指针可以隐式转换为指向const的指针。但不是反过来!作为一个安全特性,指向const的指针不能隐式转换为指向非const的指针。
指向const元素的指针的一个用例是作为函数形参:将指向非const的指针作为形参的函数可以修改作为实参传递的值,而将指向const的指针作为形参的函数则不能修改。
// pointers as arguments:
#include <iostream>
using namespace std;
void increment_all (int* start, int* stop)
{
int * current = start;
while (current != stop) {
++(*current); // increment value pointed
++current; // increment pointer
}
}
void print_all (const int* start, const int* stop)
{
const int * current = start;
while (current != stop) {
cout << *current << '\n';
++current; // increment pointer
}
}
int main ()
{
int numbers[] = {10,20,30};
increment_all (numbers,numbers+3);
print_all (numbers,numbers+3);
return 0;
}
注意,print_all使用指向常量元素的指针。这些指针指向它们不能修改的常量内容,但它们本身不是常量:也就是说,这些指针仍然可以被增加或分配不同的地址,尽管它们不能修改它们所指向的内容。
这就是将常量的第二个维度添加到指针中的地方:指针本身也可以是const。这是通过将const附加到指针类型(*之后)来指定的:
int x;
int * p1 = &x; // non-const pointer to non-const int
const int * p2 = &x; // non-const pointer to const int
int * const p3 = &x; // const pointer to non-const int
const int * const p4 = &x; // const pointer to const int
使用const和指针的语法绝对是棘手的,识别最适合每种用法的情况往往需要一些经验。在任何情况下,尽早获得指针(和引用)的常量性都是很重要的,但如果这是您第一次接触const和指针的混合使用,则不必过于担心掌握所有内容。更多的用例将在接下来的章节中展示。
为了给带指针的const的语法增加一点混乱,const限定符可以在指向类型之前或之后,具有完全相同的含义:
const int * p2a = &x; // non-const pointer to const int
int const * p2b = &x; // also non-const pointer to const int
与星号周围的空格一样,在本例中const的顺序只是样式的问题。本章使用了一个前缀const,由于历史原因,这似乎更扩展,但两者完全相同。每种风格的优点在互联网上仍有激烈的争论。
指针和字符串字面量
如前所述,字符串字面量是包含以空结束的字符序列的数组。在前面的部分中,已经使用字符串字面量直接插入cout,初始化string和初始化字符数组。
但它们也可以直接访问。字符串字面量是适当数组类型的数组,包含其所有字符加上结束的空字符,每个元素的类型为const char(作为字面量,它们永远不能被修改)。例如:
const char * foo = "hello";
它声明了一个数组,用"hello"的字面量形式表示,然后将指向其第一个元素的指针赋值给foo。如果我们想象"hello"存储在从地址1702开始的内存位置中,我们可以将前面的声明表示为:
注意,这里foo是一个指针,包含值1702,而不是'h',也不是"hello",尽管1702确实是这两个地方的地址。
指针foo指向一个字符序列。由于指针和数组在表达式中的行为本质上是相同的,因此可以使用foo以与以空结束的字符序列数组相同的方式访问字符。例如:
*(foo+4)
foo[4]
两个表达式的值都是'o'(数组的第五个元素)。
指向指针的指针
C++允许使用指向指针的指针,而指针又指向数据(甚至指向其他指针)。语法只需要在指针的声明中为每一个间接的层使用一个星号(*):
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;
假设每个变量7230、8092和10502的内存位置是随机选择的,可以表示为:
每个变量的值在其对应的单元格中表示,它们在内存中的各自地址由它们下面的值表示。
这个例子中的新东西是变量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 << '\n';
return 0;
}
sizeof是C++语言中集成的一个操作符,它返回实参的字节大小。对于非动态数据类型,此值为常量。因此,例如,sizeof(char)为1,因为char的大小总是一个字节。
无效指针和空(null)指针
原则上,指针指的是有效的地址,例如变量的地址或数组中元素的地址。但是指针实际上可以指向任何地址,包括不引用任何有效元素的地址。
典型的例子是未初始化的指针和指向数组中不存在的元素的指针:
int * p; // uninitialized pointer (local variable)
int myarray[10];
int * q = myarray+20; // element out of bounds
p和q都不指向已知包含值的地址,但上述语句都不会导致错误。在C++中,指针被允许接受任何地址值,无论该地址是否有实际数据。可能导致错误的是解引用这样的指针(即,实际访问它们所指向的值)。访问这样的指针会导致未定义的行为,从运行时的错误到访问一些随机值。
但是,有时,指针确实需要显式地不指向任何地方,而不仅仅是一个无效的地址。对于这种情况,存在一个任何类型的指针都可以接受的特殊值:空指针值。这个值在C++中可以用两种方式表示:要么用0的整数值表示,要么用nullptr关键字表示:
int * p = 0;
int * q = nullptr;
这里,p和q都是空指针,这意味着它们显式地不指向任何地方,而且它们实际上比较相等:所有空指针的比较都等于其他空指针。在较老的代码中,经常可以看到定义的常量NULL被用于引用空指针值:
int * r = NULL;
NULL在标准库的多个头文件中定义,并被定义为一些空指针常量值(如0或nullptr)的别名。
不要混淆空指针和void指针!空指针是一种任何指针都可以接受的值,表示它不指向“任何地方”,而void指针是一种指针类型,可以指向某个地方,而不需要特定的类型。一个指向存储在指针中的值(空指针),另一个指向指针所指向的数据类型(void指针)。
指向函数的指针
C++允许使用指向函数的指针进行操作。它的典型用法是将一个函数作为实参传递给另一个函数。函数指针的声明语法与普通函数声明相同,只是函数名用圆括号()括起来,并且在函数名前面插入星号(*):
// 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;
}
在上面的例子中,minus是一个指向函数的指针,该函数有两个int类型的形参。它直接初始化指向函数subtraction:
int (* minus)(int,int) = subtraction;