# 函数参数传递

## 1. 先看判断标准

判断参数传递方式，最直接的问题是：**在被调用过程内给形参重新赋值，调用方的变量是否会改变？**

- 会改变：通常称为**按引用传递**；形参是调用方变量的别名。
- 不会改变：属于**按值传递**；形参获得的是一个独立的值。

如果这个值本身是地址、指针或对象引用，就可能出现“能修改对象内容，但不能改变调用方变量绑定”的现象。

## 2. 按值传递（pass-by-value）

按值传递把实参的值复制给形参。形参是被调用过程中的独立变量，对形参重新赋值不会影响调用方。

```cpp
void f(int x) {
    x = 100; // 只修改形参
}

int n = 1;
f(n);
// n 仍然是 1
```

**按值传递复制的是“值”，不一定是整个对象。** 这个值可能是：

- 基本数据，例如 `int` 的数值；
- 指针或地址；
- 对象引用值或其他句柄值；
- 一个完整的类对象。

因此，按值传递不等于深拷贝。是否共享对象，要看被复制的值是什么。

### “对象引用值”是什么意思

**对象引用值**是一个用于找到或访问对象的值。它通常类似于地址或句柄，但不一定是程序可以直接观察到的内存地址。

```cpp
int x = 10;
int* p = &x;
int* q = p; // 复制地址值；p 和 q 是两个独立的指针变量
```

此时 `p` 和 `q` 保存相同的地址，并指向同一个 `x`：

```cpp
*q = 20;      // 修改共同指向的对象，x 也变成 20
q = nullptr;  // 只修改 q 保存的地址值，p 不变
```

Java 中的对象参数也可以用类似方式理解：

```java
Person a = new Person();
Person b = a; // 复制对象引用值，不复制 Person 对象
```

`a` 和 `b` 是两个独立的变量，但保存的引用值指向同一个对象。因此，修改对象内容通常对双方可见；让 `b` 重新指向另一个对象，不会改变 `a`。

所以，“按值传递对象引用值”表示：**复制的是指向对象的值，不是对象本身，也不是调用方变量的别名。**

## 3. 按地址传递：按值传递地址

按地址传递不是与按值传递并列的基本类别，而是**按值传递的一种具体形式**：把地址作为一个值复制给形参。C 语言常用指针实现这种方式。

```cpp
void change(int *p) {
    *p = 100;    // 修改 p 指向的对象，调用方可见
    p = nullptr; // 修改地址副本，调用方的指针不变
}

int x = 1;
int *q = &x;
change(q);
// x == 100，q 仍然指向 x
```

必须区分：

- `*p = 100`：修改地址指向的目标对象；
- `p = nullptr`：修改形参 `p` 保存的地址值。

如果要修改调用方的指针变量本身，需要传递指针的地址（如 `int**`），或在 C++ 中使用指针的引用（如 `int*&`）。

## 4. 按引用传递（pass-by-reference）

按引用传递使形参成为调用方变量的别名。对形参赋值，就是对调用方变量赋值。

```cpp
void change(int& x) {
    x = 100; // 直接修改调用方变量
}

int n = 1;
change(n);
// n == 100
```

C++ 的 `T&` 和 C# 的 `ref` 参数属于这种语义。C 语言没有语言级的引用参数，只能通过指针模拟类似效果。

## 5. 按共享传递（pass-by-sharing）

按共享传递是对某些语言行为的描述，常用于 Python、Java 对象参数和 JavaScript 对象参数。它可以理解为：**按值传递对象引用值**。

- 形参获得对象引用值的副本；
- 调用方和被调用方的引用可能指向同一个对象；
- 通过形参修改对象内容，调用方通常可见；
- 重新绑定形参，调用方的变量不变。

Python 示例：

```python
def modify(obj):
    obj[0] = 100  # 修改共享对象，调用方可见
    obj = [4, 5, 6]  # 只重新绑定形参

original = [1, 2, 3]
modify(original)
print(original)  # [100, 2, 3]
```

