指针简介
本节阅读量:指针是C++从C继承而来的强大特性,也是许多有抱负的C++学习者陷入困境的地方。然而,正如您很快就会看到的,指针并不是什么可怕的东西。
事实上,指针的行为很像左值引用。但在我们进一步解释之前,先做一个简单的开场。
考虑一个普通的变量,如下所示:
|
|
简单来说,当执行该定义生成的代码时,会将一块内存分配给该对象。假设变量x被分配了内存地址140。每当我们在表达式或语句中使用变量x时,程序将访问存储在内存地址140的值。
关于变量的好处是,不需要担心分配了什么特定的内存地址,或者需要多少字节来存储对象的值。只需要通过给定的标识符来使用变量,编译器将该名称转换为适当分配的内存地址。编译器负责寻址工作。
引用也是如此:
|
|
因为ref充当x的别名,所以每当使用ref时,程序将转到内存地址140来访问该值。同样,编译器负责寻址。
取地址操作(&)
尽管默认情况下变量使用的内存地址不会向我们公开,但确实可以访问到这些信息。运算符(&)返回其操作数的内存地址。这非常简单:
|
|
在作者的机器上,上述程序打印:
|
|
在上面的示例中,我们使用操作符(&)来获取分配给x的内存地址,并将该地址打印到控制台。内存地址通常打印为十六进制值,通常没有0x前缀。
对于使用多个字节内存的对象,& 操作符将返回该对象使用的第一个字节的内存地址。
提示
&符号往往会引起混淆,因为它根据上下文具有不同的含义:
- 当跟在类型名称后面时,& 表示左值引用:int& ref。
- 在表达式中的一元上下文中使用时,& 是取地址操作:std::cout « &x。
- 在表达式的二进制上下文中使用时,& 是逐位AND运算符:std::cout « x & y。
解引用运算符(*)
获取变量的地址本身并不是很有用。
我们可以对地址做的最有用的事情是访问存储在该地址的值。解引用运算符(*)将给定内存地址处的值作为左值返回:
|
|
在作者的机器上,上述程序打印:
|
|
这个程序相当简单。首先,我们声明一个变量x并打印其值。然后我们打印变量x的地址。最后,我们使用解引用操作符来获取变量x的内存地址处的值(也是x的值),然后将其打印到控制台。
关键点
给定一个内存地址,我们可以使用解引用操作符(*)来获取该地址的值(作为左值)。
运算符(&)和解引用运算符(*)是相反的:& 获取对象的地址,而解引用获取地址处的对象。
提示
尽管解引用运算符看起来就像乘法运算符,但可以区分它们,因为解引用运算符是一元的,而乘法运算符是二元的。
获取变量的内存地址,然后立即解引用该地址以获取值也不是那么有用(毕竟,我们可以只使用变量来访问值)。
但现在我们已经在工具包中添加了操作符(&)和解引用操作符(*),可以准备好讨论指针了。
指针(pointer)
指针是保存内存地址(通常是另一个变量的地址)作为其值的对象。这允许我们存储其他对象的地址以供以后使用。
与使用与号(&)字符声明引用类型很相似,指针类型使用星号(*)声明:
|
|
要创建指针变量,我们只需定义具有指针类型的变量:
|
|
请注意,这个星号是指针声明语法的一部分,而不是解引用操作符的使用。
旁白
在现代C++中,我们在这里讨论的指针有时被称为“原始指针”,以帮助将它们与后来引入该语言的“智能指针”区分开来。我们在后续讨论智能指针。
最佳实践
声明指针类型时,请将星号放在类型名称旁边。
警告
尽管通常不应在一行上声明多个变量,但如果您这样做,则每个变量都必须包含星号。
|
|
为了避免误用,有人习惯将星号与变量名放在一起,而不是类型名放在一起。 但是更好的做法是避免在同一语句中定义多个变量。
指针初始化
与普通变量一样,指针在默认情况下不会初始化。尚未初始化的指针有时称为野指针。野指针包含垃圾地址,解引用野指针将导致未定义的行为。因此,您应该始终初始化指针。
|
|
由于指针保存地址,因此当我们初始化指针或将值赋给指针时,该值必须是地址。通常,指针用于保存另一个变量的地址(可以使用操作符(&)的地址来获得)。
一旦有一个指针保存另一个对象的地址,就可以使用解引用操作符(*)来访问该地址上的值。例如:
|
|
这将打印:
|
|
从概念上讲,您可以这样想上面的代码:

