# 引用、指针与动态内存

> [!info] 学习导航
> 返回：[[C++ 学习指南]]
> 前置知识：[[计算机系/C++/02 类型转换、变量与生命周期|类型转换、变量与生命周期]]

引用变量是一个别名，也就是说，它是某个已存在变量的另一个名字。

一旦把引用初始化为某个变量，就可以使用该引用名称或变量名称来指向变量。

引用必须在定义时初始化，并且一旦绑定到一个变量后，就不能再绑定到其他变量。

引用的语法如下：

```cpp
int a = 10;
int &ref = a; // ref 是 a 的引用
```

- int &ref 表示 ref 是一个 int 类型的引用。
- ref 是 a 的别名，对 ref 的操作会直接作用于 a。

**C++ 引用 vs 指针**

引用很容易与指针混淆，它们之间有三个主要的不同：

- 不存在空引用，引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象，就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
- 引用的对象必须是一个变量，而指针必须是一个地址。

| **特性**                | **引用**                                                     | **指针**                                                     |
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **定义与初始化**        | 必须初始化，且不能为 null。                                  | 可以不初始化，可以在后续代码中指向其他对象，可以为 null。    |
| **语法**                | 使用 & 声明，例如：int &ref = a;                             | 使用 \* 声明，例如：int \*ptr = &a;                            |
| **重新绑定**            | 不能重新绑定，一旦初始化后始终引用同一个对象。               | 可以重新指向其他对象，例如：ptr = &b;                        |
| **空值（Nullability）** | 不能为 null，必须绑定到有效的对象。                          | 可以为 null，表示不指向任何对象。                            |
| **内存占用**            | 不占用额外内存（编译器通常将其优化为直接操作所引用的对象）。 | 占用额外内存（存储地址，通常是一个机器字长，如4字节或8字节）。 |
| **访问方式**            | 直接使用，无需解引用操作符，例如：ref = 10;                  | 需要使用 \* 解引用操作符访问或修改所指向的对象，例如：\*ptr =  10; |
| **多级间接访问**        | 不支持多级间接访问（不能有引用的引用）。                     | 支持多级间接访问（如指针的指针：int \*\*pptr;）。              |
| **函数参数传递**        | 常用于函数参数传递，语法简洁，例如：void func(int &x) { x = 10; } | 也可以用于函数参数传递，但需要使用解引用操作符，例如：void func(int \*x) { \*x = 10; } |
| **数组与引用**          | 不能直接创建引用数组，但可以创建数组的引用，例如：int (&ref)[10] = arr; | 可以创建指针数组，也可以创建指向数组的指针，例如：int \*ptrArr[10]; |
| **安全性**              | 更安全，不能为 null，且语法更直观。                          | 更灵活，但容易出错（如空指针、野指针等）。                   |
| **底层实现**            | 通常通过指针实现，但编译器会优化为直接操作所引用的对象。     | 直接存储目标对象的内存地址。                                 |

## 引用中的隐藏操作

**(1) 隐藏解引用（\* 操作符）**

- **指针版本**：

```cpp
int x = 10;
int *p = &x; // 显式取地址
*p = 20; // 显式解引用
```


- **引用版本**：

```cpp
int x = 10;
int &r = x; // 编译器自动处理地址（类似 `int *const r = &x`）
r = 20; // 编译器自动解引用（实际生成 `*r = 20`）
```

**隐藏的操作**：**int &r = x** 实际上被编译为 **int \*const r = &x**（但 **r** 的地址不可见）。

**引用不能重新绑定**：

```cpp
int x = 10, y = 20;
int &r = x;
r = y; // 不是重新绑定，而是把 y 的值赋给 x！
```

**隐藏的操作**：引用在初始化后，其绑定的地址固定（类似 **int \*const p**）

**(4) 禁止 NULL 引用（隐藏空指针检查）**

- **指针可以为 nullptr**：

```cpp
int *p = nullptr; // 合法
```


- **引用必须绑定有效对象**：

```cpp
int &r = nullptr; // 编译错误！
```