Java 示例：

```java
void modify(List<Integer> list) {
    list.set(0, 100); // 修改共享对象，调用方可见
    list = new ArrayList<>(Arrays.asList(4, 5, 6)); // 重新绑定形参，调用方不可见
}
```

Java 语言规范通常直接说 Java 是**按值传递**；“按共享传递”是为了更准确描述对象引用行为的补充术语，不是与按值、按引用并列的第三种规范类别。

|传递方式|拷贝了什么|
|---|---|
|值传递|对象数据（完整副本）|
|指针传递|指针的值（地址）|
|引用传递|**什么对象都不拷贝**（别名绑定）|

传递方式	是否拷贝对象	拷贝了什么
传值 T f(T x)	是	完整对象（调用拷贝/移动构造）
传引用 T f(T& x)	否	无
传 const 引用 T f(const T& x)	否	无
传指针 T f(T* x)	否（对象不拷贝）	指针的值（地址）
传右值引用 T f(T&& x)	否（绑定到临时/被移动对象）	无

### C++ 类对象参数与按共享传递的区别

不能只看形参名称是否写成 `obj`，还要看形参的**类型**以及类型中保存的内容。

```cpp
void byValue(Person obj); // obj 是一个新的 Person 对象
```

调用 `byValue(p)` 时，`obj` 通常由 `p` 复制或移动构造得到。此时传递的是 `Person` 对象本身，因此属于按值传递：

```cpp
void byValue(Person obj) {
    obj.name = "Tom"; // 不影响调用方的 p
}
```

Java 中虽然也写成：

```java
void modify(Person obj)
```

但 `obj` 通常是一个保存对象引用值的局部变量。调用时复制的是引用值，不是整个 `Person` 对象，所以形参和实参可能指向同一个对象，这就是按共享传递的表现。

C++ 中也可以通过指针或智能指针获得类似效果：

```cpp
void byPointer(Person* obj) {
    obj->name = "Tom"; // 修改共享对象，调用方可见
    obj = nullptr;     // 只修改指针副本，调用方指针不变
}

void bySharedPointer(std::shared_ptr<Person> obj) {
    obj->name = "Tom"; // 修改共享对象，调用方可见
    obj.reset();        // 只修改智能指针副本
}
```

| C++ 参数形式 | 实际传递的内容 | 是否复制类对象本身 | 形参重新绑定是否影响调用方 |
|---|---|---:|---:|
| `Person obj` | `Person` 对象的值 | 是（通常复制或移动） | 否 |
| `Person& obj` | 外部对象的别名 | 否 | 适用按引用语义 |
| `Person* obj` | 地址值的副本 | 否 | 否 |
| `std::shared_ptr<Person> obj` | 智能指针值的副本 | 否 | 否 |

因此，**C++ 的 `Person obj` 默认是按值传递对象本身；Java 的 `Person obj` 通常是按值传递对象引用值，因而表现为按共享传递。** 形参名称相同，不代表传递语义相同。

## 6. 不要混淆三种“引用”

“引用”一词在不同语境中可能含义不同：

| 术语 | 含义 |
|---|---|
| C++ `T&` | 变量的别名；语言语义上不是一个独立的引用值 |
| C++ `T*` | 指针值，保存地址；可以复制，按值传递时复制地址值 |
| Java/Python 对象引用 | 指向对象的引用值；可以复制，通常表现为按共享传递 |

这里的“对象引用”是广义的间接访问概念，不等同于 C++ 语言中的 `T&`：

- **对象本身**：实际的数据实体，具有类型和生命周期；
- **对象引用值**：用于找到或访问对象的值，通常可以复制；
- **C++ `T&` **：调用方变量的别名，语言语义上不是独立的引用值；
- **C++ `T*` **：保存地址的指针对象，地址值可以复制；
- **Java/Python 对象引用**：语言管理的对象引用值，通常不能直接进行指针运算。

C++ 同时具有别名型和句柄型的“引用机制”：

