火龙信奥
  • 首页
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

第四章:内存管理与指针

作者: 作者的头像   huolong , 时间:2025-09-01 14:19:55 , 所有人可见, 阅读  7

第四章:内存管理与指针 (Speicherverwaltung und Zeiger)

1. 课程概览
  • 指针 (Zeiger)
  • 指针算术 (Zeigerarithmetik)
  • 指针用于动态内存 (Zeiger für dynamischen Speicher)
  • 应用 (Anwendungen)

2. 指针:一个直观的例子
  • 类比:名片
  • 名片 (指针):包含地址(如 "X-Weg 42")。
  • 地址 (内容):指向实际的对象(如 "Hugo Hase")。
  • 核心概念:指针是一个变量,它存储另一个对象的内存地址。

3. 内存地址与指针
  • 内存地址:计算机主存中的每个位置都有一个唯一的编号(地址),通常是十六进制数。
  • 对象大小:数据类型的大小决定了对象在内存中占用的字节数。
  • 指针:一个专门存储内存地址的特殊数据对象。
  • 大小:取决于计算机架构(32 位系统通常 4 字节,64 位系统通常 8 字节)。
  • 示例: cpp char c = 'x'; // 变量 c,地址 0x32760,内容 'x' int i = 99; // 变量 i,地址 0x32762,内容 99 int* j = &i; // 指针 j,地址 0x32791,内容是 i 的地址 (0x32762)
  • j 的内容是 0x32762,即变量 i 在内存中的起始地址。
  • 通过 j 可以访问或修改 i 的值。

4. 指针的声明与初始化
  • 声明:<数据类型>* <指针名>;
  • * 表示这是一个指针变量。
  • <数据类型> 指明该指针将指向何种类型的数据(这决定了指针算术的行为)。
  • 例如:double* pUmsatz; 声明了一个指向 double 类型的指针 pUmsatz。
  • 命名习惯:指针变量名常以小写 p 开头,如 pName, pOrt。
  • 初始化:至关重要! 必须为指针赋值一个有效的地址。
  • 错误示例: cpp double* widerstand; *widerstand = 120.5; // 严重错误!widerstand 未初始化,指向未知位置
  • 正确示例: cpp double umsatz; double* pUmsatz = &umsatz; // pUmsatz 指向 umsatz 的地址 *pUmsatz = 120.5; // 通过 pUmsatz 修改 umsatz 的值
  • 常见错误:
  • 未初始化的指针(“野指针”)会指向内存中的随机位置。
  • 若指向受保护内存,程序会因访问违规而崩溃。
  • 若指向非受保护内存,可能意外修改其他数据,导致程序行为诡异,难以调试。

5. const 限定符 (Exkurs: const-Qualifizierer)
  • const:关键字,表示值在初始化后不可再修改。
  • 两种等价写法:const int a = 1; 或 int const a = 1;
  • 建议:选择一种写法并保持一致,避免混淆。
  • 用途:定义常量,如数学常数。 cpp const double pi{3.1415926535897}; // a = 5; // 错误!a 是常量,不能赋值 int c = 1; int const b{c}; // b 被初始化为 c 的值 (1),之后不可变
  • const vs constexpr:
  • const:值在运行时初始化后不可变。
  • constexpr:值必须在编译时就确定。 cpp int c; // c 的值在运行时确定 const int b{c}; // 允许,b 的值在运行时初始化 // constexpr int b{c}; // 错误!c 的值在编译时未知 constexpr double pi{3.1415926535897}; // 正确,pi 是编译时常量
  • 最佳实践:对于数学常数,优先使用 constexpr。C++20 中可使用标准库中的 std::numbers::pi。

6. 指针与常量数据
  • 指向常量的指针: cpp const int a = 1; // a 是常量整数 int const* pa = &a; // pa 是一个指向常量 int 的指针,允许 // *pa = 12; // 错误!不能通过 pa 修改 a 的值
  • int const* pa 从右向左读:pa 是一个指向 const int 的指针。
  • 常量指针: cpp int b = 2; int const* const pc = &a; // pc 是一个指向常量 int 的常量指针 // pc = &b; // 错误!pc 本身是常量,不能修改其指向

7. 指针算术 (Zeigerarithmetik)
  • 概念:对指针进行加减运算,其结果会根据指针所指向数据类型的大小进行缩放。
  • 规则:ptr + i 实际上是 ptr + (i * sizeof(指向的数据类型)) 个字节。
  • 示例 I: cpp int a[] = {100, 110, 120, 130}; int* pa = &a[0]; // pa 指向数组 a 的首元素 int sum = 0; // 通过指针算术访问数组元素 sum += *pa + *(pa + 1) + *(pa + 2) + *(pa + 3); // sum = 460 // 等价于 sum += a[0] + a[1] + a[2] + a[3];
  • 示例 II:

