NOI C++ 课程体系 / 语法入门 / 第7讲
CSP-J 入门级 · 语法入门 · 指针专题

C++ 指针入门与指针访问数组 完全指南

揭开内存的"门牌号" —— 指针的定义与操作、指针与数组的等价关系、指针运算、指针传参,一次讲透。

6 大教学模块 3 种指针操作符 5 个精讲案例 5 大易错点 1 张速查表 15 道分层练习
本讲 · 第 21/56 讲指针入门与指针访问数组
模块 01

情景导航

用"内存寻宝"的故事建立地址的直觉,用门牌号/快递单/寻宝图/通讯录四大类比理解指针,最后给出指针的定义和核心口诀。

📖 故事引入:小强的"内存寻宝"

小强写了一个程序,在内存中放了一个整数 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。

内存:[0x1000] [0x1001] [0x1002] [0x1003] ...
int x:[42] 放在 0x1000 ~ 0x1003
指针 p:存的是 0x1000(x 的地址)
地址编号 内存位置 变量名 存储的值
🔑 核心口诀
指针入门口诀 &x 取地址,*p 解引用;
int *p 声明指针,p = &x 指向谁;
*p 改值改内存,p++ 往后走一步;
数组名即首地址,*(a+i) 等价 a[i]。
模块 02

知识探究

深入指针的定义语法、三种操作符、指针与数组的等价关系、指针运算、指针传参,配合 5 个综合案例逐行讲解。

🚦 指针定义语法

指针的定义格式为:类型 *指针名;

int * p = & x
数据类型 指针声明符 指针变量名 取地址运算符

三种操作符速查:

操作符名称作用示例
&取地址获取变量的内存地址int *p = &x;
*解引用通过地址访问/修改值*p = 100;
sizeof求大小获取指针变量自身大小sizeof(p) → 8 (64位)
⚖ 指针基本操作
C++指针基本操作示例
// 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
关键理解:&x 拿到 x 的门牌号,*p 按门牌号找到房间里的东西。修改 *p 就是直接改内存里的值。
⚖ 指针大小

指针变量本身的大小只与系统位数有关,与指向的类型无关:

系统指针大小说明
32 位系统4 字节地址用 32 位(4 字节)表示
64 位系统8 字节地址用 64 位(8 字节)表示
C++sizeof 验证
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}
a = 0x1000(首地址) a+1 = 0x1004 a+2 = 0x1008 ...
a[0] = 10 a[1] = 20 a[2] = 30 ...
核心等价关系:a[i] == *(a + i)
数组名 = 首地址 地址递增 数组元素 指针访问
C++数组的两种访问方式对比
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(指针指向的类型)。

C++指针运算演示
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
📂 指针传参(值传递 vs 地址传递)

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;
}  // 原变量被交换!
C++完整演示
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
📂 综合案例

案例 1:指针交换两个数

C++swap 函数
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

案例 2:指针遍历数组求和

C++指针遍历数组
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

案例 3:指针找数组最值

C++指针求最大值
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;  // 返回最大值的地址
}

案例 4:指针实现冒泡排序

C++指针排序
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;
            }
        }
    }
}

案例 5:函数返回指针

C++返回指向最大元素的指针
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
模块 03

实践应用

通过 3 道综合题巩固指针用法,每题含完整代码、逐行解析和运行结果。

⛽ 题目 1:指针追踪输出

题目:阅读以下代码,写出输出结果。

C++指针追踪
int a[] = {1, 2, 3, 4, 5};
int *p = a;
cout << *++p << " ";
cout << *p++ << " ";
cout << *p << " ";
查看解析与答案
解析:
1. p 初始指向 a[0](值 1)
2. *++p:先 p++(p 指向 a[1]),再解引用 → 输出 2
3. *p++:先解引用 *p(值 3),再 p++(p 指向 a[2]) → 输出 3
4. *p:p 当前指向 a[2](值 3) → 输出 3
运行结果
2 2 3
⛽ 题目 2:指针实现数组逆序

题目:用两个指针(头指针和尾指针)将数组逆序排列。

C++指针逆序数组
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
思路:头指针从左往右走,尾指针从右往左走,相遇时交换完成。用指针操作比下标更直观地体现"面对面"的过程。
⛽ 题目 3:指针实现字符串长度

题目:用指针遍历字符串,统计其长度(不含 '\0')。

C++指针求字符串长度
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
模块 04

总结提升

回顾本课核心要点,用一张总结表快速复习,并拓展指针遍历数组的万能模板。

📊 核心要点总结
知识点语法/要点说明
指针定义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) → 864位系统下所有指针均为 8 字节
📦 拓展案例:指针遍历数组万能模板
C++指针遍历数组模板
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. 指针和引用有什么区别?(答案:指针可以重新指向、可以为空;引用必须在定义时绑定、不能为空)