指针的名称是ptr,ptr变量对应的地址内保存着x的地址,所以我们说ptr“指向”x。
注
关于指针命名法的注释:“X指针”(X是某种类型)是“指向X的指针”的常用缩写。所以当我们说“整数指针”时,我们实际上是指“指向整数的指针”。当我们谈论常量指针时,这种理解将很有价值。
就像引用的类型必须匹配被引用对象的类型一样,指针的类型必须与所指向的对象的类型匹配:
|
|
除了下一课将讨论的一个例外,不允许使用字面值初始化指针:
|
|
指针和赋值
我们可以以两种不同的赋值方式来使用指针:
- 改变指针所指向的对象(为指针赋值一个新的所指向的地址)
- 改变指针指向对象的值(通过解引用操作,为所指对象赋值)
首先,让我们来看一个指针更改为指向其他对象的情况:
|
|
以上打印内容:
|
|
在上面的例子中,我们定义了指针ptr,用x的地址初始化它,并解引用指针以打印指向的值(5)。然后,我们使用赋值运算符将ptr保存的地址更改为y的地址。然后,再次解引用指针,以打印所指向的值(现在是6)。
现在,让我们看看如何使用指针来更改所指向的值:
|
|
该程序打印:
|
|
在这个例子中,我们定义指针ptr,用地址x初始化它,然后打印x和ptr(5)的值。因为ptr返回一个左值,所以我们可以在赋值语句的左侧使用它,这样做是为了将ptr指向的值更改为6。然后,我们再次打印x和*ptr的值,以显示该值已按预期更新。
关键点
当使用没有解引用(ptr)的指针时,访问指针持有的地址。修改它(ptr=&y)将更改指针所指向的地址。
当解引用指针(*ptr)时,访问所指向的对象。修改它(*ptr=6;)将更改所指向对象的值。
指针的行为很像左值引用
指针和左值引用的行为类似。考虑以下程序:
|
|
该程序打印:
|
|
在上面的程序中,我们创建了一个值为5的变量x,然后创建一个它的左值引用和一个指向x的指针。接下来,使用左值引用将x的值从5更改为6,并且可以通过所有三种方法访问该更新的值。最后,使用解引用指针将值从6更改为7,并再次通过所有三个方法访问更新的值。
因此,指针和引用都提供了间接访问另一个对象的方法。主要的区别是,对于指针,需要显式地获取指向的地址,并且必须显式地解引用指针才能获得值。对于引用,获取地址和解引用隐式发生。
指针和引用之间还有一些其他差异值得一提:
- 引用必须初始化,指针不需要初始化。
- 引用不是一个单独的对象,指针是。
- 无法重新设置引用(更改为引用其他内容),指针可以更改它们所指向的内容。
- 引用必须始终绑定到对象,指针可以指向空。
- 引用是“安全的”(除了悬空引用之外),指针本身就是危险的。
取址操作返回的是指针
值得注意的是,操作符(&)不会以文本形式返回其操作数的地址。相反,它返回一个指针,该指针包含操作数的地址,其类型是从参数派生的(例如,获取int的地址将返回int指针中的地址)。
我们可以在下面的示例中看到这一点:
|
|
在Visual Studio上,这打印了:
|
|
使用gcc,它会打印“pi”(指向int的指针)。由于typeid().name()的结果依赖于编译器,因此编译器可能会打印不同的内容,但它将具有相同的含义。
指针的大小
指针的大小取决于编译可执行文件的体系结构——32位可执行文件使用32位内存地址——因此,32位机器上的指针是32位(4字节)。对于64位可执行文件,指针将是64位(8字节)。请注意,无论所指向的对象的大小如何,均是如此:
|
|
指针的大小始终相同。这是因为指针只是内存地址,并且访问内存地址所需的位数是恒定的。
悬空指针
与悬空引用很相似,悬空指针是保存不再有效的对象地址的指针(例如,因为它已被销毁)。
解引用悬空指针(例如,为了打印所指向的值)将导致未定义的行为,因为您正在尝试访问不再有效的对象。
也许令人惊讶的是,语言标准说“无效指针值的任何其他使用都具有实现定义的行为”。这意味着您可以为无效指针分配新的值,例如nullptr。然而,使用无效指针值的任何其他操作(例如复制或无效指针的数学运算)都将产生由实现定义的行为。
下面是创建悬空指针的示例:
|
|
上述程序可能会打印:
|
|
但可能不会,因为ptr所指向的对象超出作用域,并在内部块的末尾被销毁,使ptr悬空。
关键点
解引用无效指针将导致未定义的行为。无效指针值的任何其他使用都是由实现定义的。
结论
指针是保存内存地址的变量。可以使用解引用操作符(*)来解引用它们,以检索它们所持有的地址处的值。解引用野指针或悬空指针将导致未定义的行为,并可能导致应用程序崩溃。
指针比引用更灵活,也更危险。我们将在接下来的课程中继续探索这一点。