- `T&`：别名型，适合实现按引用传递；
- `T*`、`std::unique_ptr<T>`、`std::shared_ptr<T>`：句柄型或指针型，传参时通常按值复制；
- `std::reference_wrapper<T>`：可复制的引用包装器。

因此，**C++ 也有对象，也有对象引用**。准确的区别不是“C++ 有没有对象引用”，而是引用属于哪种语义：别名，还是可以复制的地址/句柄值。

## 7. 语言与术语对照

| 语言或机制 | 实际传递的内容 | 重新赋值形参是否影响调用方 |
|---|---|---|
| C `int f(int x)` | 整数值的副本 | 否 |
| C `int f(int *p)` | 地址值的副本 | 否，但可通过 `*p` 修改目标对象 |
| C++ `void f(int& x)` | 调用方变量的别名 | 是 |
| C++ `void f(T* p)` | 指针值的副本 | 否，但可通过 `*p` 修改目标对象 |
| Java 对象参数 | 对象引用值的副本 | 否，但可修改共享对象 |
| Python 对象参数 | 对象引用/绑定关系的副本 | 否，但可修改共享对象 |

## 8. 参数传递与浅拷贝、深拷贝

### 先区分三个操作

**复制引用不等于浅拷贝对象。** 参数传递讨论形参如何获得值或绑定对象；浅拷贝、深拷贝讨论创建对象副本时复制到哪一层。这是两个不同的问题。

| 操作 | 是否创建新的外层对象 | 嵌套对象的处理 | Python 示例 |
|---|---|---|---|
| 复制引用／建立另一个绑定 | 否 | 仍访问原对象 | `b = a` |
| 浅拷贝 | 是 | 外层元素的引用被复制，嵌套对象仍共享 | `b = a.copy()`（列表） |
| 深拷贝 | 是 | 按深拷贝规则递归复制嵌套对象 | `b = copy.deepcopy(a)` |

浅拷贝会复制外层对象的内容，并非“完全不复制数据”。深拷贝也不意味着每个可达实体都一定新建：不可变对象可能被复用，自定义类型可以规定复制行为，文件等外部资源不一定可复制。

> 把“传参机制”和“拷贝方式”当成一回事，并且把“复制引用值”错误地称为“浅拷贝”。

正确的说法是：

- 基本类型按值传递：复制值。
    
- 对象按值传递：复制对象，深浅取决于拷贝语义。
    
- 对象引用按值传递：复制引用值，不复制对象。
### 用嵌套列表区分三者

```python
import copy

a = [[1, 2], [3, 4]]
alias = a
shallow = a.copy()
deep = copy.deepcopy(a)

assert alias is a
assert shallow is not a
assert shallow[0] is a[0]
assert deep[0] is not a[0]

shallow[0][0] = 100  # 修改共享的内层列表
print(a)             # [[100, 2], [3, 4]]
print(deep)          # [[1, 2], [3, 4]]

shallow[1] = [9, 9]  # 替换浅拷贝列表自己的一个元素
print(a)             # [[100, 2], [3, 4]]，不受这次替换影响
```

因此，不能笼统说“浅拷贝后的修改都会影响原对象”：修改新外层对象通常不影响原外层对象，修改共享的可变内层对象才会被双方看到。

### 按值传递或按共享传递属于哪一种拷贝？

- **基本值按值传递**：例如 C/Java 的整数参数，形参获得相应的值。通常不使用深浅拷贝术语描述这种情况。
- **对象引用按值传递／按共享传递**：不自动复制被引用的对象，因此既没有执行对象的浅拷贝，也没有执行对象的深拷贝。
- **C++ 类对象按值传递**：形参对象如何初始化由类型及实参决定，可以涉及复制、移动或直接构造。发生复制时，是否共享内部数据取决于成员类型和复制构造函数，不能直接判定为深拷贝。当你把一个对象**按值传递**（或者用 `=` 拷贝）时，C++ 会调用这个对象的**拷贝构造函数**来决定“到底怎么复制这个对象”。  
	- 如果拷贝构造函数只复制成员的值（包括指针值），那就是浅拷贝；  
	- 如果拷贝构造函数额外为新对象分配内存、复制指针指向的数据，那就是深拷贝。
