12-7 指针介绍
指针一直是 C++ 的一大难题,也是许多 C++ 初学者容易卡关的地方。然而,正如你很快就会看到的,指针其实没什么可怕的。
实际上,指针的行为很像左值引用。但在进一步解释之前,我们先做一些准备工作。
相关内容:
如果您对左值引用感到生疏或不熟悉,现在是复习的好时机。我们将在第12.3 课——左值引用,第 12.4 课——左值引用常量,以及第 12.5 课——按左值引用传递。
考虑一个正态变量,例如:
char x {}; // chars use 1 byte of memory
简单来说,当执行根据此定义生成的代码时,RAM 中的一块内存将被分配给该对象。例如,假设变量x被分配了内存地址140。每当我们x在表达式或语句中使用变量时,程序都会访问140该内存地址以查找存储在那里的值。
变量的好处在于,我们无需关心具体的内存地址分配,也无需关心存储对象值需要多少字节。我们只需通过变量的标识符来引用它,编译器就会将这个名称转换成相应的内存地址。编译器会处理所有的寻址问题。
参考文献也是如此:
int main()
{
char x {}; // assume this is assigned memory address 140
char& ref { x }; // ref is an lvalue reference to x (when used with a type, & means lvalue reference)
return 0;
}
因为 ref 是 x 的别名,所以每当我们使用 ref 时,程序都会访问内存地址 140 来获取值。编译器会处理寻址问题,因此我们无需考虑寻址本身。
地址运算符 (&)
虽然默认情况下我们无法直接获取变量使用的内存地址,但我们确实可以通过其他方式获取这些信息。取地址运算符(&) 会返回其操作数的内存地址。这非常简单明了:
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // print the value of variable x
std::cout << &x << '\n'; // print the memory address of variable x
return 0;
}
在作者的电脑上,上述程序打印出:

在上面的例子中,我们使用取地址运算符 (&) 来获取分配给变量 x 的地址,并将该地址打印到控制台。内存地址通常以十六进制值的形式打印(我们在5.3 课——数字系统(十进制、二进制、十六进制和八进制)中讲解了十六进制),通常不带 0x 前缀。
对于使用超过一个字节内存的对象,address-of 将返回对象使用的第一个字节的内存地址。
提示:
& 符号容易引起混淆,因为它在不同的上下文中有不同的含义:
- 当跟在类型名称后面时,& 表示左值引用:int& ref。
- 当在表达式中以一元上下文使用时,& 是地址运算符:std::cout << &x。
- 在表达式的二进制上下文中使用时,& 是按位与运算符:std::cout << x & y。
解引用运算符 (*)
获取变量地址本身并没有太大用处。
地址最有用的用途是访问存储在该地址处的值。解引用运算符(*)(有时也称为间接运算符)将给定内存地址处的值作为左值返回:
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // print the value of variable x
std::cou << &x << '\n'; // print the memory address of variable x
std::cout << *(&x) << '\n'; // print the value at the memory address of variable x (parentheses not required, but make it easier to read)
return 0;
}
在作者的电脑上,上述程序打印出:

