文章

【GESP】C++四级考试大纲知识点梳理, (1) 指针

GESP C++四级官方考试大纲中,共有11条考点,本文针对第一条考点进行分析介绍。之前对1-3级大纲知识点的梳理思路是仅梳理编程基础知识考纲条目,对于C++编程本身语法和规则相关考纲要求,因为网上可查看的信息非常多,所以不做梳理。但目前来看,一方面系统梳理有利于孩子系统学习,另一方面感觉网上其他学习的同学也习惯针对考虑进行专项搜索,因此从4级开始,我尽量逐条整理考纲相关知识点,便于孩子针对性学习。

(1)理解 C++指针类型的概念,掌握指针类型变量的定义、赋值、解引用。


一、指针的设计目的(为什么需要指针?)

1.1 设计目的

  1. 操作内存地址

    • C++ 是底层语言,需要具备操作内存的能力,而指针是访问/操作内存地址的唯一合法方式。
    • 举个例子:就像我们需要知道朋友家的地址才能去找他一样,程序也需要知道数据存放的内存地址才能访问数据。指针就相当于存储这个”地址”的记事本。
  2. 支持间接访问(间接赋值)

    • 能通过指针修改其它变量的值,尤其在函数调用时传递地址,实现参数共享。
    • 比如下面这段代码:

      1
      2
      3
      4
      5
      6
      
      void modifyValue(int* ptr) {
          *ptr = 100;  // 通过指针修改原变量的值
      }
           
      int num = 10;
      modifyValue(&num);  // num 的值被改为 100
      

      这里 num 的地址通过参数传给了函数,函数内部就可以通过这个地址(指针)直接修改 num 的值,而不需要返回新值。

  3. 实现动态内存管理

    • new/delete 搭配使用,能手动控制内存的申请和释放。
    • 在 C++ 中,new/delete 操作符需要和指针配合使用,因为:
      • new 在堆上分配内存并返回该内存的地址,只有指针才能存储这个地址
      • 没有指针存储地址,就无法访问或释放这块内存
      • 其他变量类型只能存储栈上的数据,不能直接操作堆内存
    • 举个生活中的例子:
      • 指针就像收据,记录了你租房的具体位置(内存地址)
      • new 相当于租房,但必须把房子地址(返回的内存地址)记在收据(指针)上
      • delete 相当于退房,但必须出示收据(指针)才知道退哪间房
      • 如果丢了收据(指针),这间房就成了无人认领的”内存泄漏”
  4. 实现复杂数据结构

    • 链表、树、图等数据结构本质上都依赖指针实现节点之间的连接。
    • 比如在链表中,每个节点都包含数据和一个指向下一个节点的指针,通过这些指针就能把所有节点串联成一个完整的数据结构。没有指针就无法实现这种灵活的数据结构。
  5. 提高性能

    • 指针传递的是地址,避免值传递的大量拷贝,提高效率。
    • 举个例子:假设有一个很大的数组(比如 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. 基本语法

    1
    2
    3
    
    类型名* 指针变量名;
    // 或
    类型名 *指针变量名;
    

    两种写法都正确,但推荐第一种,因为更清晰地表明这是一个指针类型。

  2. 命名规则
    • 遵循变量命名规则
    • 建议使用 pptr 作为前缀,表明这是指针
    • 例如:pCountptrNode
  3. 初始化建议
    • 定义指针时最好立即初始化
    • 如果暂时不知道指向哪里,建议初始化为 nullptr
    • 例如:

      1
      2
      
      int* p = nullptr;  // 好的做法
      int* p;           // 不推荐,未初始化的指针很危险
      
  4. 多个指针声明
    • 每个指针变量名前都需要 *
    • 例如:

      1
      2
      
      int* p1, * p2;  // p1 和 p2 都是指针
      int* p1, q;     // p1 是指针,q 是普通 int 变量
      
  5. const 修饰
    • const int* p - 指针指向的值不能改
    • int* const p - 指针本身不能改
    • const int* const p - 都不能改
  6. 类型匹配
    • 指针类型必须与指向的变量类型匹配
    • 例如:

      1
      2
      3
      
      int x = 10;
      int* p = &x;     // 正确
      double* q = &x;  // 错误:类型不匹配
      

2.3 底层原理

  • 指针本质上是一个整数,它存储的是另一个变量的内存地址。
  • 指针本身也有自己的地址,和它指向的地址是两个不同的概念。
  • 编译器通过指针的类型来决定:

    • 解引用时要读多少字节(int = 4 字节,double = 8 字节)
    • 指针加减步长是多少(如 p + 1 步长和类型相关)

三、指针变量的赋值

3.1 指针变量赋值的基本方式

指针变量的赋值主要有以下几种方式:

  1. 取地址赋值

    1
    2
    
    int a = 10;
    int* p = &a;  // 将变量a的地址赋值给指针p
    
  2. 指针间赋值

    1
    2
    
    int* p1 = &a;
    int* p2 = p1;  // p2指向和p1相同的地址
    
  3. 动态内存分配赋值

    1
    2
    3
    4
    
    int* p = new int(10);  // 在堆上分配一个int变量的内存空间,并将这个int初始化为10
    // 等价于:
    int* p = new int;  // 先分配内存
    *p = 10;          // 再赋值为10
    
  4. 空指针赋值

    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)。
  • p1p2 都保存 &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++ 设计解引用操作的初衷是:

  1. 间接访问数据

    • 指针保存的是地址,解引用可以间接访问或修改该地址的内容。
  2. 高效操作内存

    • 解引用让你能以更底层的方式操作内存中的变量。
  3. 支持动态内存、数据结构

    • 如链表、树、堆等结构中常通过指针解引用操作访问节点值。

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 实际上:

  1. p 的存储位置取出地址(如 0x1000)
  2. 再到地址 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)
&aa 的地址
p指针变量 p,值为 &a
*p解引用:访问 p 指向的值,即 a

5.7 小结口诀

*p 是值,p 是地址;& 是取址,* 是取值。


所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

GESP各级别考纲要点、知识拓展和练习题目清单详见C++学习项目主页

luogu-”系列题目已加入洛谷Java、C++初学团队作业清单,可在线评测,团队名额有限,欢迎加入。

bcqm-”系列题目可在编程启蒙题库进行在线评测。

本文由作者按照 CC BY 4.0 进行授权