- **Python 不可变对象**：也遵循对象绑定的规则；重新给形参赋值不会改变调用方绑定，并不说明传参时深拷贝了对象。

“对象的一个拷贝”这句话本身不足以判断深浅。应查看具体语言、类型或复制操作的定义。如果实际只复制引用，应明确写成“对象引用值的副本”，而不是“对象副本”。

### Java 示例：重新绑定后，修改的是新对象

以下片段假设已导入 `java.util` 中的相关类型，方法位于类中，调用代码位于方法中：

```java
void modify(List<Integer> list) {
    list = new ArrayList<>(Arrays.asList(4, 5, 6));
    list.set(0, 100); // 此时修改的是新列表
}

List<Integer> original = new ArrayList<>(Arrays.asList(1, 2, 3));
modify(original);
System.out.println(original); // [1, 2, 3]
```

原文中的 `[100, 2, 3]` 是错误的。只有把 `list.set(0, 100)` 放在重新赋值之前，才会修改原列表，得到该结果。这两种情况都没有对原列表执行浅拷贝。

### 修改何时对调用方可见？

当形参获得对象引用值后，应看操作实际作用于哪个对象：

| 操作 | 对调用方的影响 |
|---|---|
| `x = y`：重新绑定局部形参 | 不改变调用方变量的绑定 |
| 通过 `x` 修改双方仍共享的可变对象 | 调用方可以观察到修改 |
| `x` 已指向新对象，再修改新对象 | 不会因此修改原对象 |
| 修改对象副本自己的普通数据成员 | 通常不改变原对象相应成员 |
| 通过副本内部的指针修改共享数据 | 仍可能影响调用方 |

所以，“传递了对象副本，所有修改都不可见”也不成立。例如 C++ 的类含有指针成员时，复制外层对象仍可能共享指针指向的数据。判断时要追踪具体被修改的对象，而不只看是否发生过复制。

伪代码也应先明确记号：若 `x` 表示对象、`f` 是它的属性，则旧式写法 `f[x] <- 3` 可以写成更直观的 `x.f <- 3`。如果 `f` 是数组，`f[x]` 则表示数组元素，不能混用这两种解释。

## 9. 最终结论

1. **规范层面的核心分类通常是按值传递和按引用传递。**
2. **按地址传递本质上是按值传递地址。**
3. **按共享传递是描述对象引用按值复制后共享对象的术语。**
4. **按值传递复制的是值；这个值可能是数据、地址、指针、句柄或对象本身。**
5. **C++ 的 `T&` 是变量别名；C++ 指针、智能指针以及 Java/Python 对象引用是可复制的引用值或句柄。**
6. 判断可见性时，要分别观察：
   - 重新赋值形参是否影响调用方变量；
   - 通过形参修改目标对象是否影响调用方。



# ****P****art 1 ****值传递****

### 1.1、什么是值传递？

值传递是最基础的参数传递方式。核心是 “**传递实参的完整副本**”—— 当你调用一个值传递的函数时，编译器会在函数的栈帧里，为形参创建一个独立的内存空间，然后把实参的所有数据 “原封不动” 地拷贝到这个空间里。

举个具体例子：

咱们在 `main` 函数里定义 `int num = 5`，`num` 会存在 `main` 函数的栈帧中（假设地址是 `0x0012ff44`）；当调用 `increment(num)` 时，编译器会在 `increment` 函数的栈帧里，开辟一块新内存给形参 `x`（比如地址 `0x0012ff00`），然后把 `num` 的值（5）拷贝到 `x` 的内存里。

这就意味着：

函数内部操作的 `x`，和 `main` 里的 `num` 是**两块完全独立的内存**—— 哪怕你把 `x` 改得面目全非，`num` 也不会受任何影响；等函数执行结束，`increment` 的栈帧被销毁，`x` 的内存也会被释放，整个过程对原变量毫无副作用。

