指针
在前面的章节中,变量被解释为可以通过其标识符(名称)访问的计算机内存中的位置。
这样,程序不需要关心内存中数据的物理地址;它只需在需要引用变量时使用标识符。
对于 C++ 程序,计算机的内存就像一个连续的存储单元,每个单元的大小是一个字节,
并且每个单元都有一个唯一的地址。这些单字节存储单元的排序方式允许大于一个字节
的数据表示占用具有连续地址的存储单元。
例如,地址为1776的存储单元总是紧跟着地址为1775的存储单元,而在地址为1777的存储单元的前面,
并且在它前面1000个单元的位置是地址为776的存储单元,在它后面1000个单元的位置是地址为2776的存储单元。
声明变量时,存储其值所需的内存会被分配到内存中的特定位置(其内存地址)。
通常,C++ 程序不会主动决定存储其变量的确切内存地址。
该任务留给了程序运行的环境——通常是一个操作系统,它在运行时决定特定的内存位置。
但是,程序能够在运行时获取变量的地址以便访问相对于它的特定位置的数据单元。
☞ 取地址操作符(&)
变量的地址可以通过在变量名前面加上与号(&)来获得,称为地址操作符。例如:
这将把变量myvar的地址赋值给foo;通过在变量myvar的名称前面加上地址操作符(&),
我们不再将变量本身的内容赋值给foo,而是赋值给它的地址。
在程序运行前不能知道内存中变量的实际地址,但为了帮助澄清一些概念,
让我们假设myvar在运行时放置在内存地址1776中。
在这种情况下,考虑以下代码片段:
myvar = 25;
foo = &myvar;
bar = myvar;
|
执行此操作后,每个变量所包含的值如下图所示:
首先,我们将值25赋值给myvar(我们假设该变量在内存中的地址为1776)。
第二个语句给foo赋值myvar的地址,我们假设它是1776。
最后,第三条语句将myvar的值赋值给bar。这是一个标准的赋值操作,在前面的章节中已经做过很多次了。
第二个和第三个语句之间的主要区别是操作符的地址(&)的出现。
存储另一个变量地址的变量(就像前面例子中的foo)在c++中被称为指针。指针是该语言的一个非常强大的特性,
在低级编程中有很多用途。稍后,我们将看到如何声明和使用指针
☞ 解引用操作符(*)
如前所述,存储另一个变量地址的变量称为指针。指针被称为“指向”它们所存储地址的变量。
指针的一个有趣属性是,它们可以被用来直接访问它们所指向的变量。
这是通过在指针名称前面加上解引用操作符(*)来实现的。操作符本身可以解读为“被指向的值”。
因此,接前面的例子,下面的语句:
这可以解读为:“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;
|
在这两个语句之后,下面所有的表达式的结果都为true:
myvar == 25
&myvar == 1776
foo == 1776
*foo == 25
|
第一个表达式很清楚,因为对myvar执行的赋值操作是myvar=25。
第二个使用地址操作符(&),它返回myvar的地址,我们假设myvar的值为1776。
第三个比较明显,因为第二个表达式是true,并且foo上执行的赋值操作是foo=&myvar。
第四个表达式使用解引用操作符(*),它可以被读成“被foo指向的值”,而foo指向的值确实是25。
所以,在所有这些操作之后,你也可以推断只要foo指向的地址保持不变,下面的表达式也将为真:
☞ 声明指针
由于指针可以直接引用它所指向的值,因此指向char类型的指针与指向int或float类型的指针具有不同的属性。
解引用后,需要知道类型。为此,指针的声明需要包含指针将要指向的数据类型。
指针的声明遵循以下语法:
type * name;
其中type是指针指向的数据类型。这种类型不是指针本身的类型,而是指针所指向的数据的类型。例如:
int * number;
char * character;
double * decimals;
|
这是三种指针的声明。每个指针都指向不同的数据类型,但实际上,它们都是指针,
并且它们在内存中可能占用相同的空间(指针在内存中的大小取决于程序运行的平台)。
然而,它们所指向的数据既不占用相同的空间,也不属于相同的类型:第一个指向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 is 10
secondvalue is 20
|
请注意,即使firstvalue和secondvalue都没有在程序中直接设置任何值,
它们最终都通过使用mypointer间接设置了一个值。事情是这样的:
首先,mypointer使用地址操作符(&)分配第一个值的地址。然后,mypointer指向的值被赋值为10。
因为此时mypointer指向firstvalue的内存位置,这实际上修改了firstvalue的值。
为了说明一个指针在程序的生命周期内可以指向不同的变量,本例使用第二个值和同一个指针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;
}
|
firstvalue is 10
secondvalue is 20
|
每个赋值操作都有一行如何读取的注释:例如,用“address of”替换&,用“value pointed to by”替换*。
注意,带有指针p1和p2的表达式,有的带(*)有的没有。
使用解引用操作符(*)的表达式的含义与不使用解引用操作符(*)的表达式的含义截然不同。
当*操作符位于指针名称之前时,表达式指向所指向的值,而当没有此操作符的指针出现时,
表达式指向指针本身的值(即指针所指向的地址)。
另一件需要你注意的事是这一行:
这声明了前面示例中使用的两个指针。但请注意,每个指针都有一个星号(*),以便两者都具有int*类型(指向int的指针)。
由于优先级规则,这是必需的。注意,如果代码是:
P1确实是 int* 类型,但p2是 int 类型。对于这一点,空间并不重要。但是无论如何,
只要记住每个指针放一个星号就足够了,
对于每个语句声明多个指针感兴趣的用户来说。最好:对每个变量使用不同的语句。
☞ 指针和数组
数组的概念与指针的概念相关。实际上,数组的工作原理与指向其第一个元素的指针非常相似,
而且实际上,数组总是可以隐式转换为适当类型的指针。例如,考虑这两个声明:
int myarray [20];
int * mypointer;
|
以下赋值操作是有效的:
接下来,mypointer和myarray将是等价的,并且具有非常相似的属性。
主要的区别在于mypointer可以被重新赋值一个不同的地址,而myarray不能,
它总是表示由20个int类型元素组成的同一块。因此,下面的赋值是无效的:
让我们看一个混合数组和指针的例子:
// 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,
|
指针和数组支持相同的操作集,它们的含义相同。主要的区别在于指针可以被分配新的地址,而数组不能。
在关于数组的章节中,方括号([])被解释为指定数组元素的索引。实际上,这些括号是一个解引用运算符被称为偏移运算符。
它们解引用后面的变量,就像 * 一样,但它们还将方括号之间的数字添加到解引用的地址中。例如:
a[5] = 0; // a [offset of 5] = 0
*(a+5) = 0; // pointed to by (a+5) = 0
|
这两个表达式是等价的和有效的,不管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通常大于1字节,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++):自增指针,解引用未自增地址
*++p // 同*(++p):自增指针,解引用自增地址
++*p // 与++(*p)相同:解引用指针,并增加它所指向的值
(*p)++ // 解引用指针,并对它所指向的值进行后增
|
包含这些操作符的一个典型但不那么简单的语句是:
因为++的优先级比*高,所以p和q都是递增的,但是因为两个递增操作符(++)都用作后缀而不是前缀,
所以赋给*p的值在p和q都递增之前是*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指向一个变量,但以常量限定的方式指向它,这意味着它可以读取指向的值,
但不能修改它。还要注意,表达式&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;
}
|
11
21
31
|
注意,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,用于初始化字符串和初始化字符数组。
但它们也可以直接访问。字符串字面值是包含所有字符和结束null字符的
正确数组类型的数组,每个元素都是const char类型(
作为字面值,它们永远不能被修改)。例如:
const char * foo = "hello";
|
这声明了一个“hello”的文字表示数组,然后将指向其第一个元素的指针赋值给foo。
如果我们假设“hello”存储在从1702开始的内存位置,我们可以将前面的声明表示为:
注意这里foo是一个指针,它包含1702的值,而不是'h',
也不是"hello",尽管1702确实是这两个的地址。
指针foo指向一个字符序列。因为指针和数组在表达式中的行为本质上是相同的,
所以可以使用foo访问字符,就像使用以null结束的字符序列的数组一样。例如:
两个表达式的值都是'o'(数组的第5个元素)。
☞ 指向指针的指针
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指针提供了很大的灵活性,因为它可以指向任何数据类型,从整数值或浮点数到字符串。
作为交换,他们有一个很大的限制:他们指出的数据不能直接引用
(这是合乎逻辑的,因为我们没有间接引用类型),因此,任何一个空指针地址被引用之前,
需要被转换成其他指针类型,指定一个具体的数据类型。
它的可能用途之一是向函数传递泛型参数。例如:
// 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;
}
|
y, 1603
|
sizeof是c++语言中集成的运算符,它以字节为单位返回实参的大小。
对于非动态数据类型,这个值是一个常量。因此,例如,sizeof(char)是1,
因为char的大小总是一个字节。
☞ 无效指针和空指针
原则上,指针是指向有效地址的,比如变量的地址或数组元素的地址。
但是指针实际上可以指向任何地址,包括不指向任何有效元素的地址。
典型的例子是未初始化的指针和指向数组中不存在元素的指针:
int * p; // uninitialized pointer (local variable)
int myarray[10];
int * q = myarray+20; // element out of bounds
|
p和q都不指向已知包含值的地址,但是以上语句都不会导致错误。
在c++中,指针可以接受任何地址值,无论该地址是否真的存在。
会导致错误的是对这样的指针进行解引用(即实际访问它们所指向的值)。
访问这样的指针会导致未定义的行为,从运行时出错到访问某个随机值。
但是,有时,指针确实需要显式地指向任何地方,而不仅仅是一个无效的地址。
对于这种情况,存在一个任何类型的指针都可以接受的特殊值:空指针值。
在c++中,这个值可以用两种方式表示:用零整数值,或者用nullptr关键字:
int * p = 0;
int * q = nullptr;
|
在这里,p和q都是空指针,这意味着它们显式地指向任何地方,
它们实际上是相等的:所有空指针等于其他空指针。
在旧代码中,经常会看到定义的常量NULL用来引用空指针值:
NULL在标准库的几个头文件中定义,并被定义为一些空指针常量值(如0或nullptr)的别名。
不要混淆 null指针void空指针!null指针是一个值,任何指针都可以用它来表示它指向“任何地方”,
而 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;
}
|
8
|
在上面的例子中,minus是一个指向具有两个int类型形参的函数的指针。它被直接初始化为指向subtraction:
int (* minus)(int,int) = subtraction;
|