# 第七章 指针

> Arietids Learn · 个人课程复习笔记
> 在线阅读：https://learn.arietids.net/courses/programming/chapter-07/

## 关键考点总览

1. 指针的定义与初始化（取地址`&`、解引用`*`、空指针`nullptr/NULL`）
2. 指针运算与数组名的关系（`p+k` 的计算规则、数组名在多数表达式中会“衰变”为指向首元素的指针）
3. 字符串与字符指针的使用
4. 动态内存分配（`new`/`delete`、内存泄漏）
5. 指针作为函数形参（实现数据共享、多返回值）
6. 引用类型与函数参数传递
7. 指针数组与多级指针（二级指针、动态二维数组）
8. 函数指针（定义、调用、作为参数传递）
9. 常量指针与指针常量的区别
10. `void*` 通配指针与强制类型转换

---

## 详细知识点

### 7.1 指针的概念

#### 定义/语法

- **指针**：存储地址的变量，可用于间接访问对象、在函数间共享/修改数据，并配合动态内存分配管理运行期对象（如动态数组）。
  [来源：第7章 p8，指针介绍]

- **指针定义格式**：
  ```cpp
  类型 *指针名;
  ```
  需告诉编译器：(1) 该变量是一个指针；(2) 指针指向的数据是什么类型。
  [来源：第7章 p10，定义指针]

- **定义示例**：
  ```cpp
  int  *intp;
  double *doublep;
  int *p, x, *q;  // p和q是指针，x是普通int变量
  ```
  [来源：第7章 p10，定义指针]

#### 规则/规范

- **取地址操作符 `&`**：返回变量的地址，`&x` 返回变量 x 的地址。
  - `&` 只能用于“有确定存储位置”的对象（例如具名变量等左值）；对字面量/临时对象通常非法，如 `&2` 非法。
  [来源：第7章 p11，指针的操作]

- **解引用操作符 `*`**：通过指针间接访问它所指向的单元，`*intp` 表示 intp 指向的变量。
  ```cpp
  int x, *intp = &x;
  *intp = 5;  // 等价于 x = 5;
  ```
  [来源：第7章 p11，指针的操作]

- **空指针初始化**：推荐使用 `nullptr` 表示“不指向任何对象”的空指针常量；`NULL` 在 C++ 中通常是 `0/0L` 的宏，可能与整数重载混淆，不建议优先使用。
  [来源：第7章 p12，指针的初始化]

#### 易错点与辨析

- ⚠️ **未初始化指针的危险**：未初始化指针的值不确定；对其解引用属于未定义行为（可能崩溃、数据破坏或表现“看似正常”）。
  ```cpp
  int *p;
  *p = 5;  // 严重错误！p未初始化
  ```
  [来源：第7章 p12，指针的初始化]

- ⚠️ **指针初始化与赋值的区别**：
  - 初始化：`int *p = &x;`（解引用符 `*` 与取址符 `&` 配对）
  - 赋值：`p = &y;`（指针变量名与取址符 `&` 配对，**不加 `*`**）
  [来源：第7章 p13，Notes]

#### 例题结论

- **【习题分析】** 以下代码的问题：
  - `int *p = 10;` → 错误：不能存 int 类型给指针
  - `int *p = &10;` → 错误：常量没有内存地址
  - `int *p = *10;` → 错误：解引用 `*` 只能对地址使用
  - `*p = &x;` → 错误：`*p` 是 int 类型，不能赋地址值
  [来源：第7章 p14，习题 Notes答案]

- **【正确写法】** 定义指针指向变量并间接修改：
  ```cpp
  int num = 10;
  int *p = &num;
  *p += 5;  // num变成15
  ```
  [来源：第7章 p14，习题 Notes答案]

---

### 7.2 void* 通配指针

#### 定义/语法

- **通配指针定义**：当定义指针但不关心被指向变量类型时使用。
  ```cpp
  void *指针变量名;
  ```
  [来源：第7章 p15，void*通配指针类型]

#### 规则/规范

- 其他类型的指针**可以赋值给** void 类型指针。
- 把 void 类型指针赋给其他类型指针时，**必须做强制类型转换**。
  ```cpp
  void *pointer;
  int *x, y;
  x = &y;
  pointer = x;           // 合法
  x = static_cast<int*>(pointer);  // C++ 推荐写法
  ```
  [来源：第7章 p15，void*通配指针类型]

#### 易错点与辨析

