第7章 指针基础
第7章 指针基础
- 指针是什么,如何声明和使用指针
- 指针与数组、函数的关系
- 指针运算与空指针的安全使用
想象一下,你有一张写着朋友家地址的纸条。纸条本身不是房子,但通过它你能找到房子。在C++中,指针就是这张“地址纸条”,它存储的是另一个变量在内存中的位置(地址),而不是数据本身。理解指针是解锁C++高效编程和复杂数据结构(如链表、树)的关键第一步。
7.1 指针是什么:从地址到变量
每个变量在内存中都有一个唯一的地址,就像每个房子有唯一的门牌号。指针变量专门用来存放这些地址。声明指针时,需要在类型后加上星号(*)。
int num = 42; // 一个普通的整型变量
int *pNum = # // pNum是一个“指向int的指针”,用&取num的地址赋值给它
关键操作符
- &(取地址运算符):获取变量的内存地址。例如
&num。 - *(解引用运算符):通过指针访问它指向的地址里存储的值。例如
*pNum得到 42。
下面这个表格清晰地展示了普通变量与指针变量的区别和联系:
| 元素 | 描述 | 示例代码 | 类比 |
|---|---|---|---|
| 变量 (num) | 存储实际数据的内存单元 | int num = 42; | 房子本身 |
| 地址 (&num) | 变量在内存中的位置 | cout << # (输出如0x7ffeed6a5c) | 房子的门牌号 |
| 指针变量 (pNum) | 存储地址的变量 | int *pNum = # | 写着门牌号的纸条 |
| 解引用 (*pNum) | 通过指针访问其指向的值 | *pNum = 100; (将num改为100) | 根据纸条找到房子并进去 |
我在早期项目中踩过一个坑:声明了指针却忘记初始化。未初始化的指针指向一个随机的、未知的内存地址,解引用它会导致程序崩溃(段错误)。声明指针后,立即将其指向一个有效的地址或设为nullptr,这是一个必须养成的好习惯。
7.2 指针的声明、初始化与使用
指针的声明语法是 类型* 指针变量名。星号(*)靠近类型还是变量名是风格问题,但必须保持一致。我推荐 int* p; 这种写法,强调“指向int的指针”是一个类型。
常见误区
- 混淆声明中的*和解引用中的*:
int *p;里的*是声明符,表示p是指针。*p = 5;里的*是运算符,表示操作p指向的内存。 - 对未初始化的指针进行解引用:这是导致程序崩溃的最常见原因之一。如果你看到
Segmentation fault (core dumped)这类错误,首先检查指针是否有效。 - 类型不匹配:
double d = 3.14; int *p = &d;是错误的。指针类型必须与其指向的变量类型严格一致(void*指针除外)。
正确的初始化流程如下:
// 正确示例
int value = 10;
int* ptr1 = &value; // 方式1:指向已有变量
int* ptr2 = nullptr; // 方式2:初始化为空(C++11推荐)
// int* ptr3; // 危险!未初始化,绝对避免
if (ptr1 != nullptr) { // 使用前检查是好习惯
*ptr1 = 20; // 解引用并修改,value现在变为20
int another = *ptr1; // 解引用获取值,another为20
}
编者提示:关于 nullptr(C++11引入)与旧的 NULL 宏的详细对比,以及为什么现代C++推荐使用 nullptr,我们将在第31章《C++11/14新特性》中深入探讨。
7.3 指针与数组的紧密关系
数组名在大多数情况下会被编译器自动转换为指向其首元素的指针。这是C/C++中一个极其重要且常用的特性。
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // 等价于 int* p = &arr[0];
// 通过指针访问数组元素
int first = *p; // first = 1,访问arr[0]
int second = *(p + 1); // second = 2,访问arr[1]
*(p + 2) = 30; // arr[2] 被修改为 30
这里引入了指针算术:对指针进行加/减整数操作,移动的距离是以其指向类型的大小为单位的。对于 int* p,p+1 意味着地址增加 sizeof(int) 个字节(通常是4字节),从而指向下一个int。
指针 vs. 数组名
虽然数组名在很多地方可以当作指针用,但有一个关键区别:数组名是常量指针,其值(地址)不可改变。而指针变量本身的值可以改变。
int arr[3];
int* p = arr;
p++; // 合法:p现在指向arr[1]
// arr++; // 非法!数组名arr是常量,不能自增。
7.4 指针作为函数参数(传址调用)
回忆第6章的函数参数传递,普通的传值是“复制一份”。如果想在函数内部修改外部变量的值,就需要传递该变量的地址,即使用指针参数。这被称为“传址调用”。
// 一个交换两个整数值的函数
void swap(int* a, int* b) {
int temp = *a; // 解引用a,获取其指向的值
*a = *b; // 将b指向的值,赋给a指向的内存
*b = temp; // 将temp的值,赋给b指向的内存
}
int main() {
int x = 5, y = 10;
swap(&x, &y); // 传递x和y的地址
// 现在 x == 10, y == 5
}
实际开发中,对于需要修改的简单内置类型(如int, double)或大型结构体,传递指针比传递整个对象的副本更高效。但你必须时刻注意指针的有效性,并在函数内做好空指针检查。
场景:遍历数组
- 使用指针算术
p++遍历数组比用下标arr[i]在某些历史代码中更常见。 - 性能上在现代编译器优化后差异极小,但指针形式能更直观体现“移动”的概念。
场景:函数返回多个值
- C++函数只能直接返回一个值。
- 通过传递多个指针参数,函数可以将结果写回调用者提供的变量中,实现“返回”多个值的效果。
场景:操作动态内存
- 这是指针的核心用途之一。
- 使用
new运算符在堆上分配内存,返回的是该内存块的地址,必须用指针来接收和管理。 - 这将在第9章《动态内存管理》中详细展开。
动手试一试
1. 指针追踪练习:在纸上画出以下代码执行过程中,变量、指针及其值的内存关系图。
int a = 1, b = 2;
int *p1 = &a, *p2 = &b;
*p1 = *p1 + *p2; // a 变成多少?
p2 = p1;
*p2 = 10; // 现在 a 和 b 的值分别是多少?
2. 编写一个函数:名为 findMax,接收一个整型数组和其长度(int)作为参数,返回数组中最大值的地址(即一个 int*)。在main函数中测试它。
3. 调试挑战:下面这段代码有什么问题?如何修正?
void setToZero(int* ptr) {
ptr = new int(0);
}
int main() {
int value = 42;
int* p = &value;
setToZero(p);
cout << *p; // 期望输出0,实际呢?
}
检验你的理解
- 给定
double d = 3.14; double *pd = &d;,表达式*pd和&d的类型和值分别是什么? - 如果指针
int* ptr = nullptr;,执行*ptr = 100;会发生什么?为什么? - 写出通过指针
int* p访问整型数组arr第三个元素(下标为2)的两种不同表达式。 - “传址调用”相比“传值调用”的主要优势是什么?它有什么潜在风险?
本章小结
- 指针是存储内存地址的变量,通过
&取地址,通过*解引用访问目标数据。 - 指针必须初始化,应指向有效内存地址或初始化为
nullptr,对无效指针解引用会导致程序崩溃。 - 数组名可视为指向首元素的常量指针,指针算术(p++)是基于所指向类型的大小进行内存地址移动。
- 指针作为函数参数可以实现“传址调用”,允许函数修改外部变量的值,也用于高效传递大型数据。
- 理解指针与内存地址的关系,是后续学习动态内存分配、数据结构以及高级C++特性的基石。
微信扫码关注
抖音扫码关注