模块 05

学习检测

检测你对指针的掌握程度,5 大易错点 + 速查表 + 知识点笔记,帮你查漏补缺。

⚠ 5 大易错点
ERROR 01

野指针(未初始化的指针)

错误写法
int *p;    // 未初始化!
cout << *p;  // 崩溃!
正确写法
int *p = nullptr;  // 安全
if (p) { cout << *p; }
ERROR 02

悬空指针(指向已释放的内存)

错误写法
int *p = new int(42);
delete p;
cout << *p;  // 悬空!
正确写法
int *p = new int(42);
delete p;
p = nullptr;  // 置空
ERROR 03

解引用空指针

错误写法
int *p = nullptr;
cout << *p;  // 段错误!
正确写法
if (p != nullptr) {
    cout << *p;
}
ERROR 04

指针类型不匹配

错误写法
int x = 42;
double *p = &x;  // 类型不匹配!
正确写法
int x = 42;
int *p = &x;  // 类型一致
ERROR 05

数组名是常量指针

错误写法
int a[5];
a++;  // 错误!a 是常量
正确写法
int a[5];
int *p = a;  // 用指针变量来移动
p++;  // 合法
📊 指针速查表
操作语法示例结果
定义指针int *p;int x=5; int *p=&x;p 存 x 的地址
解引用读*pcout << *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;}可修改原变量
空指针nullptrint *p = nullptr;安全的不指向
📌 知识点笔记
📝 指针的本质

指针就是存地址的变量。int* p 表示 p 存的是 int 类型变量的地址。

📦 数组与指针

数组名 a 就是 &a[0](首地址)。a[i] 和 *(a+i) 完全等价,编译器会自动转换。

⛽ 指针运算步长

p+1 移动 sizeof(指向类型) 字节。int* 步长 4,double* 步长 8。

📂 指针传参

值传递拷贝值,地址传递拷贝地址。要修改原变量必须传地址(指针或引用)。

⚠ 安全原则

指针初始化用 nullptr,解引用前检查非空,delete 后立刻置空。

📏 奥赛技巧

指针遍历数组比下标更快(编译器优化),在信息学竞赛中常用于字符串和数组处理。

模块 06

练习作业

通过分层练习巩固指针知识,从基础到挑战,每题含可展开答案。

课后练习

练习作业

完成以下练习,检验你对指针的掌握程度。点击"展开答案"查看参考解法。

⛽ 总结练习(3 题)
基础 指针基础 第 1 题

阅读以下代码,写出输出结果

C++指针追踪
int a[] = {10, 20, 30};
int *p = a;
cout << *p << " ";
p++; *p += 5;
cout << a[1];
展开答案
答案:10 25
p 初始指向 a[0],*p 输出 10。p++ 后 p 指向 a[1],*p+=5 将 a[1] 从 20 改为 25。
提高 指针运算 第 2 题

用指针实现两个数组对应元素相加

输入两个长度为 n 的数组 a 和 b,用指针遍历将它们对应元素相加,结果存到数组 c 中。

展开答案
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}
挑战 指针综合 第 3 题

用指针实现字符串反转(原地修改)

给定一个字符数组,用头尾指针交换的方式原地反转字符串。

展开答案
C++指针反转字符串
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--;
    }
}
📦 A 组 · 基础题(4 题)
基础指针定义

定义一个指向 double 的指针 p,让它指向变量 x = 3.14

展开答案
double x = 3.14;
double *p = &x;

思路:指针类型要和变量一致:double *p = &x;,& 是取地址符。

易错:用 int *p 去指向 double 变量,类型不匹配编译报错。

基础指针访问

已知 int a[5] = {1,2,3,4,5}; int *p = a; 写出访问 a[3] 的指针表达式

展开答案
cout << *(p + 3);  // 输出 4

思路:数组名就是首地址,p + 3 表示地址向后移 3 个元素,*(p+3) 等价 a[3],即 4。

易错:把 *(p+3) 写成 *p+3——后者是“p 指的值再加 3”,含义完全不同。

基础指针运算

指针 p 指向 a[2],p++ 后 *p 的值是多少?(a = {10,20,30,40,50})

展开答案
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),类型不匹配编译报错。

📂 B 组 · 提高题(4 题)
提高数组遍历

用指针遍历数组,找出最大值和最小值及其位置

展开答案
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]。

💡 C 组 · 挑战题(4 题)
挑战指针与动态内存

用 new 动态创建数组,用指针遍历输入并求平均值

展开答案
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 移动的是一整行。

挑战综合应用

用指针实现一个简易的学生成绩管理系统(输入 n 个学生的成绩,输出最高分、最低分和平均分)

展开答案
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[] 释放内存;求平均时用整数除法丢了小数部分。