**示例**：

```
#include <iostream>
```

### 1.2、值传递的底层原理

- **内存分配：函数调用时会在栈上分配新空间存储形参副本。**
    
- **复制开销：对于基本类型（如** `int`）几乎无开销，但对大型对象（如 `std::string`）会触发构造函数和析构函数调用。
    

### 1.3、值传递的 3 个关键特点

**1）、安全性拉满，但拷贝开销是硬伤**  

值传递的 “独立性” 是最大优势 —— 函数再怎么操作形参，都不会影响原始数据，完全符合 “无副作用函数” 的设计原则，特别适合处理敏感数据（比如用户密码、配置参数）。但问题在于 “拷贝开销”：

- 如果传递的是 `int`、`float` 这类基本类型（通常 4~8 字节），拷贝速度极快，开销可忽略；
    
- 但如果传递的是**大型对象**（比如包含 1000 个元素的 `std::vector`、有多个成员的复杂类），拷贝就需要复制所有成员数据 —— 比如一个 `vector<int> vec(1000, 1)`，拷贝时要复制 1000 个 `int`（共 4000 字节），如果频繁调用这个函数，性能损耗会非常明显。
    

**2）、会触发对象的拷贝构造函数**  

对于自定义类对象，值传递不仅拷贝成员数据，还会显式调用**拷贝构造函数**（如果用户没定义，编译器会生成默认拷贝构造）。这一点很容易被忽略，比如：

```
class MyClass {
```

如果 `MyClass` 的拷贝构造函数里有复杂逻辑（比如深拷贝动态内存），值传递的开销会进一步增大。

**3）、形参的生命周期独立于实参**  

形参只在函数执行期间存在（位于函数栈帧），函数结束后会自动销毁（调用析构函数，若有），不会和实参的生命周期产生关联。这一点比引用 / 指针更安全，不用担心 “悬空引用”“野指针” 的问题。

### 1.4、值传递的适用场景

### **场景 1：传递基本数据类型**  

### 比如 `int`、`char`、`bool`、`double` 等，拷贝开销小，安全性优先。例如实现一个 “计算两数之和” 的函数：

```
int add(int a, int b) { return a + b; }  // 值传递最适合
```

**场景 2：传递小型结构体 / 对象**  

比如包含 2~3 个成员的结构体（如表示坐标的 `Point`）、无复杂成员的类，拷贝开销可接受。例如：

```
struct Point {
```

**场景 3：函数无需修改原始数据，且需保证数据安全**  

比如处理用户输入的验证函数、日志打印函数，用值传递可避免意外篡改原始数据。

##   

# ****P****art 2**** 引用传递****

### 2.1、什么是引用传递？

引用（`&`）本质是变量的 “别名”—— 它没有独立的内存空间，而是和原始变量 “绑定” 在一起，共享同一块内存地址。引用传递的核心是 “**传递别名关系**”，而非数据本身。

咱们先澄清一个常见误区：“引用是指针的语法糖”—— 从底层实现看，编译器确实会把引用当作 “隐式指针” 处理（比如在 64 位系统中，引用的底层也是 8 字节的地址），但语法上做了严格限制：

- 引用必须在定义时**立即绑定变量**（不能像指针那样 “先定义，后赋值”）；
    
- 引用一旦绑定，就不能再指向其他变量（指针可以随时改指向）；
    
- 引用不能绑定 `nullptr`（指针可以指向空）。
    

这些限制让引用比指针更安全，同时又保留了 “直接操作原始数据” 的高效性。比如调用 `increment(num)` 时，形参 `int& x` 是 `num` 的别名 —— `x` 的地址和 `num` 完全相同，操作 `x` 就等同于操作 `num`。

## 示例：

```
#include <iostream>
```

### 2.2、引用传递的底层原理

- **别名机制：引用本质是变量的别名（C++标准规定引用必须绑定到已存在的对象）。**
    