这个程序很简单。首先,我们声明一个变量 x 并打印它的值。然后,我们打印该变量 x的地址。最后,我们使用解引用运算符获取该变量 x 内存地址处的值(也就是变量 x 的值),并将其打印到控制台。
关键见解:
给定一个内存地址,我们可以使用解引用运算符()来获取该地址处的值(作为左值)。
寻址运算符 (&) 和解引用运算符 () 的作用正好相反:寻址运算符获取对象的地址,而解引用运算符获取指定地址处的对象。
提示:
虽然解引用运算符看起来和乘法运算符一模一样,但你可以通过解引用运算符是一元运算符而乘法运算符是二元运算符来区分它们。
获取变量的内存地址,然后立即解引用该地址来获取值,这种做法也没什么用(毕竟,我们可以直接使用变量来访问值)。
但现在我们的工具包中已经添加了地址运算符 (&) 和解引用运算符 (*),我们就可以开始讨论指针了。
指针
指针是一个对象,它的值是一个内存地址(通常是另一个变量的地址)。这使我们能够存储其他对象的地址以供后续使用。
顺便提一下……
在现代 C++ 中,我们这里讨论的指针有时被称为“原始指针”或“哑指针”,以区别于最近引入语言的“智能指针”。我们将在第 22 章介绍智能指针。
指定指针的类型(例如 int* )称为指针类型。与引用类型使用与号 (&) 声明类似,指针类型使用星号 (*) 声明:
int; // a normal int
int&; // an lvalue reference to an int value
int*; // a pointer to an int value (holds the address of an integer value)
要创建指针变量,我们只需定义一个指针类型的变量:
int main()
{
int x { 5 }; // normal variable
int& ref { x }; // a reference to an integer (bound to x)
int* ptr; // a pointer to an integer
return 0;
}
请注意,此星号是指针声明语法的一部分,而不是解引用运算符的用法。
最佳实践:
声明指针类型时,在类型名称旁边放置星号。
警告
虽然通常不应该在一行中声明多个变量,但如果一定要这样做,则每个变量都必须包含星号。int* ptr1, ptr2; // incorrect: ptr1 is a pointer to an int, but ptr2 is just a plain int! int* ptr3, * ptr4; // correct: ptr3 and ptr4 are both pointers to an int虽然这有时被用作不将星号放在类型名称旁边(而是放在变量名称旁边)的理由,但它更好的理由是避免在同一语句中定义多个变量。
指针初始化
与普通变量一样,指针默认情况下不会被初始化。未初始化的指针有时被称为“野指针”。野指针指向的是一个无意义的地址,解引用野指针会导致未定义行为。因此,您应该始终将指针初始化为一个已知的值。
最佳实践:
务必初始化指针。
int main()
{
int x{ 5 };
int* ptr; // an uninitialized pointer (holds a garbage address)
int* ptr2{}; // a null pointer (we'll discuss these in the next lesson)
int* ptr3{ &x }; // a pointer initialized with the address of variable x
return 0;
}
由于指针保存的是地址,因此当我们初始化或给指针赋值时,该值必须是地址。通常,指针用于保存另一个变量的地址(我们可以使用取地址运算符 (&) 获取该地址)。
一旦我们有了指向另一个对象地址的指针,就可以使用解引用运算符 (*) 来访问该地址处的值。例如:
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // print the value of variable x
int* ptr{ &x }; // ptr holds the address of x
std::cout << *ptr << '\n'; // use dereference operator to print the value at the address that ptr is holding (which is x's address)
return 0;
}
打印出来的内容:

从概念上讲,你可以这样理解上面的代码片段:

这就是指针名称的由来——ptr,它保存着 x 的地址 0x7a9ddd6de040,所以我们说它 ptr指向 x。
作者注:
关于指针命名规则的说明:“X 指针”(其中 X 代表某种类型)是“指向 X 的指针”的常用简写。因此,当我们说“整数指针”时,实际上指的是“指向整数的指针”。理解这一点在讨论常量指针时会很有帮助。
就像引用的类型必须与被引用对象的类型相匹配一样,指针的类型也必须与被指向对象的类型相匹配:
int main()
{
int i{ 5 };
double d{ 7.0 };
int* iPtr{ &i }; // ok: a pointer to an int can point to an int object
int* iPtr2 { &d }; // not okay: a pointer to an int can't point to a double object
double* dPtr{ &d }; // ok: a pointer to a double can point to a double object
double* dPtr2{ &i }; // not okay: a pointer to a double can't point to an int object
return 0;
}


除了我们将在下一课讨论的一个例外情况外,用字面值初始化指针是不允许的:
int* ptr{ 5 }; // not okay
int* ptr{ 0x0012FF7C }; // not okay, 0x0012FF7C is treated as an integer literal
指针和赋值
我们可以用两种不同的方式对指针进行赋值:
- 要改变指针指向的内容(通过给指针分配一个新地址)
- 要更改所指向的值(通过给解引用的指针赋一个新值)
首先,我们来看一个指针被改变,指向另一个对象的情况:
#include <iostream>
int main()
{
int x{ 5 };
int* ptr{ &x }; // ptr initialized to point at x
std::cout << *ptr << '\n'; // print the value at the address being pointed to (x's address)
int y{ 6 };
ptr = &y; // // change ptr to point at y
std::cout << *ptr << '\n'; // print the value at the address being pointed to (y's address)
return 0;
}
以上打印:

在上面的例子中,我们定义指针 ptr,用 x 的地址初始化它,然后解引用该指针以打印它指向的值(5)。接着,我们使用赋值运算符将 x 的地址 ptr 更改为 y 的地址。然后,我们再次解引用该指针以打印它指向的值(现在为 6 )。
现在我们来看看如何使用指针来改变它所指向的值:
#include <iostream>
int main()
{
int x{ 5 };
int* ptr{ &x }; // initialize ptr with address of variable x
std::cout << x << '\n'; // print x's value
std::cout << *ptr << '\n'; // print the value at the address that ptr is holding (x's address)
*ptr = 6; // The object at the address held by ptr (x) assigned value 6 (note that ptr is dereferenced here)
std::cout << x << '\n';
std::cout << *ptr << '\n'; // print the value at the address that ptr is holding (x's address)
return 0;
}
该程序会输出:

在这个例子中,我们定义指针 ptr ,用 x 的地址初始化它,然后打印 x和*ptr 的值 5。因为 *ptr 返回一个左值,我们可以将其用于赋值语句的左侧,这样就可以将 ptr 指向的值更改为 6 。然后我们再次打印 x和 ptr 的值,以表明值已按预期更新.
关键见解:
当我们不使用解引用(ptr)而直接使用指针时,我们访问的是指针所指向的地址。修改这个地址(ptr = &y)会改变指针指向的内容。
当我们解引用指针时(ptr),我们访问的是指针指向的对象。修改指针(ptr = 6;)会改变指针指向的对象的值。
指针的行为与左值引用非常相似。
指针和左值引用的行为类似。考虑以下程序:
#include <iostream>
int main()
{
int x{ 5 };
int& ref { x }; // get a reference to x
int* ptr { &x }; // get a pointer to x
std::cout << x;
std::cout << ref; // use the reference to print x's value (5)
std::cout << *ptr << '\n'; // use the pointer to print x's value (5)
ref = 6; // use the reference to change the value of x
std::cout << x;
std::cout << ref; // use the reference to print x's value (6)
std::cout << *ptr << '\n'; // use the pointer to print x's value (6)
*ptr = 7; // use the pointer to change the value of x
std::cout << x;
std::cout << ref; // use the reference to print x's value (7)
std::cout << *ptr << '\n'; // use the pointer to print x's value (7)
return 0;
}
该程序会输出:

在上面的程序中,我们首先创建了一个值为 5 的普通变量x,然后创建了一个指向该变量的左值引用 ref和一个指向该变量的指针 ptr。接下来,我们使用左值引用将 x 的值从5改为 6,并证明可以通过所有三种方法访问更新后的值。最后,我们使用解引用的指针将 x 的值从 6改为 7,并再次证明可以通过所有三种方法访问更新后的值。
因此,指针和引用都提供了一种间接访问其他对象的方法。主要区别在于,使用指针时,我们需要显式地获取要指向的地址,并且需要显式地解引用才能获取值。而使用引用时,寻址和解引用操作都是隐式进行的。
指针和引用之间还有一些其他值得一提的区别:
- 引用必须初始化,指针不需要初始化(但应该初始化)。
- 引用不是对象,指针才是。
- 引用不能被重新定位(改变为引用其他对象),而指针可以改变其指向的对象。
- 引用必须始终绑定到一个对象,指针可以指向空对象(我们将在下一课中看到一个例子)。
- 引用是“安全的”(悬空引用除外),指针本质上是危险的(我们将在下一课中讨论这一点)。
地址运算符返回一个指针
值得注意的是,取地址运算符 (&) 不会直接返回操作数的地址字面值(因为 C++ 不支持地址字面值)。相反,它返回一个指向操作数的指针(其值即为操作数的地址)。换句话说,给定变量 int x,&x 返回一个 int* 包含 x 地址的指针。
我们可以从以下例子中看到这一点:
#include <iostream>
#include <typeinfo>
int main()
{
int x{ 4 };
std::cout << typeid(x).name() << '\n'; // print the type of x
std::cout << typeid(&x).name() << '\n'; // print the type of &x
return 0;
}
在 Visual Studio 中,打印结果如下:

使用 gcc 编译器时,它会输出i(int) 和pi(指向 int 的指针)。由于 typeid().name() 的结果取决于编译器,你的编译器可能会输出不同的内容,但含义相同。

