揭开内存的"门牌号" —— 指针的定义与操作、指针与数组的等价关系、指针运算、指针传参,一次讲透。
用"内存寻宝"的故事建立地址的直觉,用门牌号/快递单/寻宝图/通讯录四大类比理解指针,最后给出指针的定义和核心口诀。
小强写了一个程序,在内存中放了一个整数 42,但他忘了这个数放在哪里。于是他画了一张"内存地图",在地图上标记了每个格子的编号(地址),然后写道:"42 在地址 0x7ffd 这个格子里。"
这就像你住在 阳光小区 3 栋 501 室——"3 栋 501 室"就是你的"内存地址",而住在那个人就是你的"变量值"。
在计算机中,每个变量都存放在内存的某个位置,这个位置有一个唯一的编号,叫做内存地址(Memory Address)。
指针(Pointer)就是专门用来存储内存地址的变量。有了指针,程序就能"找到"另一个变量的位置,甚至修改它的值。
一栋楼里每间房都有门牌号,快递员按门牌号找到你家。内存地址就像门牌号,指针就是"写着门牌号的小纸条"。
地址 = 门牌号,指针 = 纸条快递单上写着收件地址,快递员按地址把包裹送到。指针变量里存着另一个变量的地址,解引用就是"按地址取包裹"。
*p = 按地址取包裹寻宝图上写着"往东走 10 步到宝箱",指针运算就是"按步数移动位置"。p+1 就是"往后走一步",指向下一个元素。
p+n = 往后移 n 步通讯录里存着每个人的电话号码,你通过号码找到人。数组名就是首地址,*(a+i) 就是"通过第 i 个地址找到值"。
a[i] == *(a+i)内存可以被想象成一个巨大的格子数组,每个格子存 1 个字节(8 位),每个格子都有一个唯一的编号——地址。
当 C++ 编译器看到 int x = 42; 时,它会在内存中找一个空位放 42,比如地址 0x7ffd。此后程序可以通过这个地址找到 42。
深入指针的定义语法、三种操作符、指针与数组的等价关系、指针运算、指针传参,配合 5 个综合案例逐行讲解。
指针的定义格式为:类型 *指针名;
三种操作符速查:
| 操作符 | 名称 | 作用 | 示例 |
|---|---|---|---|
| & | 取地址 | 获取变量的内存地址 | int *p = &x; |
| * | 解引用 | 通过地址访问/修改值 | *p = 100; |
| sizeof | 求大小 | 获取指针变量自身大小 | sizeof(p) → 8 (64位) |
// 1. 定义普通变量
int x = 42;
// 2. 定义指针并指向 x
int *p = &x; // p 存的是 x 的地址
// 3. 解引用:通过指针读取值
cout << *p; // 输出 42
// 4. 解引用:通过指针修改值
*p = 100;
cout << x; // 输出 100(x 被修改了)
// 5. 空指针
int *q = nullptr; // q 不指向任何变量
42100
指针变量本身的大小只与系统位数有关,与指向的类型无关:
| 系统 | 指针大小 | 说明 |
|---|---|---|
| 32 位系统 | 4 字节 | 地址用 32 位(4 字节)表示 |
| 64 位系统 | 8 字节 | 地址用 64 位(8 字节)表示 |
int a = 10;
double b = 3.14;
int *pa = &a;
double *pb = &b;
cout << sizeof(pa) << endl; // 8 (64位)
cout << sizeof(pb) << endl; // 8 (64位)
注意:虽然 int* 和 double* 大小一样,但指针的步长不同——p+1 移动的字节数等于指向类型的大小。
数组名就是首地址!这是指针与数组最核心的关系。
int a[5] = {10, 20, 30, 40, 50};
// 方式1:下标访问
cout << a[2]; // 输出 30
// 方式2:指针访问(完全等价)
cout << *(a + 2); // 输出 30
// 用指针遍历数组
int *p = a; // p 指向 a[0]
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " ";
}
// 输出: 10 20 30 40 50
指针支持三种运算:自增/自减(p++)、加减整数(p+n)、指针相减(p1-p2)。
步长规则:p+n 实际移动的字节数 = n * sizeof(指针指向的类型)。
int a[5] = {10, 20, 30, 40, 50};
int *p = a; // p 指向 a[0],地址 0x1000
p++; // p 指向 a[1],地址 0x1004(+4 字节)
cout << *p; // 输出 20
p += 2; // p 指向 a[3],地址 0x100C
cout << *p; // 输出 40
int *q = &a[4];
cout << (q - p); // 输出 1(相差 1 个 int)
20401
C++ 默认是值传递(函数内修改不影响原变量),用指针可以实现地址传递(函数内修改影响原变量)。
void swap_val(int a, int b) {
int t = a; a = b; b = t;
} // 原变量不变!
void swap_ptr(int *a, int *b) {
int t = *a; *a = *b; *b = t;
} // 原变量被交换!
void swap_ptr(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
int main() {
int x = 3, y = 5;
swap_ptr(&x, &y); // 传地址
cout << x << " " << y; // 输出 5 3
}
5 3
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10, b = 20;
swap(&a, &b);
cout << a << " " << b; // 20 10
}
20 10
int main() {
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // p 指向首元素
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += *(p + i); // *(p+i) 等价 a[i]
}
cout << sum; // 输出 15
}
15
int* findMax(int a[], int n) {
int *maxPtr = a; // 假设第一个最大
for (int *p = a + 1; p < a + n; p++) {
if (*p > *maxPtr) maxPtr = p;
}
return maxPtr; // 返回最大值的地址
}
void bubbleSort(int a[], int n) {
for (int i = 0; i < n-1; i++) {
for (int *j = a; j < a + n - i - 1; j++) {
if (*j > *(j+1)) {
int t = *j; *j = *(j+1); *(j+1) = t;
}
}
}
}
int* findMax(int a[], int n) {
int *maxPtr = a;
for (int *p = a + 1; p < a + n; p++) {
if (*p > *maxPtr) maxPtr = p;
}
return maxPtr;
}
int main() {
int a[5] = {3, 7, 2, 9, 5};
int *m = findMax(a, 5);
cout << *m; // 输出 9
}
9
通过 3 道综合题巩固指针用法,每题含完整代码、逐行解析和运行结果。
题目:阅读以下代码,写出输出结果。
int a[] = {1, 2, 3, 4, 5};
int *p = a;
cout << *++p << " ";
cout << *p++ << " ";
cout << *p << " ";
2 2 3
题目:用两个指针(头指针和尾指针)将数组逆序排列。
void reverse(int a[], int n) {
int *left = a; // 头指针
int *right = a + n - 1; // 尾指针
while (left < right) {
int t = *left; *left = *right; *right = t;
left++; right--;
}
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
reverse(a, 5);
for (int i = 0; i < 5; i++)
cout << a[i] << " ";
}
5 4 3 2 1
题目:用指针遍历字符串,统计其长度(不含 '\0')。
int strLen(const char *s) {
int len = 0;
while (*s != '\0') {
len++; s++; // 逐个字符遍历
}
return len;
}
int main() {
char str[] = "NOI2024";
cout << strLen(str); // 输出 7
}
7
回顾本课核心要点,用一张总结表快速复习,并拓展指针遍历数组的万能模板。
| 知识点 | 语法/要点 | 说明 |
|---|---|---|
| 指针定义 | int *p; | 声明一个指向 int 的指针 |
| 取地址 | &x | 获取变量 x 的内存地址 |
| 解引用 | *p | 通过指针 p 访问/修改指向的值 |
| 指针赋值 | p = &x; | 让 p 指向变量 x |
| 数组名=首地址 | a == &a[0] | 数组名就是第一个元素的地址 |
| 指针访问数组 | *(a+i) == a[i] | 指针访问与下标访问完全等价 |
| 指针自增 | p++ | 指向下一个元素(步长=sizeof(类型)) |
| 指针传参 | swap(&a, &b) | 地址传递可修改原变量 |
| 空指针 | int *p = nullptr; | 不指向任何变量的安全指针 |
| 指针大小 | sizeof(p) → 8 | 64位系统下所有指针均为 8 字节 |
void processArray(int a[], int n) {
for (int *p = a; p < a + n; p++) {
// *p 就是当前元素的值
cout << *p << " ";
}
}
// 调用方式
int main() {
int data[] = {3, 1, 4, 1, 5};
processArray(data, 5); // 输出 3 1 4 1 5
}
for (int *p = a; p < a+n; p++) —— "从开头走到末尾,*p 就是当前值"
1. 如果指针指向了数组范围之外的内存,会发生什么?(答案:未定义行为,可能读到垃圾值或程序崩溃)
2. 为什么 C++ 中数组传参时要用额外参数 n 表示长度?(答案:数组名退化为指针后丢失了大小信息)
3. 指针和引用有什么区别?(答案:指针可以重新指向、可以为空;引用必须在定义时绑定、不能为空)
检测你对指针的掌握程度,5 大易错点 + 速查表 + 知识点笔记,帮你查漏补缺。
int *p; // 未初始化!
cout << *p; // 崩溃!
int *p = nullptr; // 安全
if (p) { cout << *p; }
int *p = new int(42);
delete p;
cout << *p; // 悬空!
int *p = new int(42);
delete p;
p = nullptr; // 置空
int *p = nullptr;
cout << *p; // 段错误!
if (p != nullptr) {
cout << *p;
}
int x = 42;
double *p = &x; // 类型不匹配!
int x = 42;
int *p = &x; // 类型一致
int a[5];
a++; // 错误!a 是常量
int a[5];
int *p = a; // 用指针变量来移动
p++; // 合法
| 操作 | 语法 | 示例 | 结果 |
|---|---|---|---|
| 定义指针 | int *p; | int x=5; int *p=&x; | p 存 x 的地址 |
| 解引用读 | *p | cout << *p; | 输出 5 |
| 解引用写 | *p = 10; | *p = 10; | x 变为 10 |
| 地址递增 | p++ | p = a; p++; | 指向下一个元素 |
| 指针访问数组 | *(a+i) | *(a+2) == a[2] | 等价下标访问 |
| 指针传参 | func(&x) | void f(int*p){*p=1;} | 可修改原变量 |
| 空指针 | nullptr | int *p = nullptr; | 安全的不指向 |
指针就是存地址的变量。int* p 表示 p 存的是 int 类型变量的地址。
数组名 a 就是 &a[0](首地址)。a[i] 和 *(a+i) 完全等价,编译器会自动转换。
p+1 移动 sizeof(指向类型) 字节。int* 步长 4,double* 步长 8。
值传递拷贝值,地址传递拷贝地址。要修改原变量必须传地址(指针或引用)。
指针初始化用 nullptr,解引用前检查非空,delete 后立刻置空。
指针遍历数组比下标更快(编译器优化),在信息学竞赛中常用于字符串和数组处理。
通过分层练习巩固指针知识,从基础到挑战,每题含可展开答案。
完成以下练习,检验你对指针的掌握程度。点击"展开答案"查看参考解法。
int a[] = {10, 20, 30};
int *p = a;
cout << *p << " ";
p++; *p += 5;
cout << a[1];
输入两个长度为 n 的数组 a 和 b,用指针遍历将它们对应元素相加,结果存到数组 c 中。
int a[3] = {1, 2, 3};
int b[3] = {4, 5, 6};
int c[3];
int *pa = a, *pb = b, *pc = c;
for (int i = 0; i < 3; i++) {
*(pc + i) = *(pa + i) + *(pb + i);
}
// c = {5, 7, 9}
给定一个字符数组,用头尾指针交换的方式原地反转字符串。
void reverseStr(char s[]) {
int n = strlen(s);
char *left = s, *right = s + n - 1;
while (left < right) {
char t = *left; *left = *right; *right = t;
left++; right--;
}
}
double x = 3.14;
double *p = &x;
思路:指针类型要和变量一致:double *p = &x;,& 是取地址符。
易错:用 int *p 去指向 double 变量,类型不匹配编译报错。
cout << *(p + 3); // 输出 4
思路:数组名就是首地址,p + 3 表示地址向后移 3 个元素,*(p+3) 等价 a[3],即 4。
易错:把 *(p+3) 写成 *p+3——后者是“p 指的值再加 3”,含义完全不同。
int *p = a + 2; // 指向 a[2]=30
p++; // 指向 a[3]
cout << *p; // 输出 40
思路:p 指向 a[2],p++ 让地址向后移一个元素指向 a[3],*p 就是 40。
易错:以为 p++ 是把 p 指的值加 1(30 变 31)——p++ 移动的是地址,不是值。
void doubleVal(int *p) {
*p = *p * 2;
}
// 调用:doubleVal(&x);
思路:传 &x 进去,函数内通过 *p = *p * 2 修改原变量。
易错:调用时写成 doubleVal(x) 而不是 doubleVal(&x),类型不匹配编译报错。
void findMinMax(int a[], int n) {
int *maxP = a, *minP = a;
for (int *p = a + 1; p < a + n; p++) {
if (*p > *maxP) maxP = p;
if (*p < *minP) minP = p;
}
cout << "Max:" << *maxP
<< " Min:" << *minP;
}
思路:用两个指针 maxP、minP 记录最大、最小元素的地址,遍历比较 *p 与 *maxP、*minP。
易错:比较时写成 p > maxP——那是比较地址,应该比较 *p > *maxP。
int compare(int a[], int b[], int n) {
int cnt = 0;
for (int *pa = a, *pb = b; pa < a+n; pa++, pb++) {
if (*pa == *pb) cnt++;
}
return cnt;
}
思路:两个指针 pa、pb 同步前进,逐位比较 *pa == *pb 并计数。
易错:两个数组长度不同时只判 pa < a+n 会越界读 b,要同时保证两个指针都不越界。
void selectSort(int a[], int n) {
for (int *i = a; i < a+n-1; i++) {
int *minP = i;
for (int *j = i+1; j < a+n; j++) {
if (*j < *minP) minP = j;
}
int t = *i; *i = *minP; *minP = t;
}
}
思路:每轮用指针扫描剩余部分找最小值地址 minP,再把 *i 与 *minP 交换。
易错:找到了 minP 却忘了交换,或者交换的是指针变量本身而不是它们指向的值。
int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
for (int i = 0; i < 3; i++) {
for (int *p = a[i]; p < a[i]+4; p++) {
cout << *p << " ";
}
cout << endl;
}
思路:a[i] 是第 i 行的首地址,行内用 int *p 从 a[i] 走到 a[i]+4 逐个输出。
易错:直接写 int *p = a 类型不匹配——二维数组名是“指向一行的指针”,取行要用 a[i]。
int n;
cin >> n;
int *a = new int[n];
double sum = 0;
for (int *p = a; p < a+n; p++) {
cin >> *p;
sum += *p;
}
cout << sum / n;
delete[] a; // 记得释放!
思路:new int[n] 按需分配数组,指针遍历输入并累加求平均,用完 delete[] a 释放。
易错:忘了 delete[] a 造成内存泄漏;写成 delete a(不带 [])对数组也是错的。
const char *names[] = {"NOI", "CSP", "IOI", "USACO"};
int n = 4;
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (strcmp(names[j], names[j+1]) > 0) {
const char *t = names[j];
names[j] = names[j+1];
names[j+1] = t;
}
思路:排序交换的是指针本身(names[j] 与 names[j+1] 对调),比较字符串内容必须用 strcmp。
易错:直接用 names[j] > names[j+1] 比较——比的是地址大小,不是字典序。
int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}};
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += *(*(a + i) + i); // 主对角线
}
cout << sum; // 1+5+9 = 15
思路:*(*(a+i)+i) 就是 a[i][i],把主对角线元素累加得 1+5+9=15(已验证)。
易错:以为 a+i 是“第 i 个元素”——二维数组名加 1 移动的是一整行。
int main() {
int n;
cin >> n;
int *scores = new int[n];
for (int *p = scores; p < scores+n; p++)
cin >> *p;
int *maxP = scores, *minP = scores;
double sum = 0;
for (int *p = scores; p < scores+n; p++) {
if (*p > *maxP) maxP = p;
if (*p < *minP) minP = p;
sum += *p;
}
cout << "Max:" << *maxP
<< " Min:" << *minP
<< " Avg:" << sum/n;
delete[] scores;
}
思路:new 动态开 n 个成绩,指针一边遍历一边维护最大、最小指针和总和,最后算平均。
易错:忘了 delete[] 释放内存;求平均时用整数除法丢了小数部分。