- **零开销：无需复制数据，直接通过内存地址访问。**
    

### 2.3、常量引用与非常量引用

```
// 常量引用：禁止修改实参
```

**常量引用的优势**：

- 避免无意修改数据
    
- 支持临时对象绑定（如 `print("hello")`）
    

### 2.4、引用传递的 4 个核心特点

**1）、零拷贝 overhead，效率拉满**  

引用传递不需要拷贝任何数据，只需要在编译期建立 “别名绑定”—— 哪怕传递的是 1 GB 的大对象，开销也只是 “绑定地址”（底层指针的操作），比值传递的效率高几个数量级。这也是为什么传递 `std::string`、`std::vector` 这类大型容器时，几乎都用引用。

**2）、const 引用是 “只读保护神”**  

如果咱们不需要修改原始数据，一定要用 `const T&`（常量引用）—— 它有两个关键作用：

- 禁止函数修改原始数据，保证安全性（比如 `void print(const vector<int>& vec)`，函数里不能改 `vec` 的元素）；
    
- 允许绑定**临时变量**（非 const 引用不行）。
    

比如：

```
// 非const引用：不能传临时变量
```

### 这一点在实战中非常常用 —— 比如函数参数是 `const string&`，调用时既可以传变量，也可以传字符串字面量，灵活性更高。

**3）、引用的生命周期必须 “小于等于” 原始变量**  

这是引用传递最容易踩的坑 —— 如果引用绑定的变量被销毁了，引用就会变成 “悬空引用”，再访问就会触发未定义行为（程序崩溃、乱码等）。比如：

```
// 错误示例：返回局部变量的引用
```

解决办法：确保引用绑定的变量是 “长生命周期” 的（比如全局变量、堆上的变量、main 函数里的变量）。

**4）、不会触发拷贝构造函数**  

因为引用传递不拷贝数据，所以自定义类对象传递时，不会调用拷贝构造函数 —— 这也是比值传递高效的重要原因。比如：

```
class MyClass {
```

### 2.5、引用传递的适用场景

### **场景 1：传递大型对象 / 容器，且需避免拷贝**  

### 比如 `std::vector`、`std::map`、自定义的大体积类（如 `Image`、`DataBuffer`），用引用传递可节省大量拷贝时间。例如：

```
// 处理大型vector，用const引用避免拷贝，且禁止修改
```

**场景 2：函数需要修改原始数据**  

比如实现 “排序函数”“数据更新函数”，用引用直接修改原始数据，无需通过返回值传递结果。例如：

```
// 交换两个整数的值，用引用直接修改原始变量
```

**场景 3：实现多返回值（比结构体更简洁）**  

C++ 函数只能有一个返回值，但用引用参数可以实现 “多返回值”—— 比如计算一个数组的 “总和” 和 “平均值”：

```
#include <vector>
```

##   

# ****P****art 3**** 指针传递****

### 3.1、什么是指针传递？和引用有啥本质区别？

指针是存储变量**内存地址**的变量 —— 它有自己独立的内存空间（比如 64 位系统中占 8 字节），存储的是另一个变量的地址。指针传递的核心是 “**传递指针的副本**”—— 函数接收的形参是实参指针的拷贝，两者指向同一块内存，但形参本身是独立的（修改形参的指向不会影响实参）。

咱们先理清 “指针传递” 和 “引用传递” 的核心区别：

|   |   |   |
|---|---|---|
|对比维度|引用传递|指针传递|
|内存空间|无独立空间（别名）|有独立空间（存地址）|
|初始化要求|必须立即绑定变量|可先定义，后赋值|
|指向修改|一旦绑定，不能改指向|可随时修改指向|
|空值支持|不能指向 nullptr|可指向 nullptr|

比如调用 `increment(&num)` 时，实参是 `num` 的地址（`&num`），形参 `int* x` 是这个地址的副本 —— `x` 的内存里存的是 `num` 的地址，所以解引用 `*x` 就能操作 `num`；但如果修改 `x` 本身（比如让 `x` 指向另一个变量 `temp`），实参指针不会受影响，因为 `x` 只是副本。

