C++ 语言指针 (pointer)
指针 (pointer) 是指向 (point to) 另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
定义指针类型的方法将声明符写成 *d 的形式,其中 d 是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号 * 。
1. 获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符 (操作符 & )。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* ipl 和 ip2 都是指向 int 型对象的指针。 */
int *ipl, *ip2;
/* dp2 是指向 double 型对象的指针,dp1 是 double 型对象。 */
double dp1, *dp2;
int val = 42;
/* pval 存放变量 val 的地址,或者说 pval 是指向变量 val 的指针。 */
int *pval = &val;
return 0;
}
第四条语句把 pval 定义为一个指向 int 的指针,随后初始化 pval 令其指向名为 val 的 int 对象。因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
2. 指针值
指针的值 (即地址) 应属下列 4 种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。
尽管第 2 种和第 3 种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象,所以试图访问此类指针 (假定的) 对象的行为不被允许。
3. 利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符 (操作符 * ) 来访问该对象,解引用操作仅适用于那些确实指向了某个对象的有效指针。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* pval 存放着变量 val 的地址,或者说 pval 是指向变量 val。 */
int val = 99;
int *pval = &val;
/* 由符号 * 得到指针 pval 所指的对象,输出 99。 */
std::cout << *pval << std::endl;
/* 由符号 * 得到指针 pval 所指的对象,可经由 pval 为变量 val 赋值。 */
*pval = 22;
std::cout << *pval << std::endl;
int i = 42;
/* & 紧随类型名出现,因此是声明的一部分,ref 是一个引用。 */
int &ref = i;
/* * 紧随类型名出现,因此是声明的一部分,p 是一个指针。 */
int *p;
/* & 出现在表达式中,是一个取地址符。 */
p = &i;
/* * 出现在表达式中,是一个解引用符。 */
*p = i;
/* & 是声明的一部分,* 是一个解引用符。 */
int &r = *p;
return 0;
}
99
22
请按任意键继续. . .
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。如上述程序所示,为 *pval 赋值实际上是为 *pval 所指的对象赋值。
像 & 和 * 这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义。在声明语句中,& 和 * 用于组成复合类型;在表达式中,它们的角色又转变成运算符。
4. 空指针
空指针 (null pointer) 不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int *pl = nullptr;
int *p2 = 0;
int *p3 = NULL;
return 0;
}
得到空指针最直接的办法就是用字面值 nullptr 来初始化指针,这也是 C++11 新标准引入的一种方法。nullptr 是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。也可以通过将指针初始化为字面值 0 来生成空指针。
过去的程序还会用到一个名为 NULL 的预处理变量 (preprocessor variable) 来给指针赋值,这个变量在头文件 cstdlib 中定义,它的值就是 0。预处理变量不属于命名空间 std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无须在前面加上 std:: 。
当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用 NULL 初始化指针和用 0 初始化指针是一样的。在新标准下,现在的 C++ 程序最好使用 nullptr ,同时尽量避免使用 NULL 。
把 int 变量直接赋给指针是错误的操作,即使 int 变量的值恰好等于 0 也不行。
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int *pl = nullptr;
int *p2 = 0;
int *p3 = NULL;
int zero = 0;
int *pi = zero;
return 0;
}
1>d:\visual_studio_workspace\...\yongqiang.cpp(18): error C2440: 'initializing': cannot convert from 'int' to 'int *'
1> d:\visual_studio_workspace\...\yongqiang.cpp(18): note: Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or function-style cast
建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0 ,这样程序就能检测并知道它没有指向任何具体的对象了。
5. 赋值和指针
指针和引用都能提供对其他对象的间接访问,引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。指针和它存放的地址之间就没有这种限制,和其他任何变量 (只要不是引用) 一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
记住赋值永远改变的是等号左侧的对象。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
int val = 66;
/* p1 被初始化,但没有指向任何对象。 */
int *p1 = 0;
/* p2 被初始化,存有 val 的地址。 */
int *p2 = &val;
int *pi = nullptr;
/* pi 的值被改变,pi 指向 val。 */
pi = &val;
/* val 的值被改变,指针 pi 并没有改变。 */
*pi = 1;
return 0;
}
pi = &val;
为 pi 赋一个新的值,也就是改变了那个存放在 pi 内的地址值。
*pi = 1;
则 *pi (指针 pi 指向的对象) 发生改变。
6. 其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。如果指针的值是 0,条件值为 false,任何非 0 指针对应的条件值都是 true。
对于两个类型相同的合法指针,可以用相等操作符 (==) 或不相等操作符 (!=) 来比较它们,比较的结果是布尔类型。如果两个指针存放的地址值相同,则它们相等;反之它们不相等。这里两个指针存放的地址值相同 (两个指针相等) 有三种可能:它们都为空、都指向同一个对象,或者都指向了同一个对象的下一地址。需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等。
7. void* 指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个 void* 指针存放着一个地址,这一点和其他指针类似。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
double obj = 3.1415926, *pobj = &obj;
/* void* 能存放任意类型对象的地址,obj 可以是任意类型的对象。 */
void *pv = &obj;
pv = pobj;
return 0;
}
利用 void* 指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个 void* 指针。不能直接操作 void* 指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以 void* 的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
8. 定义多个变量
复合类型 (compound type) 是指基于其他类型定义的类型,例如引用和指针。一条声明语句由一个基本数据类型 (base type) 和紧随其后的一个声明符 (declarator) 列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。一条定义语句可能定义出不同类型的变量。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
int main()
{
/* i 是一个 int 数,p 是一个 int 指针,r 是一个 int 引用。 */
int i = 1024, *p = &i, &r = i;
return 0;
}
有一种观点会误以为,在定义语句中,类型修饰符 (* 或 & ) 作用于本次定义的全部变量。造成这种错误看法的原因有很多,其中之一是我们可以把空格写在类型修饰符和变量名中间。
int* p; // 合法,但是容易产生误导
这种写法可能产生误导是因为 int* 放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是 int 而非 int* 。* 仅仅是修饰了 p 而已,对该声明语句中的其他变量,它并不产生任何作用。
int* p1, p2; // p1 是指向 int 的指针,p2 是 int
涉及指针或引用的声明,一般有两种写法。第一种把修饰符和变量标识符写在一起:
int *p1, *p2; // p1 和 p2 都是指向 int 的指针
这种形式着重强调变量具有的复合类型。第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量。
int* p1; // p1 是指向 int 的指针
int* p2; // p2 是指向 int 的指针
推荐釆用第一种写法,将 * (或是 & ) 与变量名连在一起。
9. 指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过 * 的个数可以区分指针的级别。也就是说,** 表示指向指针的指针,*** 表示指向指针的指针的指针。
此处 pi 是指向 int 的指针,而 ppi 是指向 int 指针的指针。
解引用 int 指针会得到一个 int 数,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用。
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = π // ppi points to a pointer to an int
cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **ppi
<< endl;
int i = 2;
int *p1 = &i; // p1 points to i
*p1 = *p1 * *p1; // equivalent to i = i * i
cout << "i = " << i << endl;
*p1 *= *p1; // equivalent to i *= i
cout << "i = " << i << endl;
return 0;
}
The value of ival
direct value: 1024
indirect value: 1024
doubly indirect value: 1024
i = 4
i = 16
请按任意键继续. . .
该程序使用三种不同的方式输出了变量 ival 的值:第一种直接输出;第二种通过 int 指针 pi 输出;第三种两次解引用 ppi,取得 ival 的值。
10. 指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
//============================================================================
// Name : Yongqiang Cheng
// Author : Yongqiang Cheng
// Version : Version 1.0.0
// Copyright : Copyright (c) 2020 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int ival = 1024;
int *p; // p 是一个 int 指针
int *&r = p; // r 是一个对指针 p 的引用
r = &ival; // r 引用了一个指针,给 r 赋值 &ival 就是令 p 指向 ival
*r = 512; // 解引用 r 得到 ival,也就是 p 指向的对象,将 ival 的值改为 512
return 0;
}
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。离变量名最近的符号 (此例中是 &r 的符号 & ) 对变量的类型有最直接的影响,因此 r 是一个引用。声明符的其余部分用以确定 r 引用的类型是什么,此例中的符号 * 说明 r 引用的是一个指针。最后,声明的基本数据类型部分指出 r 引用的是一个 int 指针。
* - 解引用
& - 取地址
References
(美) Stanley B. Lippman, (美) Josée Lajoie, (美) Barbara E. Moo 著, 王刚, 杨巨峰 译. C++ Primer 中文版[M]. 第 5 版. 电子工业出版社, 2013. https://www.informit.com/store/c-plus-plus-primer-9780321714114
|