---
title: "地址与指针"
description: "从环境准备到 STL，系统学习 C++ 编程的基础知识。"
---

> Documentation Index
> Fetch the complete documentation index at: https://cppguide.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 地址与指针

![](https://img.dailyup.xyz/originals/8208463149377-cc5339ff-927e-4e72-a30e-c488102bef35.png)

指针是 C++ 中非常重要的一种数据类型，它专门用来存储其它变量的内存地址，通过指针，我们可以直接访问和操作存储在内存中的数据。

在讨论指针前，我们先来看看 C++ 中变量的内存地址这个概念。

## 内存地址

在 C++ 中，所有的变量、数据、甚至代码都是存储在内存中的，在这里我们不讨论具体的内存模型是怎么样的（想要深入了解可以自行查阅资料，我在后面也会给出一些参考资源），**我们现在主要关心的是变量所在的内存地址**。

假设我们有一个变量 `a`，在变量的前面加上取地址符号 `& ` ，`&a` 会得到这个变量所在的内存地址。

```cpp
#include <iostream>
using namespace std;

int main() {
    int a = 1, b = 2, c = 3;
    
    cout << "a的地址:" << &a << endl;
    cout << "b的地址:" << &b << endl;
    cout << "c的地址:" << &c << endl;
    
    return 0;
}
```

运行该程序，输出结果为：

```
a的地址:0x7fff282891bc
b的地址:0x7fff282891c0
c的地址:0x7fff282891c4
```

你运行的结果应该大致看起来和我差不多，但具体的地址肯定不同，这是当程序在创建变量时由系统自行分配的。

另外，你每次运行程序时，都会得到不同的地址，这是因为每次运行程序都会重新创建变量，因此分配的内存地址也应该也是不同的。

这里的地址 `0x7fff282891bc` 看起来有点奇怪，有数字又有字母，实际上，前缀`0x` 代表十六进制，后面的一串数字则是由十六进制表示的实际内存地址。

细心的同学还会发现，这里三个变量的地址刚好差 4，实际上是指 4 个字节（byte），因为 `int` 变量刚好是占 4 个字节。

> 在 C++ 中，符号 `&` 根据上下文的不同有多种含义。
>
> - 作为**取地址运算符**，例如：&a，表示取变量 a 的地址
> - 作为**按位与运算符**，例如： a & b，表示对变量 a 和 b 进行与运算
> - 用于**声明引用的分隔符**，例如：int& ref，表示声明了一个引用，在函数参数中用得较多

## 数组与地址

数组与指针是两种完全不同的对象，但又有着密不可分的关系，接下来我们来看看它们之间有哪些共同和不同的地方。

一个数组的名字，实际上就是一个指针，**该指针指向该数组存放的起始地址**，也就是数组第一个元素所在的地址，这一点使得数组与指针之间产生了密切的联系。

例如，有如下代码：

```cpp
int q[5] = {1, 2, 3, 4, 5};
cout << q << endl; 
cout << &q[0] << endl;
```

`q` 与 `&q[0]` 的输出结果是完全相同，因为他们代表的是同一个元素的地址。


【TODO】

- 地址的连续性
- 二维数组越界访问如何处理


## 指针变量

在 C++ 中，**指针是一种特殊的变量，它是专门用来存储变量的内存地址的**。直接看例子容易明白。

```cpp
#include <iostream>
using namespace std;

int main() {
  	int a = 5;
	
	// 定义指针
    int* pointer_a;

	// 为指针赋值
    pointer_a = &a;
    
    cout << "a: " << a << endl;
    cout << "&a: " << &a << endl;
    cout << "pointer_a: " << pointer_a << endl;
    
    return 0;
}
```

这里定义了一个指针变量 `pointer_a`，该变量存储了变量 `a` 的地址。程序的运行结果如下：

```
a: 5
&a: 0x7ffc0e3f045c
pointer_a: 0x7ffc0e3f045c
```

可以看到，这里指针 `pointer_a` 的值与变量 `a` 的地址是完全一致的。

在定义指针变量时，除了正常的变量名称，需要在前面加上 `*` 表明这是一个指针变量，`*` 的位置可以加在变量名前，也可以紧跟在变量类型后面。

```cpp
int *pointer_a; // 位置1: 加在变量名前
int* pointer_a; // 位置2: 紧跟在变量类型后
```

推荐使用位置2，紧跟在变量类型后，这样就不会误解，以为 `*pointer_a` 这个整体是变量名，指针变量名是不包含 `*` 的。

实际上，`point_a` 和 `*pointer_a` 代表的完全不同的意思，在指针变量名前加星号，如 `*pointer_a`，是直接**获取该指针中存储的地址所指向的值**。

```cpp
// 通过指针访问变量
cout << "pointer_a 指向的值: " << *pointer_a << endl;
```

下面的示意图显示了普通变量 `a` ，a 的地址 `&a`，以及指针变量 `pointer_a` 这几者之间的关系。

![](https://img.dailyup.xyz/optimized/8208463148715-f9da4d04-e782-443f-a1c6-cbde278af507.png)

**更改指针变量所指向的值**

既然指针变量存储的是变量的地址，我们就可以通过这个地址去修改变量的值。

下面是一个简单的例子：

```cpp
#include <iostream>
using namespace std;

int main() {
  	int a = 5;
    int* pointer_a;

    pointer_a = &a;
    
  	// 更改指针变量所指向的值
    *pointer_a = 8;
    
    cout << "a: " << a << endl;
    cout << "&a: " << &a << endl;
    cout << "pointer_a: " << pointer_a << endl;
    cout << "pointer_a 指向的值: " << *pointer_a << endl;
    
    return 0;
}
```

这里我们添加了一行，通过指针去修改它所指向的值，可以发现，原始的变量 `a` 的值也被修改了。

> 需要注意的是，指针变量也有大小，它是指针本身在内存中占用的空间，而不是它所指向的数据的大小，别弄混淆了。**指针的大小是由操作系统和编译器决定的**，通常在 32 位系统上是 4 个字节，在 64 位系统上是 8 字节。

## 指针与函数

在前面讲解函数的章节中，我们已经看到函数的参数传递分为值传递和引用传递两种方式。

引用传递实际上就是将实参变量的地址传递给函数，当时讲到一个交换两个变量的函数 `swap`，大家还记得吗？

```cpp
void swap(int &a, int &b) {
  int temp = a;
  a = b;
  b = temp;
}
swap(a, b); // 调用函数
```

在上面的代码中，我们通过引用传递实现了两个变量的交换。

既然指针又是存储内存地址的变量，我们也可以通过传递指针变量的方式来实现同样的功能。

```cpp
void swap(int* a, int* b) {
  int t = *a;
  *a = *b;
  *b = t;
}
swap(&a, &b); // 调用函数
```

两者实现的效果一样，不同的是函数参数的定义和调用函数的方式有所区别，通过这两种不同的写法，可以更好地帮助理解指针与内存地址之间的关系。

## 指针的运算

如果指针 $p$ 指向数组元素 $e$，那么

- 表达式 $p + i$ 为指向元素 $e$ 向后 $i$ 个位置的元素的指针
- 表达式 $p - i$ 为指向元素 $e$ 向前 $i$ 个位置的元素的指针

例如下面的代码：

```cpp
int q[5] = {1, 2, 3, 4, 5};
int* p = &q[2];
cout << *p << endl; // 输出 3
p = p + 2;
cout << *p << endl; // 输出 5
p = p - 1;
cout << *p << endl; // 输出 4
```

前面有讲到，数组名本身就是数组所在的内存地址，所以我们可以使用 `数组名 + i` 来遍历数组中的元素。如下面的代码所示：

```cpp
int q[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
  cout << *(q + i) << endl; // 利用指针运算来遍历数组元素
}
```

## 动态分配内存

也可以使用指针来动态分配内存，例如：

```cpp
// 动态分配了一个 int 类型的内存空间
int* p = new int; 

// 将值 10 存储到刚刚分配的内存空间中 
*p = 10;
```

这里的 `new` 运算符用于动态分配指定类型的内存空间，将分配的内存空间地址存储在指针变量 `p` 中，然后使用 `*p` 来设定该地址所指向的值。

需要注意的是，在使用完动态分配的内在后，需要使用 `delete` 运算符去释放对应的内存空间。

```cpp
// 释放动态分配的内存空间
delete p;
```

### 动态创建数组对象

上面是单个对象动态创建，同样的方式也可以用于动态创建数组，下面是一个动态创建数组的完整示例：

```cpp
#include <iostream>
using namespace std;

int main() {
  int size;
  cin >> size;
  
  // 动态创建含有 size 个元素的数组对象
  int* p = new int[size];

  // 继续读入元素到数组
  for (int i = 0; i < size; i++) {
    cin >> p[i];
  }
  
  // 测试：遍历输出数组每一个元素
  for (int i = 0; i < size; i++) {
    cout << p[i] << " ";
  }
  
  // 释放动态分配的内存空间
  delete[] p; 
  
  return 0;
}
```

可以发现，使用 `new` 运算符动态创建数组，数组元素的个数可以不用是常量（这里是通过外部读入的），也就是说数组元素的个数是在运行时确定的，相比普通的数组创建来说有更高的灵活度。

另外，销毁数组是用 `delete[]` 运算符，别忘了 `[]` 符号。

## 使用指针的一些常见错误

指针可以直接操作内存地址，这是 C++ 强大的功能之一，但也正是因为它的强大与灵活，一旦没使用好，很容易造成程序崩溃、内存泄漏或逻辑错误等问题。在使用时要特别小心。

这里介绍几种使用指针时的常见错误用法。

### 错误 1

```cpp
// 访问未初始化或无效的指针
int a, *p;
a = 5;
*p = a + 5;
cout << a << endl;
```

修改后：

```cpp
int a, *p;
a = 5;
p = &a;
*p = a + 5;
cout << a << endl;
```

### 错误 2

```cpp
// 同一行声明多个指针
int* p1, p2;
int a = 5;
int b = 3;
p1 = &a;
p2 = &b; // 出错
```

运行代码，会遇到 `int*` 到 `int` 的类型转换错误。

当我们想在一行定义两个同类型的指针，初学者容易这样写，这是一个常见的声明指针的错误。

修改后：

```cpp
int *p1, *p2; // 通过分开定义来修正错误
int a = 5;
int b = 3;
p1 = &a;
p2 = &b; 
```

### 错误 3

```cpp
int a = 5;
int* p = &a;
*p++; // 出错

cout << *p << endl;
```

写这段代码的本意是希望将 p 指针指向的值增加 1，但运行该程序得到的结果可能是一个很大或很小的，错误的整数。

修改后：

```cpp
int a = 5;
int* p = &a;
(*p)++; // 修改的语句

cout << *p << endl;
```

## 小结

C++ 中的指针是一种强大的工具，它们提供了直接访问和操作内存的能力，使用得当，可以极大的简化一些编程任务的执行，但如果未使用好，容易造成一些不易调试的内存访问问题。因引在使用指针时需要谨慎，多积累一些内存管理的技巧，养成良好的编码实践习惯。

Source: https://cppguide.xyz/basic/pointer/index.mdx