**示例**：

```
#include <iostream>
```

### 3.2、指针传递的 4 个关键特点

**1）、灵活性最高，但安全性最低**  

指针的 “可修改指向” 是最大优势 —— 比如遍历链表时，指针可以从 “当前节点” 指向 “下一个节点”；但这也是风险点：如果操作不当，很容易出现**空指针**（指向 `nullptr`）或**野指针**（指向已销毁的内存）。

举个野指针的典型场景：

```
// 错误示例：返回局部变量的指针
```

避坑办法：

- 指针使用前必须检查 `nullptr`（用 `if (p != nullptr)`）；
    
- 避免返回局部变量的指针；
    
- 动态内存分配的指针（`new` 出来的），用完后必须 `delete`，避免内存泄漏。  
    

2）、传递数组的 “唯一方式”  

C++ 中数组名本质是 “指向首元素的指针”，传递数组时，实际上是传递数组首元素的指针（即指针传递）。比如：

```
// 传递数组：arr是指向首元素的指针，len是数组长度（必须显式传递，数组名不含长度信息）
```

这里要注意：数组传递时不会拷贝整个数组，只会传递首元素地址，所以必须显式传递数组长度（否则函数不知道数组有多少元素）。

**3）、二级指针：修改一级指针的指向**  

前面说过 “指针传递时，修改形参指针的指向不会影响实参”—— 那如果咱们想在函数里修改一级指针的指向（比如动态分配内存），该怎么办？答案是**二级指针**（指针的指针，`T**`）。

比如在函数里为一级指针分配动态内存：

```
#include <iostream>
```

这里的关键逻辑：`&arr` 是一级指针 `arr` 的地址（二级指针），函数里 `*ptr` 就是 `arr` 本身，所以 `*ptr = new int[size]` 相当于直接修改 `arr` 的指向，让它指向新分配的堆内存。

**4）、兼容 C 语言代码**  

C 语言没有 “引用” 特性，所有需要 “操作原始数据” 的场景都用指针 —— 如果咱们的 C++ 项目需要调用 C 库函数（比如 `stdio.h`、`string.h` 里的函数），就必须用指针传递。比如调用 C 的 `strcpy` 函数：

```
#include <cstring>  // C库字符串函数
```

### 3.3、指针传递的适用场景

**场景 1：处理可选参数（允许传递 nullptr）**  

如果函数的某个参数是 “可选的”（比如 “可选的输出参数”），用指针传递最合适 —— 不需要该参数时，传递 `nullptr` 即可。例如：

```
#include <iostream>
```

**场景 2：修改一级指针的指向（必须用二级指针）**  

比如动态内存分配、链表节点插入 / 删除（修改头指针指向）、树的节点操作等场景，只能用二级指针或 “指针的引用”（`T*&`）。

**场景 3：传递数组或动态内存（堆上的对象）**  

数组传递只能用指针（配合长度参数）；堆上的对象（`new` 出来的）也必须用指针访问，传递时自然是指针传递。

**场景 4：兼容 C 语言代码或旧版 C++ 代码**  

如果项目需要和 C 代码交互，或者维护旧的 C++ 代码（未使用引用特性），必须用指针传递。

  

# ****P****art 4**** 三种传递机制对比****

咱们从 “底层实现”“性能”“安全性”“实战场景” 等 7 个维度，做一个全方位对比，帮大家快速决策：