指针的大小
指针的大小取决于可执行文件编译时所用的架构——32 位可执行文件使用 32 位内存地址——因此,在 32 位机器上,指针为 32 位(4 字节)。在 64 位可执行文件中,指针为 64 位(8 字节)。请注意,无论指针指向的对象大小如何,此规律均适用。
#include <iostream>
int main() // assume a 32-bit application
{
char* chPtr{}; // chars are 1 byte
int* iPtr{}; // ints are usually 4 bytes
long double* ldPtr{}; // long doubles are usually 8 or 12 bytes
std::cout << sizeof(chPtr) << '\n'; // prints 4
std::cout << sizeof(iPtr) << '\n'; // prints 4
std::cout << sizeof(ldPtr) << '\n'; // prints 4
return 0;
}

指针的大小始终保持不变。这是因为指针本质上就是一个内存地址,而访问一个内存地址所需的位数是恒定的。
悬空指针
与悬空引用类似,悬空指针是指指向不再有效的对象(例如,已被销毁的对象)地址的指针。
解引用悬空指针(例如,为了打印指针指向的值)会导致未定义行为,因为您正在尝试访问一个不再有效的对象。
或许令人惊讶的是,该标准规定“任何其他对无效指针值的使用都具有实现定义的行为”。这意味着你可以给无效指针赋一个新值,例如 nullptr(因为这不会使用无效指针的值)。但是,任何其他使用无效指针值的操作(例如复制或递增无效指针)都会导致实现定义的行为。
关键见解:
对无效指针进行解引用会导致未定义行为。任何其他对无效指针值的使用都由具体实现决定。
以下是一个创建悬空指针的示例:
#include <iostream>
int main()
{
int x{ 5 };
int* ptr{ &x };
std::cout << *ptr << '\n'; // valid
{
int y{ 6 };
ptr = &y;
std::cout << *ptr << '\n'; // valid
} // y goes out of scope, and ptr is now dangling
std::cout << *ptr << '\n'; // undefined behavior from dereferencing a dangling pointer
return 0;
}
上述程序可能会输出:

但可能不会,因为指向的对象ptr超出了作用域,并在内部块的末尾被销毁,留下ptr悬空的指针。
结论
指针是存储内存地址的变量。可以使用解引用运算符 (*) 解引用指针,以获取其所指向地址处的值。解引用空指针、悬空指针或空指针会导致未定义行为,并可能导致应用程序崩溃。
指针比引用更灵活,但也更危险。我们将在接下来的课程中继续探讨这个问题。
测验时间
问题 1
这个程序会输出哪些值?假设 short 类型占用 2 个字节,并且是在一台 32 位机器上运行。
#include <iostream>
int main()
{
short value{ 7 }; // &value = 0012FF60
short otherValue{ 3 }; // &otherValue = 0012FF54
short* ptr{ &value };
std::cout << &value << '\n';
std::cout << value << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
*ptr = 9;
std::cout << &value << '\n';
std::cout << value << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
ptr = &otherValue;
std::cout << &otherValue << '\n';
std::cout << otherValue << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
std::cout << sizeof(ptr) << '\n';
std::cout << sizeof(*ptr) << '\n';
return 0;
}
解决方案
0012FF60
7
0012FF60
7
0012FF60
9
0012FF60
9
0012FF54
3
0012FF54
3
4
2
简单解释一下这里的 4 和 2。在 32 位机器中,指针的长度是 32 位,但 sizeof() 函数总是以字节为单位输出大小。32 位是 4 字节,所以 sizeof(ptr) 的值是 4。因为ptr 是一个指向 short 类型的指针,所以 *ptr() 的值也是 short 类型。在这个例子中,short 类型的大小是 2 字节,所以 sizeof(*ptr) 的值是 2。
问题 2
这段代码有什么问题?
int v1{ 45 };
int* ptr{ &v1 }; // initialize ptr with address of v1
int v2 { 78 };
*ptr = &v2; // assign ptr to address of v2

解决方案:
上述代码片段的最后一行无法编译。
让我们更详细地分析一下这个程序。
第一行和第四行包含标准的变量定义以及初始化值。没什么特别的。
第二行中的星号是指针声明语法的一部分(并非解引用)。所以这一行的意思是“将v1地址赋值给ptr”。这没问题,因为指针保存的是地址。
第五行中的星号表示解引用,用于获取指针指向的值。所以这行代码的意思是“将v2地址赋值给指针ptr指向的值”。指针ptr指向的值 45是一个整数。因此,这个赋值语句毫无意义——你不能将地址赋值给一个整数!
第五行应该是:
ptr = &v2;
这样就正确地将 v2 的地址赋给了指针 ptr。

浙公网安备 33010602011771号