- ⚠️ 不同类型指针之间不能直接赋值：
  ```cpp
  int *x;
  double *tem;
  tem = x;  // 错误：cannot convert 'int*' to 'double*'
  ```
  [来源：第7章 p15，void*通配指针类型]

---

### 7.3 指针运算和数组名

#### 定义/语法

- **指针加/减法**：若 p 指向数组元素，则 `p±k` 会按元素大小移动（等价于地址偏移 `k * sizeof(*p)` 字节）；该运算仅在同一数组对象范围内（含 one-past-the-end）才有定义。
  [来源：第7章 p17，指针运算]

- **数组名与指针的关系**：数组名本质是数组对象；在多数表达式中会衰变为指向首元素的指针（因此不能对数组名赋值或自增）。
  ```cpp
  int a[5] = {1,2,3,4,5}, *p;
  p = a;  // p指向数组首地址
  ```
  [来源：第7章 p18，指针与数组名]

#### 规则/规范

- **指针访问数组元素的等价写法**：`p[i]` 等价于 `*(p+i)`
  [来源：第7章 p18，指针与数组名]

- **合法的数组遍历方式**：
  ```cpp
  for (i=0; i<5; ++i) cout << a[i] << ' ';
  for (i=0; i<5; ++i) cout << p[i] << ' ';
  for (i=0; i<5; ++i) cout << *(a+i) << ' ';
  for (i=0; i<5; ++i) cout << *(p+i) << ' ';
  for (p=a; p<a+5; ++p) cout << *p << ' ';
  ```
  [来源：第7章 p18，指针与数组名]

- **非法遍历方式**：数组名是常量，**不能对其进行自增操作**。
  ```cpp
  for (i=0; i<5; ++i) {
      cout << *a << ' ';
      ++a;  // 错误！数组名是常量地址
  }
  ```
  [来源：第7章 p18，指针与数组名]

- **数组作为函数形参**：实质上是指针作为形参，以下两种写法等价：
  ```cpp
  int fun(int p[], int size);
  int fun(int *p, int size);
  ```
  传递的是数组起始地址，数组大小需要另外一个参数指定。
  [来源：第7章 p20，数组传递的进一步讨论]

#### 例题结论

- **【习题分析】** 假设 `int arr[5] = {1, 2, 3, 4, 5}`：
  - `arr` → 首地址
  - `arr[2]` → 第三个元素（值为3）
  - `&arr[2]` → 第三个元素的地址
  - `arr + 2` → 第三个元素的地址
  - `*(arr+2)` → 第三个元素（值为3）
  - `*(arr+2)*2` → 第三个元素的两倍（值为6）
  [来源：第7章 p19，习题 Notes答案]

- **【执行结果分析】**
  ```cpp
  int arr[] = {10, 20, 30, 40, 50};
  int *p = arr + 2;
  cout << *p << " " << p[2];
  ```
  输出：`30 50`
  （`*p` 是 arr[2]=30，`p[2]` 是 *(p+2)=arr[4]=50）
  [来源：第7章 p19，习题 Notes答案]

- **【执行结果分析】** 排序函数调用：
  ```cpp
  #include <algorithm>

  // 只对前5个元素排序
  int a1[10] = {9,8,7,6,5,4,3,2,1,0};
  std::sort(a1, a1 + 5);
  // 结果：5 6 7 8 9 4 3 2 1 0（前5个有序）
  // 只对后5个元素排序
  int a2[10] = {9,8,7,6,5,4,3,2,1,0};
  std::sort(a2 + 5, a2 + 10);
  // 结果：9 8 7 6 5 0 1 2 3 4（后5个有序）

  ```
  [来源：第7章 p20，数组传递的进一步讨论]

---

### 7.4 字符串再讨论

#### 定义/语法

- **字符指针引用字符串**：常用一个字符指针来引用一个字符串，从该指针所指向的字符开始，直到 `'\0'` 字符结束。
  ```cpp
  char bstr[] = "201811122119";
  char *p = bstr;
  cout << p << endl;   // 输出整个字符串
  cout << *p << endl;  // 输出第一个字符 '2'
  ```
  [来源：第7章 p22，字符串再讨论]

#### 规则/规范

- 字符串以 `'\0'` 结束，只需确定字符串开始的地址即可，**不需要指定长度**。
  [来源：第7章 p35，Notes]

- **字符串字面量具有静态存储期**，通常放在只读存储区；尝试通过指针修改其内容属于未定义行为（应使用 `const char*` 接收）。
  [来源：第7章 p23，Notes]

#### 易错点与辨析