|   |   |   |   |
|---|---|---|---|
|对比维度|值传递（Pass-by-Value）|引用传递（Pass-by-Reference）|指针传递（Pass-by-Pointer）|
|传递的内容|实参的完整数据副本|实参的别名（绑定地址）|实参指针的副本（存地址）|
|底层内存开销|高（拷贝全部数据，大对象明显）|低（仅绑定地址，无数据拷贝）|低（仅拷贝地址，4/8 字节）|
|能否修改原始数据|不能（仅改副本）|能（const 可禁止）|能（const 可禁止，需解引用）|
|空值支持|不涉及（传递的是数据）|不支持（不能绑定 nullptr）|支持（可传 nullptr，需检查）|
|生命周期依赖|无（形参独立）|有（引用 ≤ 实参生命周期）|有（指针指向的内存需有效）|
|语法复杂度|简单（直接传值）|简单（&声明，直接访问）|较复杂（*解引用、&取地址）|
|拷贝构造调用|会（对象传递时）|不会（无数据拷贝）|不会（无数据拷贝）|
|核心适用场景|小数据、只读操作、安全优先|大数据、需修改、非空参数|可选参数、二级指针、兼容 C 代码|
|常见错误|传递大对象导致性能差|绑定临时变量（非 const）、悬空引用|空指针未检查、野指针、内存泄漏|

  

# ****P****art 5**** 实战决策流程****

### 5.1、三步搞定参数传递选择（不用再纠结）

咱们在实际开发中，不用死记硬背，按这个流程选就行：

**1）、第一步：判断是否需要修改原始数据？**

- 不允许为空（参数必须有效）→ 用**引用传递**（安全，无空指针风险）；
    
- 允许为空（可选参数）→ 用**指针传递**（需加 nullptr 检查）。
    
- 数据小（基本类型、小型结构体）→ 用**值传递**（安全简单）；
    
- 数据大（大型对象、容器）→ 用 **const 引用**（高效，禁止修改）；
    
- 不需要修改 → 看数据大小：
    
- 需要修改 → 看是否允许参数为空：
    

**2）、第二步：判断是否需要兼容 C 代码？**

- 是 → 必须用**指针传递**；
    
- 否 → 优先用引用（语法简洁，安全性高）。
    

**3）、第三步：判断是否需要修改指针指向？**

- 是（如动态内存分配、修改链表头指针）→ 用**二级指针**或**指针的引用**；
    
- 否 → 按第一步、第二步选择。
    

### 5.2、最容易踩的 5 个坑（避坑指南）

**坑 1：用值传递传递大型对象**  

比如传递 `vector<int> bigVec(1000000, 1)`，值传递会拷贝 100 万个 `int`，直接导致性能崩溃。  
避坑：用 `const vector<int>&`。

**坑 2：引用绑定临时变量（非 const）**  

比如 `void func(string& s) {}`，调用 `func("hello")` 会编译报错。  
避坑：用 `const string&`，允许绑定临时变量。

**坑 3：返回局部变量的引用 / 指针**  

比如前面的 `getLocalRef()` 和 `getLocalPtr()`，返回后变量已销毁，形成悬空引用 / 野指针。  
避坑：返回全局变量、堆上的变量，或直接返回值（小数据）。

**坑 4：指针未检查 nullptr**  

比如 `void func(int* x) { (*x)++; }`，调用 `func(nullptr)` 会崩溃。  
避坑：所有指针使用前，必须加 `if (x != nullptr)` 检查。

**坑 5：动态内存分配后不释放**  

比如 `int* p = new int[5];`，用完后不 `delete[] p`，导致内存泄漏。  
避坑：用智能指针（`unique_ptr`、`shared_ptr`）管理动态内存，或严格遵循 “谁分配谁释放”。

##   

# ****总结****

C++ 的三种参数传递机制，本质是 “**效率**” 和 “**安全性**” 的权衡：

- 值传递是 “安全派”，适合小数据、只读场景，但拷贝开销大；
    
- 引用传递是 “平衡派”，兼顾高效和安全，是大多数 C++ 场景的首选；
    
- 指针传递是 “灵活派”，适合可选参数、兼容 C 代码，但需手动规避空指针 / 野指针风险。
    

咱们在写代码时，不用追求 “某一种方式万能”，而是根据具体场景选择 —— 比如传递 `int` 用值传递，传递 `vector` 用 const 引用，传递可选参数用指针。只有理解每种机制的底层逻辑和适用边界，才能写出高效、安全、易维护的 C++ 代码。