**隐藏的操作**：编译器确保引用总是绑定有效内存（但可以通过非法手段绕过，如 **int &r = \*(int\*)0**，但这是未定义行为）。

**4. 引用隐藏的操作总结**

| **隐藏的操作**   | **引用示例**                  | **实际编译器行为**     |
| ---------------- | ----------------------------- | ---------------------- |
| **解引用 \***    | **r = 20**                    | **\*(&r) = 20**         |
| **取地址 &**     | **int &r = x**                | **int \*const r = &x** |
| **禁止重新绑定** | **r = y**（赋值，非重绑定）   | 类似 **\*const** 指针   |
| **禁止 nullptr** | **int &r = nullptr;**（错误） | 编译期检查             |


## 指针

## 指针的补充

### 关键对比

| **特性**        | **int \*ptr[N]（指针数组）** | **int (\*ptr)[N]（数组指针）** |
| --------------- | ---------------------------- | ------------------------------ |
| **类型**        | 数组，元素为 **int\***       | 指针，指向 **int[N]**          |
| **ptr 的本质**  | 数组名（地址常量）           | 指针变量                       |
| **sizeof(ptr)** | **N \* sizeof(int\*)**       | **sizeof(int\*)**（指针大小）  |
| **元素访问**    | **ptr[i]**（返回 **int\***） | **(\*ptr)[i]**（返回 **int**） |
| **典型场景**    | 存储多个指针                 | 指向一个完整数组               |

**多维数组传参**：

- 函数接收二维数组时，参数应声明为 **int (\*ptr)[N]**，而不是 **int \*\*ptr**。

### 常见误区

**❌ 误区 1：int \*p = new int; 是空指针**

- **正确**：**p** 指向堆内存，不是 **nullptr**（除非 **new** 失败且用了 **nothrow**）。
- **错误**：认为 **p** 是空指针，导致后续解引用时未初始化错误。

**❌ 误区 2：int \*p; 是空指针**

- **正确**：未初始化的指针是**野指针**（指向随机地址），不是 **nullptr**。
- **错误**：假设未初始化的指针默认是 **nullptr**（C++ 不会自动初始化局部变量）。

```cpp
int *p = nullptr; // p 是空指针
```


直接 **int \*p;**（不初始化）是**未定义行为**，可能是野指针（指向随机地址）


| **概念**   | **定义**                     | **示例**                | **后果**                 |
| ---------- | ---------------------------- | ----------------------- | ------------------------ |
| **空指针** | 显式指向 **nullptr** 的指针  | **int  \*p = nullptr;** | 解引用时崩溃（明确错误） |
| **野指针** | 指向无效内存的指针（未置空） | **delete  p;** 后未置空 | 解引用导致 **UB**        |

1. 空指针（Null Pointer）

定义

空指针是指 明确赋值为 nullptr（C++11 起）或 NULL（传统 C）的指针，表示它不指向任何有效内存地址。


示例：


```cpp
int *p = nullptr; // 空指针
if (p == nullptr) {
  cout << "p 是空指针";
}
```

特点

安全：解引用空指针会导致 编译错误或运行时崩溃（如 Segmentation Fault），但这是可预测的行为。


用途：用于初始化指针或表示“无目标”。


2. 野指针（Dangling Pointer）

定义

野指针是指 指向已释放或无效内存的指针，但指针本身未被置空。


常见场景：


释放后未置空：


```cpp
int *p = new int(5);
delete p; // p 变为野指针（仍保留原地址）
// cout << *p; // 未定义行为！
```

指向局部变量（栈内存失效）：


```cpp
int *getPointer() {
  int x = 10;
  return &x; // 返回局部变量的地址（函数结束后 x 失效）
}
int *p = getPointer(); // p 是野指针
```

### 指针到底是什么？

**"指针其实就是地址"这句话基本是正确的，但需要一些补充和澄清**：

1. **本质关系**：
   - 指针的本质是存储内存地址的变量，它的值确实代表一个内存地址。
   - 例如在C语言中：**int \*p = &a;**，这里**p**存储的就是变量**a**的内存地址。

