第四章:内存管理与指针 (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),之后不可变 constvsconstexpr: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