- ⚠️ **字符串操作错误示例**：
  ```cpp
  const char *pt1 = "1234";   // 字符串字面量应以 const char* 接收
  char pt2[] = "12";          // pt2 是字符数组（容量为3，含 '\0'）
  const char *pt3 = "34";

  pt3 = pt2;                  // A: 合法，指针可以重新指向别处
  // pt2 = pt1;               // B: 错误！数组名不可赋值
  strcpy(pt2, pt1);           // C: 错误！数组溢出（pt2 只有3字节）

  ```
  [来源：第7章 p23，字符串常量]

#### 例题结论

- **【执行结果分析】** strcpy 返回值应用：
  ```cpp
  char bString[80], aString[80] = "201811122119";
  strcpy(bString, aString)[2] = 'A';  // bString = "20A811122119"
  strcpy(bString, "Hello, world!");    // bString = "Hello, world!"
  strcpy(bString, aString+5);          // bString = "1122119"
  ```
  [来源：第7章 p22，Notes答案]

- **【填空题答案】** 将数字字符串转换为整数：
  ```cpp
  int func(char* str) {
      int num = 0, digital;
      while (*str >= '0' && *str <= '9') {  // 填空1
          digital = *str - '0';              // 填空2
          num = num * 10 + digital;
          str++;
      }
      return num;
  }
  ```
  [来源：第7章 p25，习题 Notes答案]

---

### 7.5 动态内存分配

#### 定义/语法

- **new 运算符**：动态分配空间并返回首地址。
  ```cpp
  int n = 10;
  int *array1 = new int[n];           // 动态分配整型数组
  int *array2 = new int[n]();         // 归零初始化
  int *array3 = new int[n]{1,2,3,4,5}; // 初始化赋值
  ```
  [来源：第7章 p27，内存的动态分配]

- **delete 运算符**：回收动态分配的空间。
  ```cpp
  delete p;          // 回收指针指向的单个变量空间
  delete [] array;   // 回收动态数组空间
  ```
  [来源：第7章 p28，动态内存释放]

#### 规则/规范

- 动态分配的空间**需要显式用 delete 释放**，delete 需要该动态分配空间的首地址。
  [来源：第7章 p28，动态内存释放]

- **内存泄漏**：如果一个动态分配的空间没有被任何指针变量引用，则无法用 delete 释放。
  [来源：第7章 p28，动态内存释放]

- ⚠️ **不要重复释放**同一块内存。
  [来源：第7章 p28，动态内存释放]

- **动态分配可能失败**：当内存用完时，new 会抛出 `bad_alloc` 异常。使用 `new(std::nothrow)` 不会抛出异常，而是返回空指针 `nullptr`（需要 `#include <new>`）。
  ```cpp
  p = new(std::nothrow) int;
  if (!p) {
      cout << "allocation failure\n";
      return -1;
  }
  ```
  [来源：第7章 p29，动态分配可能会失败]

- **assert 宏**：判断表达式真假，为假则终止程序。
  ```cpp
  #include <cassert>
  int *p = new(std::nothrow) int;
  assert(p != nullptr);  // p 为 nullptr 则退出程序
  ```
  [来源：第7章 p29，动态分配可能会失败]

#### 执行逻辑

- **内存分配区域（概念分类；具体地址布局因平台/实现而异）**：
  1. **静态分配**：全局变量和静态变量，在整个程序运行期间都存在
  2. **动态分配（堆）**：需要时向系统申请，不再使用时显式释放
  3. **自动分配（栈）**：函数内定义的自动变量，函数结束后自动释放
  [来源：第7章 p30，内存分配的进一步介绍]

---

### 7.6 指针和函数

#### 定义/语法

- **指针形参实现数据共享**：用指针作为参数可以在被调用函数中修改调用函数的变量。
  ```cpp
  void swap_p(int *pa, int *pb) {
      int c;
      c = *pa;
      *pa = *pb;
      *pb = c;
  }
  // 调用：swap_p(&x, &y);
  ```
  [来源：第7章 p33，指针变量作为形参：输入]

- **指针形参实现多返回值**：
  ```cpp
  void add_on(int a, int *add1, int *add2, int *add3) {
      *add1 = a + 1;
      *add2 = a + 2;
      *add3 = a + 3;
  }
  ```
  [来源：第7章 p36，指针变量作为形参：输出]

- **返回指针的函数**：
  ```cpp
  返回类型 *函数名(形参列表);
  ```
  返回的指针在其被使用时必须有效，**不能返回局部变量的地址**。
  [来源：第7章 p38，返回指针的函数]