cpp struct KundeT { double umsatz; // 8 字节 float skonto; // 4 字节 }; // 总共 12 字节 KundeT Kunde[5]; KundeT* pKunde = &Kunde[0]; // pKunde 指向 Kunde[0] int i = 3; // pKunde + i 指向 Kunde[3] // 如果 pKunde 的地址是 0x10000,那么 pKunde + 3 的地址是 0x10000 + 3*12 = 0x10038 *pKunde = *(pKunde + i); // 将 Kunde[3] 的值赋给 Kunde[0]

  • [] 与 * 的关系:
  • C/C++ 中的数组名 a 在大多数情况下可以看作是指向其首元素的指针(&a[0])。
  • a[i] 在内部被翻译为 *(a + i)。
  • 因此,pa[2] 等价于 *(pa + 2)。
  • 重要区别:数组名 a 是一个常量指针(int* const a),不能被修改(如 a++ 是非法的),而指针变量 pa 可以被修改。

8. 指针与结构体 (Zeiger auf struct)
  • 访问结构体成员: cpp struct Punkt { int x, y; }; Punkt p{10, 10}; // 声明并初始化结构体变量 p Punkt* ptr = &p; // ptr 指向 p ptr->x = 20; // 通过指针访问成员 x ptr->y = 30; // 通过指针访问成员 y (*ptr).x = 40; // 等价于 ptr->x = 40 (*ptr).y = 50; // 等价于 ptr->y = 50
  • -> 运算符:ptr->x 是 (*ptr).x 的简写,用于通过指针访问结构体或类的成员。

9. 动态内存分配:动机与 new/delete
  • 动机:静态数组的大小必须在编译时确定。但有时我们需要在运行时根据用户输入来决定数组大小。
  • 问题:例如,读取两个长度为 n 的向量(n 未知),计算它们的点积。
  • 解决方案:使用动态内存分配。

  • new 操作符:在程序运行时从堆(heap)中分配内存。

  • delete 操作符:释放不再需要的动态内存,防止内存泄漏。

  • 示例:动态一维数组:

```cpp int n; cout << "请输入数组长度: "; cin >> n;

// 动态分配一个包含 n 个 double 类型元素的数组 double* vektor = new double[n];

// 使用数组... for (int i = 0; i < n; ++i) { vektor[i] = i * 1.5; }

// ... 计算点积等操作

// 释放内存 delete[] vektor; // 注意:对于数组,使用 delete[] vektor = nullptr; // (可选) 将指针置空,避免悬空指针 ```


10. 动态二维数组(矩阵)
  • 思路:模拟一个 a x m 的矩阵。
  • 创建一个包含 a 个元素的指针数组(行指针数组)。
  • 为每一行分配一个包含 m 个元素的数组。
  • 代码实现:

```cpp #include using namespace std; int main(){ int z{3}; // 行数 int s{4}; // 列数

  // 1. 分配行指针数组
  double** matrix = new double*[z]; // matrix 是一个指向指针的指针

  // 2. 为每一行分配空间
  for(int i = 0; i < z; ++i){
      matrix[i] = new double[s]; // 第 i 行是一个包含 s 个 double 的数组
  }

  // 3. 初始化矩阵
  for(int i = 0; i < z; ++i){
      for(int j = 0; j < s; ++j){
          matrix[i][j] = i + j/10.0;
      }
  }

  // 4. 输出矩阵
  for(int i = 0; i < z; ++i){
      for(int j = 0; j < s; ++j){
          cout << matrix[i][j] << " ";
      }
      cout << endl;
  }
  // 输出:
  // 0 0.1 0.2 0.3
  // 1 1.1 1.2 1.3
  // 2 2.1 2.2 2.3

  // 5. 释放内存(**非常重要!顺序不能错**)
  // 先释放每一行的数组
  for(int i = 0; i < z; ++i){
      delete[] matrix[i];
  }
  // 再释放行指针数组
  delete[] matrix;

  return 0;

} ```

  • 关键点:
  • 访问方式与静态二维数组完全相同:matrix[i][j]。
  • 内存释放必须先释放行,再释放行指针数组。顺序颠倒会导致未定义行为。
  • 这种方法允许不同行有不同的列数(锯齿数组),但在此例中是规则矩阵。

11. 静态与动态数组的比较
特性 静态数组 动态数组
内存位置 栈 (Stack) 堆 (Heap)
大小确定时间 编译时 运行时
大小 固定 可变
内存管理 自动(函数结束时释放) 手动(必须用 delete[])
访问速度 快 稍慢(多一次间接寻址)
风险 栈溢出(太大) 内存泄漏(忘记释放)、悬空指针(释放后仍使用)

总结:动态内存提供了极大的灵活性,但责任也更大。必须始终在使用完后调用 delete(或 delete[])来释放内存。

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

©2026加盟我们 | 关于我们 | ACM课程 | 常见问题 | 成果墙 | 评测记录 | 浙ICP备2021013995号
在线画图 | OI WIki | 打字练习
火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码