指针
需要预先了解:
1. &取地址符
& 是取地址符,用于获取变量的内存地址。大多数的数据类型往往占用不止一个字节的空间, & 取到的就是其中第一个字节的地址。
#include <iostream>
int main()
{
int a = 10; // 创建一个 int 类型的变量 a
std::cout << "a 的地址: " << &a << std::endl;
std::cout << "地址的大小: " << sizeof(&a) << " 个字节" << std::endl;
return 0;
}
编译并运行代码后,看到这样的输出:
a 的地址: 0x378cfffe2c
地址的大小: 8 个字节
1.1 地址大小与 CPU 架构的关联
- 对于x86_32架构的CPU,其地址总线为32位,对应地址的大小是32个二进制位,也就是4个字节。
- 对于x86_64架构的CPU,其地址总线是64位,对应地址的大小是64个二进制位,也就是8个字节。
这里的输出的地址只有 10 个十六进制位,但是地址的大小是 8 个字节,也就是 8 * 8 = 64 个二进制位。说明地址是一个 64 位的值,但是前 24 位都是 0,被省略掉,所以我们只看到了后面 40 个二进制位,也就是 10 个十六进制位。
如果想看完整的输出,可以调用 <cstdio> 的 printf 函数,使用 %p 格式化输出地址:
#include <cstdio>
int main()
{
int a = 10; // 创建一个 int 类型的变量 a
printf("a 的地址:%p\n", &a); // 输出 a 的地址
printf("地址的大小:%lu\n", sizeof(&a));
return 0;
}
编译并运行后,得到如下输出:
a 的地址:000000697edffd7c
地址的大小:8
2. 指针
指针是一种变量,它存储的值是另一个变量的内存地址。
声明指针时,语法为类型* 指针名;,例如:
int* int_ptr; // 声明一个指向 int 类型的指针
double* double_ptr; // 声明一个指向 double 类型的指针
char* char_ptr; // 声明一个指向 char 类型的指针
可以通过**取地址符&来获取变量的地址,通过解引用操作符***来访问指针所指向的变量:
#include <iostream>
int main()
{
int a = 5;
int* ptr = &a; // ptr 是指向 a 的指针
std::cout << "a 的值: " << a << std::endl;
std::cout << "ptr 的值: " << ptr << std::endl;
std::cout << "ptr 指向的值: " << *ptr << std::endl;
return 0;
}
2.1 指针的注意事项
1. 声明指针时的 * 号只作用于它右侧最近的一个标识符,标记这个变量是一个指针变量。因此当同时声明多个变量的时候,需要给每个变量前都加一个星号:
#include <iostream>
int main()
{
int a = 5, b = 6;
int *pa = &a, *pb = &b; // 正确写法
// int *pa = &a, pb = &b; // 错误写法,编译器会报错
// 这里的 pb 是一个 int 类型的变量,而不是一个指针
return 0;
}
2. 无论什么变量类型,它的指针的大小都是固定的,与地址的大小一样。也就是 32 位系统下是 4 个字节,64 位系统下是 8 个字节。
我们对 int 类型变量 a 取地址,&a 的类型就是 int*,它的大小是 8 个字节;对 char 类型变量 b 取地址,&b 的类型就是 char*,它的大小也是 8 个字节。虽然指针的类型不同,但是它们的大小都是一样的。
不同类型的指针大小一致,内容都是对应的变量之间的地址,我们可以按照以下方式来进行类型转换,例如将 int* 转换为 char*,我们就可以按照 char 的方式来访问 int 类型变量的每一个字节:
#include <iostream>
int main()
{
std::cout << "int 类型变量的大小: " << sizeof(int) << " 字节" << std::endl;
std::cout << "char 类型变量的大小: " << sizeof(char) << " 字节" << std::endl;
int a = 0x41414141; // 0x41 是 ASCII 码中 'A' 的十六进制表示
char* pchar = (char*)&a; // 将 int* 转换为 char*
std::cout << "pchar 指向的值: " << *pchar << std::endl;
return 0;
}
编译运行后,得到以下输出:
int 类型变量的大小: 4 字节
char 类型变量的大小: 1 字节
pchar 指向的值: A
2.2 指针的运算
指针可以进行加法和减法运算,指针的加法和减法会根据指针所指向的数据类型来进行计算。
对于某种类型 Type 的指针 Type* ptr,当我们进行 ptr + k 的操作时,指针会向前移动 k 个 Type 类型的元素,也就是移动 k * sizeof(Type) 个字节。
#include <iostream>
int main()
{
std::cout << "int 类型变量的大小: " << sizeof(int) << " 字节" << std::endl;
std::cout << "char 类型变量的大小: " << sizeof(char) << " 字节" << std::endl;
int a = 0x41424344; // 0x41 是 'A',0x42 是 'B',0x43 是 'C',0x44 是 'D'
int* p_int = &a; // 定义一个 int* 指针,指向 a
char* p_char = (char*)&a; // 定义一个 char* 指针,指向 a
// 输出指针的值
std::cout << "p_int 和 p_char 的值" << p_int << " " << p_char << std::endl;
// 输出指针加 1 后的值
std::cout << "加1后:" << p_int + 1 << " " << p_char + 1 << std::endl;
// 输出指针加 2 后的值
std::cout << "加2后:" << p_int + 2 << " " << p_char + 2 << std::endl;
// 输出指针减 1 后的值
std::cout << "减1后:" << p_int - 1 << " " << p_char - 1 << std::endl;
std::cout << *(p_char + 0) << " " << *(p_char + 1) << " " << *(p_char + 2) << " " << *(p_char + 3) << std::endl;
return 0;
}
2.3 空指针
空指针表示该指针不指向任何有效的内存地址,有NULL和nullptr两种表达方式。但是在很多编译器中,对于NULL的实现只是简单的定义为整数0,而nullptr有自己的类型std::nullptr_t,是更安全、更建议的方式。
int* ptr1 = NULL;
int* ptr2 = nullptr;
3. 二级指针与多级指针
3.1 二级指针
二级指针是指向指针的指针。它的声明语法为 类型** 指针名;,即比一级指针多一个 * 号:
int a = 10;
int* p = &a; // 一级指针:存储变量 a 的地址
int** pp = &p; // 二级指针:存储指针 p 的地址
内存关系图
假设 a 在地址 0x1000,p 在地址 0x2000,pp 在地址 0x3000:

pp存的是p的地址 (0x2000)p存的是a的地址 (0x1000)a存的是实际数据10
通过二级指针访问原始数据
需要 两次解引用 才能拿到最底层的值:
#include <iostream>
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
// 三层值的关系
std::cout << "a 的值: " << a << std::endl; // 10
std::cout << "*p 的值: " << *p << std::endl; // 10
std::cout << "**pp 的值: " << **pp << std::endl; // 10
// 三层地址的关系
std::cout << "&a (a 的地址): " << &a << std::endl; // 0x1000
std::cout << "p 的值 (即 &a): " << p << std::endl; // 0x1000
std::cout << "&p (p 的地址): " << &p << std::endl; // 0x2000
std::cout << "pp 的值 (即 &p): " << pp << std::endl; // 0x2000
// 修改原始数据
**pp = 20;
std::cout << "修改后 a = " << a << std::endl; // 20
return 0;
}
*pp 就是 p(一级指针),**pp 就是 *p 也就是 a。
3.2 多级指针的一般规律
| 指针级别 | 声明语法 | 存储内容的类型 | 解引用到底层数据 |
|---|---|---|---|
| 一级 | T* p; |
T 的地址 |
*p |
| 二级 | T** pp; |
T* 的地址 |
**pp |
| 三级 | T*** ppp; |
T** 的地址 |
***ppp |
| n 级 | T + n 个 * |
T+(n-1)个* 的地址 |
* 重复 n 次 |
4. new 与 delete
4.1 new 运算符
new运算符用于向操作系统申请一块足够容纳目标类型大小的堆内存,并会在刚分配好的内存上执行对应类型的构造函数,完成对象的初始化,可以有以下初始化方式:
- 默认初始化:
new 变量类型;对于内置类型值为随机垃圾值,对于自定义类会调用默认构造函数 - 值初始化:
new 变量类型();内置类型会零初始化(int为 0、指针为nullptr);类调用默认构造 - 直接初始化:
new 变量类型(值);通过小括号传参构造对象。 - 列表初始化:
new 变量类型{值};统一初始化语法,可以防止窄化转换
int *p1 = new int;
std::cout << "未初始化的垃圾值: " << *p1 << std::endl;
int *p2 = new int(42);
std::cout << "初始化的值: " << *p2 << std::endl;
int *p3 = new int();
std::cout << "被零初始化的值:" << *p3 << std::endl;
new []用于一次性分配连续的多个对象(数组),语法为new 类型[元素个数],同样支持多种初始化方式:
int* arr1 = new int[10]; // 其中的元素未初始化,是随机值
int* arr2 = new int[10](); // 其中的元素初始化为0
int* arr3 = new int[10]{1, 2, 3}; // 前三个元素初始化为1, 2, 3,其余元素初始化为0
4.2 delete 运算符
对单个对象进行delete:语法为delete 指针;,会先调用析构函数,再释放内存。
int* p = new int(10);
delete p;
p = nullptr; // 避免悬垂指针
对new[]分配的数组进行delete:会逐个调用元素的析构函数,再释放整块的内存,语法为delete[] 指针;。
int* arr = new int[10];
delete[] arr;