#### 规则/规范

- **常量指针**（`const int *p`）：不能通过解引用更改变量的值。
  ```cpp
  int x = 5;
  const int *p = &x;
  *p = 20;  // 错误：assignment of read-only location
  ```
  [来源：第7章 p34，防止数据被间接引用修改]

- **指针常量**（`int * const p`）：指针本身不能改变，定义时必须初始化。
  ```cpp
  int x = 5, y = 6;
  int * const p = &x;
  *p = 20;  // 合法，x变成20
  p = &y;   // 错误：assignment of read-only variable
  ```
  [来源：第7章 p34，防止数据被间接引用修改]

【整理自 PPT】**常量指针 vs 指针常量 记忆口诀**：`const` 在 `*` 左边，指向内容不可变；`const` 在 `*` 右边，指针本身不可变。
（由[来源：第7章 p34] 整理）

#### 例题结论

- **【填空题答案】** 函数返回动态数组：
  ```cpp
  int *input(int n) {
      int *buf;              // 填空1
      buf = new int[n];      // 填空2
      for (int i = 0; i < n; i++) cin >> buf[i];
      return buf;
  }
  int main() {
      int num;
      int *data;             // 填空3
      cin >> num;
      data = input(num);
      for (int i = 0; i < num; i++) cout << data[i] << ' ';
      delete [] data;        // 填空4
      return 0;
  }
  ```
  [来源：第7章 p40，习题 Notes答案]

---

### 7.7 引用类型和函数

#### 定义/语法

- **引用**：给一个变量取一个别名，使一个内存单元可以通过不同的变量名来访问。
  ```cpp
  类型 &引用名 = 变量名;
  
  int i = 1;
  int &j = i;
  j = 2;  // i == 2
  ```
  [来源：第7章 p42，引用]

#### 规则/规范

- **必须在定义引用变量时就初始化**，之后不能再改变绑定。
  [来源：第7章 p42，引用]

- 引用在语义上是别名；实现上编译器通常用指针实现，但标准不保证其占用空间一定等同于指针，也不应把引用当作“可为 null 的指针”。
  [来源：第7章 p42，引用]

- **引用实现函数参数传递**（比指针更简洁）：
  ```cpp
  void swap(int &a, int &b) {
      int c;
      c = a;
      a = b;
      b = c;
  }
  // 调用：swap(x, y);  // 不需要&
  ```
  [来源：第7章 p43，引用参数传递与函数返回]

#### 例题结论

- **【执行结果分析】** 引用与指针混合使用：
  ```cpp
  int swap(int &x, int *y, int z=1) {
      int *a = &x;
      static int b = x;  // 静态变量，只初始化一次
      b += 2;
      *a = (*a) - z + b;
      return b;
  }
  int main() {
      int a = 8, b = 5;
      cout << swap(a, &b) << " " << a << " " << b << endl;  // 10 17 5
      cout << swap(a, &b) << " " << a << " " << b << endl;  // 12 28 5
      return 0;
  }
  ```
  输出：
  ```
  10 17 5
  12 28 5
  ```
  [来源：第7章 p44，习题 Notes答案]

---

### 7.8 指针数组和多级指针

#### 定义/语法

- **指针数组**：多个指针组织成的数组。
  ```cpp
  类型名 *数组名[数组长度];
  int *a[5];  // 包含5个int指针的数组
  ```
  [来源：第7章 p46，指针数组和多级指针]

- **多级指针**：指向指针的指针。
  ```cpp
  二级指针：类型名 **变量名;
  三级指针：类型名 ***变量名;
  ```
  多级指针需要逐层解引用：若 `T** pp` 指向一个 `T*`，则 `*pp` 为 `T*`，`**pp` 才是 `T` 对象。
  [来源：第7章 p46，指针数组和多级指针]

#### 代码模式

- **动态创建 M×N 二维数组 - 方法一**（二级指针 + 指针数组）：
  ```cpp
  int **a, i, j, k = 0;
  a = new int *[3];                     // 分配指针数组
  for (i = 0; i < 3; ++i) 
      a[i] = new int[4];                // 每个指针指向一维数组
  // 使用：a[i][j]
  // 释放：
  for (i = 0; i < 3; ++i) delete [] a[i];
  delete [] a;
  ```
  [来源：第7章 p48，方法一示例]

