CPP 指针

PointY Lv2

基础

指针

指针本质就是一个变量,只不过它存的不是普通的数值,而是另一个变量的内存地址

1
2
int a = 42;
int* p = &a; // p 存的是 a 的地址
  • &a — 取地址,拿到 a 在内存中的位置
  • *p — 解引用,通过地址找到那个位置上存的值
1
2
3
4
#include <iostream>

std::cout << p; // 输出地址,类似 0x2000
std::cout << *p; // 输出 42

修改 *p 就是在修改 a 本身:

1
2
3
4
#include <iostream>

*p = 100;
std::cout << a; // 100

声明时注意初始化,未初始化的指针指向随机地址,解引用就是未定义行为

1
2
int* bad;       // 危险:指向未知地址
int* safe = nullptr; // 安全:明确表示"不指向任何东西"

内存

  • 内存可以看作一大片连续编号的字节,编号就是地址
  • 每个变量都在内存里占一小段位置,&a 拿到的是这段位置的起始地址
  • 占多大由类型决定:int 通常 4 字节、double 通常 8 字节。可以用 sizeof 查:
1
2
3
4
#include <iostream>

std::cout << sizeof(int); // 通常 4
std::cout << sizeof(double); // 通常 8

程序运行时的内存大致分几个区域,指针可以指向其中任意一块:

  • :局部变量,离开作用域自动销毁
  • new 分配出来的,需要手动 delete
  • 静态区:全局变量、static 变量,程序运行期间一直存在
  • 代码区:程序指令本身

堆 vs 栈

判断一个变量(普通数组、指针数组都一样)在堆还是栈,唯一的依据是它是怎么创建的,而不是它指向谁:

  • 局部变量、局部数组 → ,离开作用域自动销毁
  • new / new[] 出来的 → ,必须手动 delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void demo() {
int a[3] = {1, 2, 3}; // 普通局部数组 → 栈

int x = 1, y = 2, z = 3;
int* ptrs[3] = {&x, &y, &z}; // 指针数组:数组本身在栈,指向的 x/y/z 也在栈

int* heap_ptrs[3];
for (int i = 0; i < 3; i++)
heap_ptrs[i] = new int(i); // 数组本身仍在栈,但每个元素指向的内容在堆
// 用完要逐个 delete heap_ptrs[i],数组本身不用管

int* p = new int[3]; // p 这个指针在栈,但它指向的数组在堆
delete[] p;
}

关键:指针自己放在哪,和被它指向的对象在哪,是两回事

写法变量/数组本身它(们)指向的内容
int a[3];—(元素是 int 值,直接存在数组里)
int* ptrs[3];看每个指针怎么初始化,可能在栈也可能在堆
int* p = new int[3];指针在栈数组在堆

另外:栈空间有限(通常几 MB),大数组别直接放栈上,容易栈溢出;堆空间大得多,大块内存要用 new 去堆上分配。


指针与数组

用指针遍历数组

数组名本身就可以当作指向首元素的指针

1
2
3
4
5
6
7
8
#include <iostream>

int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // 等价于 int* p = &arr[0];

for (int i = 0; i < 5; i++) {
std::cout << *(p + i) << " "; // 10 20 30 40 50
}

注意p + i 不是地址加 i 个字节,而是加 i 个元素的大小。如果 int 是 4 字节,p + 1 实际上地址偏移了 4 字节。

指针算术

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int arr[] = {10, 20, 30, 40, 50};
int* p = arr;

p++; // 现在指向 arr[1]
p += 2; // 现在指向 arr[3]

int* q = &arr[4];
std::cout << q - p; // 1,两个指针之间隔了 1 个元素

常见写法——用指针做循环:

1
2
3
4
5
6
#include <iostream>

int arr[] = {1, 2, 3, 4, 5};
for (int* p = arr; p < arr + 5; p++) {
std::cout << *p << " ";
}

这和标准库迭代器的设计思路一致:[begin, end) 左闭右开区间。

函数传参:指针 vs 引用

用指针修改外部变量

C 语言时代只能用指针实现”在函数里改外面的变量”:

1
2
3
4
5
6
7
8
void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}

int x = 1, y = 2;
swap(&x, &y); // 调用时要手动取地址

C++ 有了引用,同样的功能写起来更干净:

1
2
3
4
5
6
7
8
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}

int x = 1, y = 2;
swap(x, y); // 调用时不需要 &

选择建议:

  • 能用引用就用引用,语法更清晰
  • 需要表达”可能为空”时用指针(引用必须绑定到有效对象)
  • 和 C 接口交互时用指针

传数组给函数

数组传参时会退化为指针,必须额外传长度:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

void printArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
}

int data[] = {1, 2, 3, 4, 5};
printArray(data, 5);

现代 C++ 更推荐用 std::span(C++20)或传 std::vector&,避免指针加长度的手动管理。

std::span——数组和 vector 都能直接传,长度由 span 自己维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <span>
#include <vector>

void printArray(std::span<const int> s) {
for (int x : s) {
std::cout << x << " ";
}
}

int data[] = {1, 2, 3, 4, 5};
printArray(data); // 数组自动推导长度