### 数组名与首元素地址

即存储数组首元素的指针，其本身也存放在数组首元素的地址 即a的内容和&a的内容是一样的!

- **数组名 a 会被隐式转换为首元素的指针**（即 建立一个int \*a=**&a[0]**），类型是 **T\***（例如 **int\***）。

**即存储在数组首元素的指针，其本身也存放在数组首元素的地址**

- **&a 的类型是“指向整个数组的指针”（即 int (\*)[5]），而非指向单个元素的指针。**

**虽然 &a 和 a 的数值相同（都指向内存的同一位置，如 0x0004），但它们的类型不同，导致指针运算行为不同：**

即 `*a = &a;`
**即a的地址和a存储的值是一样的。**

```cpp
printf("%p\n", &a); // 输出 0x0004（与 a 相同）
printf("%p\n", &a + 1); // 输出 0x0018（假设 int 占 4 字节，跳过整个数组 5*4=20 字节）
printf("%p\n", a + 1); // 输出 0x0008（跳过 1 个 int，4 字节）
  int a[5] = {1, 2, 3, 4, 5};
  printf("%p\n", a); // 输出数组首元素地址（如 0x0004）
  printf("%p\n", &a[0]); // 输出数组首元素地址（如 0x0004）
  printf("%p\n", &a); // 输出整个数组的地址（如 0x0004），与首元素地址相同
```

**3. 为什么可以对 a 取地址？**

- **数组名 a 是一个标识符**，代表整个数组对象，而非单纯的指针变量。
- C/C++ 允许对任何对象（包括数组）使用 **&** 运算符获取其地址。
- **关键区别**：
  - **a**：隐式转换为 **int\***（指向首元素）。
  - **&a**：显式获取数组对象的地址，类型是 **int      (\*)[5]**。

**4. 类型系统的视角**

| **表达式** | **类型**        | **含义**                     |
| ---------- | --------------- | ---------------------------- |
| **a**      | **int\***       | 首元素地址（隐式转换）       |
| **&a**     | **int (\*)[5]** | 整个数组的地址（指针到数组） |
| **a[0]**   | **int**         | 数组第一个元素               |

**核心结论**：
 **可以对数组名 a 取地址（&a），因为 a 是一个完整的数组对象，而非单纯的指针。&a 的类型是指向数组的指针，与 a 的首元素指针类型不同，但地址值相同。**

```cpp
int a[5][2] = {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}};
```

| **表达式** | **类型**             | **隐式转换行为（退化）**             | **实际含义**                     |
| ---------- | -------------------- | ------------------------------------ | -------------------------------- |
| **a**      | **int[5][2]**        | 退化为 **int (\*)[2]**               | 指向第一个子数组 **a[0]** 的指针 |
| **&a**     | **int (\*)[5][2]**   | 无                                   | 指向整个二维数组的指针           |
| **a[0]**   | **int[2]**           | 退化为 **int\***                     | 指向 **a[0][0]** 的指针          |
| **\*a**     | **int[2]**（解引用） | 退化为 **int\***（与 **a[0]** 相同） | 指向 **a[0][0]** 的指针          |

```cpp
  printf("a:   %p (类型: int (*)[2])\n", a);
  printf("&a[0]: %p (类型: int (*)[2])\n", &a[0]);
  printf("&a:   %p (类型: int (*)[5][2])\n", &a);
  printf("*a:   %p (类型: int*, 指向 a[0][0])\n", *a);
  printf("a[0]:  %p (同 *a)\n", a[0]);
  printf("&a[0][0]: %p (同 *a)\n", &a[0][0]);
```

虽然 **a**、**&a**、**\*a** 的地址值相同，但类型不同导致指针运算结果不同：

```cpp
printf("a + 1:  %p (跳过 1 个子数组, +8 字节)\n", a + 1);
printf("&a + 1: %p (跳过整个数组, +40 字节)\n", &a + 1);
printf("*a + 1: %p (跳过 1 个 int, +4 字节)\n", *a + 1);
```