- **动态创建 M×N 二维数组 - 方法二**（一维数组模拟）：
  ```cpp
  int *a, i, j, k = 0;
  a = new int[12];                      // 分配3×4=12个元素
  // 访问第i行第j列：a[i*4+j]
  // 释放：
  delete [] a;
  ```
  [来源：第7章 p50，方法二示例]

- **字符串数组使用指针数组存储**：
  ```cpp
  const char *p[2] = {"Beijing", "Shanghai"};
  for (int i = 0; i < 2; i++) cout << p[i];
  ```
  [来源：第7章 p51，字符串数组]

#### 例题结论

- **【填空题答案】** 字符串搜索函数：
  ```cpp
  bool search(const char * const *dept, int n, const char *s) {  // 填空1
      for (int i = 0; i < n; i++)
          if (strcmp(dept[i], s) == 0)
              return true;   // 填空2
      return false;          // 填空3
  }
  int main() {
      const char *deptName[4] = {"CS", "AU", "EE", "SE"};  // 填空4
      // ...
  }
  ```
  [来源：第7章 p52，习题 Notes答案]

- **【执行结果分析】** 指针数组排序后输出：
  ```cpp
  const char *str[3] = {"morning", "afternoon", "apple"};
  // 按字典序排序后：{"afternoon", "apple", "morning"}
  for (int i = 0; i < 3; i++)
      cout << str[i] + i << endl;  // 注意 +i 偏移
  ```
  输出：
  ```
  afternoon    // str[0]+0，完整输出
  pple         // str[1]+1，跳过第一个字符'a'
  rning        // str[2]+2，跳过前两个字符"mo"
  ```
  [来源：第7章 p53，习题]

- **【执行结果分析】** 二级指针操作：
  ```cpp
  char text[][20] = {"Hello world!", "I love SJTU!", "How are you?"};
  char *str[3] = {text[0], text[1], text[2]};
  char **p = str + 1;  // p指向str[1]
  **p = 'U';           // text[1][0]变成'U'
  cout << *p << endl;           // U love SJTU!
  cout << *(p+1) << endl;       // How are you?
  cout << *(*(p-1)+11);         // ! （text[0][11]）
  ```
  [来源：第7章 p54，习题]

---

### 7.9 程序接收运行时参数

#### 定义/语法

- **main 函数参数**：程序运行时的参数作为 main 函数的参数传入。
  ```cpp
  int main(int argc, char *argv[]) {
      // argc: 参数个数
      // argv: 参数字符串数组
  }
  ```
  [来源：第7章 p55，程序接收运行时参数]

#### 例题结论

- **【执行结果分析】** 命令行参数：
  ```
  c:\> myprogram.exe -d -c new.txt
  ```
  输出：
  ```
  argc=4
  argv[0]=myprogram.exe
  argv[1]=-d
  argv[2]=-c
  argv[3]=new.txt
  ```
  [来源：第7章 p55，程序接收运行时参数]

---

### 7.10 函数指针

#### 定义/语法

- **函数指针**：保存函数入口地址的指针。
  ```cpp
  返回类型 (*指针名)(形参列表);
  
  int my_isdigit(int n, int k);        // 示例函数：避免与 <cctype> 的 isdigit 冲突
  int (*p)(int, int) = my_isdigit;     // 或：p = my_isdigit;
  ```
  [来源：第7章 p57，指向函数的指针]

- **通过函数指针调用函数**（三种等价写法）：
  ```cpp
  int x = 0;
  my_isdigit(12, x);
  (*p)(12, x);
  p(12, x);
  ```
  [来源：第7章 p57，指向函数的指针]

#### 代码模式

- **函数指针作为参数**（实现通用排序）：
  ```cpp
  void quicksort(int data[], int low, int high,
                 int (*comp)(int, int)) {
      // ...
      mid = divide(data, low, high, comp);
      // ...
  }
  
  // 定义比较函数
  int intcmp(int a, int b) {
      if (abs(a) == abs(b)) return 0;
      if (abs(a) < abs(b)) return -1;
      else return 1;
  }
  
  // 调用
  int b[] = {7, 9, -4, 3, 8, -1, 2, 5, -6, 0};
  quicksort(b, 0, 9, intcmp);
  ```
  [来源：第7章 p58-59，函数指针的应用]

---

## 总结（来自 PPT 原文）

1. 本章介绍了指针的概念，指针变量的定义、运算
2. 采用指针，可以使数组有多种访问方式
3. 将指针作为形式参数可以使一个函数与其调用函数共享数据
4. 动态分配内存
5. `*`：多级指针、数组指针、函数指针

[来源：第7章 p62，总结]
