【GESP】C++四级考试大纲知识点梳理, (1) 指针
GESP C++四级官方考试大纲中,共有11条考点,本文针对第一条考点进行分析介绍。之前对1-3级大纲知识点的梳理思路是仅梳理编程基础知识考纲条目,对于C++编程本身语法和规则相关考纲要求,因为网上可查看的信息非常多,所以不做梳理。但目前来看,一方面系统梳理有利于孩子系统学习,另一方面感觉网上其他学习的同学也习惯针对考虑进行专项搜索,因此从4级开始,我尽量逐条整理考纲相关知识点,便于孩子针对性学习。
(1)理解 C++指针类型的概念,掌握指针类型变量的定义、赋值、解引用。
一、指针的设计目的(为什么需要指针?)
1.1 设计目的
操作内存地址:
- C++ 是底层语言,需要具备操作内存的能力,而指针是访问/操作内存地址的唯一合法方式。
- 举个例子:就像我们需要知道朋友家的地址才能去找他一样,程序也需要知道数据存放的内存地址才能访问数据。指针就相当于存储这个”地址”的记事本。
支持间接访问(间接赋值):
- 能通过指针修改其它变量的值,尤其在函数调用时传递地址,实现参数共享。
比如下面这段代码:
1 2 3 4 5 6
void modifyValue(int* ptr) { *ptr = 100; // 通过指针修改原变量的值 } int num = 10; modifyValue(&num); // num 的值被改为 100
这里
num
的地址通过参数传给了函数,函数内部就可以通过这个地址(指针)直接修改num
的值,而不需要返回新值。
实现动态内存管理:
- 和
new
/delete
搭配使用,能手动控制内存的申请和释放。 - 在 C++ 中,
new
/delete
操作符需要和指针配合使用,因为:new
在堆上分配内存并返回该内存的地址,只有指针才能存储这个地址- 没有指针存储地址,就无法访问或释放这块内存
- 其他变量类型只能存储栈上的数据,不能直接操作堆内存
- 举个生活中的例子:
- 指针就像收据,记录了你租房的具体位置(内存地址)
new
相当于租房,但必须把房子地址(返回的内存地址)记在收据(指针)上delete
相当于退房,但必须出示收据(指针)才知道退哪间房- 如果丢了收据(指针),这间房就成了无人认领的”内存泄漏”
- 和
实现复杂数据结构:
- 链表、树、图等数据结构本质上都依赖指针实现节点之间的连接。
- 比如在链表中,每个节点都包含数据和一个指向下一个节点的指针,通过这些指针就能把所有节点串联成一个完整的数据结构。没有指针就无法实现这种灵活的数据结构。
提高性能:
- 指针传递的是地址,避免值传递的大量拷贝,提高效率。
举个例子:假设有一个很大的数组(比如 1GB),如果用值传递的方式把整个数组传给函数,就需要复制这 1GB 的数据,既耗时又占用大量内存。但如果传递数组的指针,只需要复制一个地址(通常是 4 或 8 字节),函数就能直接操作原数组,效率提升明显。代码对比如下:
1 2 3 4 5 6 7 8 9
// 值传递方式 - 需要复制整个数组 void processArray1(vector<int> arr) { /*...*/ } // 指针传递方式 - 只复制地址 void processArray2(vector<int>* arr) { /*...*/ } vector<int> bigArray(1000000000); // 1GB 数组 processArray1(bigArray); // 复制 1GB 数据 processArray2(&bigArray); // 只复制一个地址
二、指针变量的定义
2.1 使用方式
- 声明一个指针变量,用于指向某种类型的数据。
- 比如
int*
是指向int
类型变量的指针;double*
是指向double
类型变量的指针。
2.2 指针变量定义规则
基本语法:
1 2 3
类型名* 指针变量名; // 或 类型名 *指针变量名;
两种写法都正确,但推荐第一种,因为更清晰地表明这是一个指针类型。
- 命名规则:
- 遵循变量命名规则
- 建议使用
p
或ptr
作为前缀,表明这是指针 - 例如:
pCount
、ptrNode
- 初始化建议:
- 定义指针时最好立即初始化
- 如果暂时不知道指向哪里,建议初始化为
nullptr
例如:
1 2
int* p = nullptr; // 好的做法 int* p; // 不推荐,未初始化的指针很危险
- 多个指针声明:
- 每个指针变量名前都需要
*
例如:
1 2
int* p1, * p2; // p1 和 p2 都是指针 int* p1, q; // p1 是指针,q 是普通 int 变量
- 每个指针变量名前都需要
- const 修饰:
const int* p
- 指针指向的值不能改int* const p
- 指针本身不能改const int* const p
- 都不能改
- 类型匹配:
- 指针类型必须与指向的变量类型匹配
例如:
1 2 3
int x = 10; int* p = &x; // 正确 double* q = &x; // 错误:类型不匹配
2.3 底层原理
- 指针本质上是一个整数,它存储的是另一个变量的内存地址。
- 指针本身也有自己的地址,和它指向的地址是两个不同的概念。
编译器通过指针的类型来决定:
- 解引用时要读多少字节(int = 4 字节,double = 8 字节)
- 指针加减步长是多少(如
p + 1
步长和类型相关)
三、指针变量的赋值
3.1 指针变量赋值的基本方式
指针变量的赋值主要有以下几种方式:
取地址赋值:
1 2
int a = 10; int* p = &a; // 将变量a的地址赋值给指针p
指针间赋值:
1 2
int* p1 = &a; int* p2 = p1; // p2指向和p1相同的地址
动态内存分配赋值:
1 2 3 4
int* p = new int(10); // 在堆上分配一个int变量的内存空间,并将这个int初始化为10 // 等价于: int* p = new int; // 先分配内存 *p = 10; // 再赋值为10
空指针赋值:
1
int* p = nullptr; // 将指针初始化为空
⚙️ 3.2 底层原理
&a
是取地址操作符,编译器会将变量a
的内存地址(如0x7ffe1234
)传给指针p
。p
存储这个地址本身。- 指针的赋值是一个简单的地址拷贝操作,不涉及内存拷贝。
四、📘 指针赋值 vs 内存拷贝
4.1 指针赋值
将一个地址赋值给另一个指针变量。只是复制地址,不复制指向的数据。
1
2
3
int a = 10;
int* p1 = &a;
int* p2 = p1; // p2 现在也指向 a
p2
拷贝了p1
中保存的地址(如 0x1000),它们都指向变量a
。
4.2 内存拷贝(深拷贝)
复制的是“指针指向的数据”本身,在内存中生成一个新副本。
1
2
3
int a = 10;
int* p1 = &a;
int* p2 = new int(*p1); // 新建一个 int,其值等于 *p1 的值
p2
指向的是一块新的内存,值为 10,与a
无关。
4.3 指针赋值(浅拷贝)图示
1
2
3
int a = 10;
int* p1 = &a;
int* p2 = p1;
1
2
3
4
5
6
7
8
+---------+ +-----------+
| int |<---------- | a=10 |
+---------+ +-----------+
^ ^
| |
p1 (0x1000) p2 (0x1000)
指针 指针
保存地址 同样地址
4.3.1 说明
a
是变量,值为10
,在内存中某个地址(比如0x1000
)。p1
和p2
都保存&a
,即0x1000
。- 修改
*p1
或*p2
,都会改变a
的值。
4.4 内存拷贝(深拷贝)图示
1
2
3
int a = 10;
int* p1 = &a;
int* p2 = new int(*p1); // 深拷贝
1
2
3
4
5
6
7
+-----------+ +-----------+
| a=10 | | 10 |
+-----------+ +-----------+
^ ^
| |
p1 (0x1000) p2 (0x2000)
指向 a 指向新内存(值为10)
4.4.1 说明
p1
指向a
。p2
指向新开辟的堆内存(new int(...)
),其值为*p1
的值,即 10。- 修改
*p1
不影响*p2
,反之亦然。
4.5 示例代码
✅ 4.5.1 指针赋值:地址共享,值同步
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p1 = &x;
int* p2 = p1;
*p2 = 99;
cout << x << endl; // 输出 99,因为 p2 修改的是 x 本身
}
p2 = p1
只是地址复制。*p2 = 99
实际修改的是x
。
✅ 4.5.2 深拷贝:内存复制,值独立
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p1 = &x;
int* p2 = new int(*p1); // 深拷贝:新建内存,复制值
*p2 = 99;
cout << x << endl; // 输出 42(原变量不变)
cout << *p2 << endl; // 输出 99
delete p2; // 清理内存
}
*p2 = 99
改的是新的内存空间,和x
无关。- 内存更独立、安全,适合需要独立副本的场景。
4.6 对比总结
对比项 | 指针赋值(浅拷贝) | 内存拷贝(深拷贝) |
---|---|---|
是否复制值 | ❌ 否,仅复制地址 | ✅ 是,复制值到新地址 |
是否共用内存 | ✅ 是,同一块内存 | ❌ 否,指向不同的内存块 |
是否互相影响 | ✅ 是,一个修改,另一个看到变化 | ❌ 否,互不干扰 |
内存管理 | 简单,不需要释放 | 需要手动释放(如 delete ) |
场景举例 | (1)参数传递需要修改实参(如函数中修改原变量) 、(2)数据结构中节点间共享数据(如链表、树)、(3)节省内存、提高效率(不重复分配空间) | (1)复制对象副本,避免数据共享引发副作用、(2)类中定义拷贝构造函数、赋值运算符(规则三/五)、(3)多线程程序中避免数据竞争 |
五、指针的解引用(Dereferencing)
5.1 什么是指针的解引用(Dereferencing)?
指针的解引用就是通过指针访问它所指向的内存中的实际数据。 在 C++ 中使用
*
操作符来完成这个操作。
一句话定义:
解引用 = 拿到地址指向的值。
5.2 基本语法
1
2
3
4
int a = 10;
int* p = &a;
cout << *p << endl; // 输出 10,解引用:访问 p 指向的内容
p
是一个指向int
的指针,值是&a
(地址)*p
是“解引用”,意思是“取出 p 指向地址里的值”
5.3 设计目的(为什么要有解引用)
C++ 设计解引用操作的初衷是:
间接访问数据
- 指针保存的是地址,解引用可以间接访问或修改该地址的内容。
高效操作内存
- 解引用让你能以更底层的方式操作内存中的变量。
支持动态内存、数据结构
- 如链表、树、堆等结构中常通过指针解引用操作访问节点值。
5.4 底层原理(内存视角)
假设你有以下代码:
1
2
int a = 42;
int* p = &a;
内存结构大致如下:
1
2
3
4
5
地址 内容
0x1000 a 的值:42
0x2000 p 的值:0x1000(也就是 &a)
*p == 访问 0x1000 的值 == 42
当你写 *p
,CPU 实际上:
- 从
p
的存储位置取出地址(如 0x1000) - 再到地址 0x1000 读出值(即
a
的内容)
5.5 基本解引用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
int main() {
int a = 5;
int* p = &a;
cout << "a = " << a << endl;
cout << "*p = " << *p << endl; // 解引用
*p = 20; // 通过解引用修改 a
cout << "a after *p = 20: " << a << endl;
}
输出:
1
2
3
a = 5
*p = 5
a after *p = 20
⚠️ 5.5.1 错误用法示例:野指针解引用
1
2
int* p; // 未初始化,p 是野指针
*p = 100; // ❌ 未定义行为(UB):访问未知地址
所以:指针解引用之前必须确保指向合法内存!
5.6 解引用 vs 地址操作 对照图
1
2
int a = 10;
int* p = &a;
表达式 | 含义 |
---|---|
a | 变量 a 的值(10) |
&a | a 的地址 |
p | 指针变量 p,值为 &a |
*p | 解引用:访问 p 指向的值,即 a |
5.7 小结口诀
*p 是值,p 是地址;& 是取址,* 是取值。
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP各级别考纲要点、知识拓展和练习题目清单详见C++学习项目主页
“luogu-”系列题目已加入洛谷Java、C++初学团队,作业清单,可在线评测,团队名额有限,欢迎加入。
“bcqm-”系列题目可在编程启蒙题库进行在线评测。