- **printf("%p\n",     \*a); 在二维数组中合法**：
  - **\*a** 退化为 **int\***，与 **%p** 兼容，输出 **a[0][0]** 的地址。
- **与一维数组的区别**：
  - 一维数组的 **\*a** 是值（**int**），二维数组的 **\*a** 是指针（**int\***）。
- **核心原因**：
  - 二维数组的层级解引用触发了两次隐式转换（**a → int      (\*)[2]** → **\*a → int\***），而一维数组只有一次（**a → int\*** → **\*a → int**）。

**关键结论**：
 **在二维数组中，\*a 退化为指向首元素的指针（int\*），因此可以用 %p 输出地址；而在一维数组中，\*a 是整数值，与 %p 不兼容。**

然而小心未定义行为

- **一维数组 int b[5]**：
  - **\*b** 是 **b[0]**（**int** 类型），与 **%p** 不匹配，导致未定义行为。
- **二维数组 int a[5][2]**：
  - **\*\*a** 是 **a[0][0]**，int 类型，导致未定义行为


## `int *p` 与 `int* p`

### `int *p`

更符合C的习惯

\*p是一个int类型的变量

结合硬件层面来说，指针只是保存内存的一个地址，不管你保存的是int char还是long，指针就是个固定长度的[内存地址](https://zhida.zhihu.com/search?content_id=47510914&content_type=Answer&match_order=1&q=内存地址&zhida_source=entity)，**它本身没有类型**，只是可以把它指向的内存中保存的数据解析为各种类型。

所以，指针没有类型，所以不存在xx类型的[指针](https://zhida.zhihu.com/search?content_id=47510914&content_type=Answer&match_order=4&q=指针&zhida_source=entity)这一说，只存在这个指针保存的数据是什么类型。因此，星号（\*）应该和变量结合，而不应该和类型结合。

[\*]是运算符，所以它的正确使用是作用在操作数上[而不是和int类型组合]，这在第二幅图片也有说明即[表达式](https://zhida.zhihu.com/search?content_id=47510914&content_type=Answer&match_order=1&q=表达式&zhida_source=entity)\*ip。

因此，int表示的是[\*ip]这个表达式的类型。从表达式[\*ip]反向推出未使用\*操作的ip是个地址。

### `int* p`

C++的习惯

Int\*是一种类型——>指向int 的指针。P的类型是int\*

我们采取第一种方式，即int \*p。


## C++ 常量

**常量是固定值，在程序执行期间不会改变。这些固定的值，又叫做字面量。**

**常量可以是任何的基本数据类型，可分为整型数字、浮点数字、字符、字符串和布尔值。**

**常量就像是常规的变量，只不过常量的值在定义后不能进行修改。**

`#define` 预处理器
**下面是使用 #define 预处理器定义常量的形式：**

**const 关键字**

**您可以使用 const 前缀声明指定类型的常量，如下所示：**

```cpp
const type variable = value;
```

**常量必须在定义时初始化**

**Const int \* p=&I,常量指针，不可通过指针改变值，但是指针可以改变指向的地址**

**Int \* const p =&I,指针常量，不可改变指向的地址，但是可以改变值**


## 动态内存


- **栈：**在函数内部声明的所有变量都将占用栈内存。
- **堆：**这是程序中未使用的内存，在程序运行时可用于动态分配内存。

很多时候，您无法提前预知需要多少内存来存储某个定义变量中的特定信息，所需内存的大小需要在运行时才能确定。

在 C++ 中，您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存，这会返回所分配的空间地址。这种运算符即 **new** 运算符。

如果您不再需要动态分配的内存空间，可以使用 **delete** 运算符，删除之前由 new 运算符分配的内存。

**new 和 delete 运算符**

下面是使用 new 运算符来为任意的数据类型动态分配内存的通用语法：

```cpp
new data-type;
```

在这里，**data-type** 可以是包括数组在内的任意内置的数据类型，也可以是包括类或结构在内的用户自定义的任何数据类型。让我们先来看下内置的数据类型。例如，我们可以定义一个指向 double 类型的指针，然后请求内存，该内存在执行时被分配。我们可以按照下面的语句使用 **new** 运算符来完成这点：

```cpp
double* pvalue = NULL; // 初始化为 null 的指针
 pvalue = new double; // 为变量请求内存
```

如果自由存储区已被用完，可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针，并采取以下适当的操作：

```cpp
double* pvalue = NULL;
if( !(pvalue = new double ))
 { cout << "Error: out of memory." <<endl;
exit(1); }
```

- **赋值表达式的结果**：
       C/C++ 中，赋值表达式（如 **a = b**）的返回值是赋值后的左值（即 **a** 的值）。因此 **pvalue     = new double** 的返回值是 **pvalue** 的值（即新分配的地址或 **nullptr**）。

**malloc()** 函数在 C 语言中就出现了，在 C++ 中仍然存在，但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比，其主要的优点是，new 不只是分配了内存，它还创建了对象。

在任何时候，当您觉得某个已经动态分配内存的变量不再需要使用时，您可以使用 delete 操作符释放它所占用的内存，如下所示：

```cpp
delete pvalue; // 释放 pvalue 所指向的内存
```

**数组的动态内存分配**

假设我们要为一个字符数组（一个有 20 个字符的字符串）分配内存，我们可以使用上面实例中的语法来为数组动态地分配内存，如下所示：

```cpp
char* pvalue = NULL; // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存
```

要删除我们刚才创建的数组，语句如下：

```cpp
delete [] pvalue; // 删除 pvalue 所指向的数组
```

下面是 new 操作符的通用语法，可以为多维数组分配内存，如下所示：

**一维数组**

```cpp
// 动态分配,数组长度为 m int *array=new int [m]; //释放内存 delete [] array;
```

**二维数组**

```cpp
int array; // 假定数组第一维长度为 m， 第二维长度为 n // 动态分配空间 array = new int *[m]; for( int i=0; i<m; i++ ) { array[i] = new int [n]; } //释放 for( int i=0; i<m; i++ ) { delete [] array[i]; }
int *array;
// 假定数组第一维为 m， 第二维为 n， 第三维为h
// 动态分配空间
array = new int [m];
for( int i=0; i<m; i++ )
{
  array[i] = new int *[n];
  for( int j=0; j<n; j++ )
  {
    array[i][j] = new int [h];
  }
}
// 释放
for( int i=0; i<m; i++ )
{
  for( int j=0; j<n; j++ )
  {
    delete[] array[i][j];
  }
  delete[] array[i];
}
delete[] array;
```

delete[] p和delete p对于简单类型的释放没什么太大区别，前者确实会释放整个数组内存，后者只释放数组开头元素的内存在delete操作后p实际上成为了野指针，不再指向任何有效值。这也是为什么打印出了两个奇怪值的原因。

new 的功能是在堆区新建一个对象，并返回该对象的指针。

所谓的**【新建对象】**的意思就是，将调用该类的构造函数，因为如果不构造的话，就不能称之为一个对象。

而 malloc 只是机械的分配一块内存，如果用 mallco 在堆区创建一个对象的话，是不会调用构造函数的。

严格说来用 malloc 不能算是新建了一个对象，只能说是分配了一块与该类对象匹配的内存而已，然后强行把它解释为【这是一个对象】，按这个逻辑来，也不存在构造函数什么事。

同样的，用 delete 去释放一个堆区的对象，会调用该对象的析构函数。

用 free 去释放一个堆区的对象，不会调用该对象的析构函数。

## 相关内容
- [[计算机系/C++/07 拷贝控制与移动语义|拷贝控制与移动语义]]
- [[计算机系/C++/13 异常处理与智能指针|异常处理与智能指针]]

---

## 学习导航
- 上一篇：[[计算机系/C++/02 类型转换、变量与生命周期|类型转换、变量与生命周期]]
- 返回目录：[[C++ 学习指南]]
- 下一篇：[[计算机系/C++/04 输入输出、文件与流|输入输出、文件与流]]