std::vector<int> v{1, 2, 3};
printArray(v); // vector 也能直接传

const std::vector<int>&——长度由 vector 自己维护:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <vector>

void printArray(const std::vector<int>& v) {
for (int x : v) {
std::cout << x << " ";
}
}

std::vector<int> data{1, 2, 3, 4, 5};
printArray(data); // 不需要单独传 size

如果拿到的本来就是 C 数组(例如和 C 库交互),std::span 是更合适的桥接方式。

动态内存:new / delete

分配与释放

栈上的变量出了作用域自动销毁,但有时候需要对象活得更久,或者大小在运行时才知道:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

// 分配单个对象
int* p = new int(42);
std::cout << *p; // 42
delete p; // 用完必须释放

// 分配数组
int* arr = new int[100];
arr[0] = 1;
delete[] arr; // 数组用 delete[],不是 delete

规则:new 对应 deletenew[] 对应 delete[],混用是未定义行为。

内存泄漏的典型场景

1
2
3
4
void leak() {
int* p = new int(42);
// 忘了 delete,函数返回后 p 没了,但内存没释放
}
1
2
3
4
5
6
7
void earlyReturn() {
int* p = new int[100];
if (someCondition) {
return; // 提前返回,delete[] 被跳过
}
delete[] p;
}
1
2
3
4
5
void exceptionLeak() {
int* p = new int[100];
riskyFunction(); // 如果抛异常,下面的 delete[] 执行不到
delete[] p;
}

这些场景就是智能指针要解决的问题。

智能指针

智能指针是对裸指针的封装,利用 RAII(资源获取即初始化)机制在离开作用域时自动释放内存。需要 #include <memory>

unique_ptr

独占所有权,同一时刻只有一个 unique_ptr 指向对象:

1
2
3
4
5
6
#include <iostream>
#include <memory>

auto p = std::make_unique<int>(42);
std::cout << *p; // 42
// 离开作用域自动释放,不需要 delete

不能复制,但可以移动:

1
2
3
4
5
6
#include <memory>
#include <utility>

auto p1 = std::make_unique<int>(42);
// auto p2 = p1; // 编译错误:不能复制
auto p2 = std::move(p1); // OK:p1 变为 nullptr,所有权转给 p2

shared_ptr

多个指针共享同一个对象,内部维护引用计数,最后一个 shared_ptr 销毁时释放内存:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <memory>

auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 引用计数变为 2

std::cout << p1.use_count(); // 2
p1.reset(); // p1 放弃所有权,计数变为 1
// p2 销毁时计数归零,内存释放

适用场景:对象的生命周期不确定由谁管理,比如缓存、观察者模式中的共享资源。

代价是引用计数带来的额外开销,别无脑用。

weak_ptr

weak_ptrshared_ptr 的搭档,不增加引用计数,只是”观察”一个 shared_ptr 管理的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <memory>

auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不影响引用计数

std::cout << sp.use_count(); // 仍是 1

// 用之前要先 lock(),拿到一个临时的 shared_ptr
if (auto p = wp.lock()) {
std::cout << *p; // 42
} else {
// 对象已经被释放了
}

主要用途是打破 shared_ptr 的循环引用。两个对象互相持有对方的 shared_ptr 时,引用计数永远不为 0,谁都不会被释放,造成泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <memory>

struct Node {
std::shared_ptr<Node> next;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 互相引用 → 引用计数永远 >= 1,内存泄漏

// 把其中一个改成 weak_ptr 即可打破循环
struct Node2 {
std::weak_ptr<Node2> next; // 关键:成员用 weak_ptr,不增加引用计数
};

auto a2 = std::make_shared<Node2>();
auto b2 = std::make_shared<Node2>();
a2->next = b2; // 赋值到 weak_ptr,不会增加计数
b2->next = a2; // 同样不增加计数 → 作用域结束 a2、b2 正常释放

判断标准:只是”借用/观察”对象而不想延长它的生命,就用 weak_ptr

现代 C++ 标准库的核心就是 unique_ptr / shared_ptr / weak_ptr 三个(auto_ptr 已被移除),日常开发记住它们的定位即可:独占、共享、观察。

常见问题

野指针 / 悬垂指针

1
2
3
4
5
6
7
8
9
10
#include <iostream>

// 悬垂指针:指向已释放的内存
int* p = new int(42);
delete p;
std::cout << *p; // 未定义行为!内存已归还,可能读到垃圾值或崩溃

// 防御:释放后置空
delete p;
p = nullptr;
1
2
3
4
5
// 函数返回局部变量的指针
int* bad() {
int x = 42;
return &x; // x 在函数返回后已销毁,调用者拿到的是悬垂指针
}

double free

1
2
3
4
int* p = new int(42);
int* q = p; // 两个指针指向同一块内存
delete p;
delete q; // 崩溃:同一块内存被释放两次

解决方案:用 unique_ptr 明确所有权,从根本上避免”谁该 delete”的混乱。

  • 标题: CPP 指针
  • 作者: PointY
  • 创建于 : 2026-08-14 22:28:00
  • 更新于 : 2026-08-14 22:24:58
  • 链接: https://siyuhong.github.io/2026/08/14/cpp-pointer/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论