基本概念

指针是存储内存地址的变量,通过 & 获取变量地址,* 解引用指针访问值

int x = 10;
int* ptr = &x; // ptr 存储 x 的地址
cout << *ptr; // 输出 10(解引用)

指针与const

1、const int*:指向常量的指针

含义:指针指向的数据是常量,不能通过指针修改,但指针本身可以指向其他地址。

用途:保护数据不被意外修改(如函数参数传递只读数据)

const int* ptr;  // ptr 可以指向不同的常量,但不能通过 ptr 修改值
int x = 10;
ptr = &x; // 合法
// *ptr = 20; // 错误!不能修改指向的值

2、int* const:常量指针

含义:指针本身是常量,不能改变指向的地址,但可以通过指针修改指向的数据。

用途:确保指针始终指向固定的内存(如硬件寄存器映射)

int x = 10;
int* const ptr = &x; // ptr 必须初始化,且不能再指向其他地址
*ptr = 20; // 合法,可以修改 x 的值
// ptr = &another_x; // 错误!不能修改指针的指向

3、const int* const:指向常量的常量指针

含义:指针本身和它指向的数据都是常量,既不能修改指向,也不能修改数据。

用途:严格保护数据和指针(如全局只读配置)

int x = 10;
const int* const ptr = &x; // ptr 和 *ptr 均不可修改
// *ptr = 20; // 错误!不能修改值
// ptr = &another_x; // 错误!不能修改指向

4、记忆

const 在 * 左侧数据是常量const int*

const 在 * 右侧指针是常量int* const

指针与数组

数组名本质是指向首元素的指针,支持指针算术

int x[3] = {10,11,12};
int *ptr = x; // 不需要使用&符号
cout << *ptr++ << endl; // 输出 10
cout << *ptr << endl; // 输出 11

*ptr++相当于*(ptr++),指针*的优先级很低

但下面这样是不行的,会报错error: lvalue required as increment operand,因为数组名是常量指针

int x[3] = {10,11,12};
cout << *x++ << endl;

野指针/空指针/垂悬指针

野指针

定义:声明指针但未赋值,指向随机内存地址(可能是垃圾值)

场景 1:未初始化的指针

int* p;  // 未初始化,是野指针
*p = 10; // 危险!可能崩溃

场景 2:指针越界

int arr[3] = {1, 2, 3};
int* p = &arr[0];
p += 5; // 越界,变成野指针
cout << *p; // 访问非法内存

场景 3:函数返回局部变量地址

int* foo() {
int x = 10;
return &x; // 返回局部变量地址
} // x 被销毁,返回的指针悬空

int* p = foo(); // p 是野指针
cout << *p; // 可能输出垃圾值或崩溃

空指针

空指针是指不指向任何有效内存地址的指针,在 C++ 中用 nullptr(或旧版 /C++中的NULL0)表示。它的核心作用是明确表示“指针当前无效”

int* ptr = nullptr;  // 明确初始化,避免指向随机地址

悬垂指针

定义:指针指向的内存已被释放,但指针仍保留原地址,访问会导致未定义行为(崩溃或数据错误)

示例1:返回局部变量的指针

int* createDanglingPointer() {
int x = 10; // x 是局部变量(栈内存)
return &x; // 返回 x 的地址
} // 函数结束,x 被销毁,返回的指针悬空!

int main() {
int* ptr = createDanglingPointer();
cout << *ptr; // 危险!访问已释放的内存(可能崩溃或输出垃圾值)
}
Image 1

示例2:delete 后未置空

int* ptr = new int(10);  // 动态分配堆内存
delete ptr; // 释放内存
cout << *ptr; // 危险!ptr 仍是悬垂指针

修复方法:释放后立即置空

delete ptr;
ptr = nullptr; // 明确标记为无效

指针与传参

在 C++ 中,函数参数传递主要有 3 种方式

传值(Pass by Value)

void func(int x) { x = 10; }  // 修改的是副本

特点:拷贝实参的值,函数内修改不影响原始数据

优势:简单安全,避免副作用

缺点:拷贝大对象(如结构体、数组)时性能低

传指针(Pass by Pointer)

void func(int* x) { *x = 10; }  // 通过指针修改原始数据

特点:传递变量的地址,避免拷贝大对象,函数内通过解引用(*x)修改原始数据

优势:1、高效(仅传递地址,4/8 字节);2、显式表达“可能修改数据”的意图

缺点:1、需手动检查空指针(if (x != nullptr));2、可能引发悬垂指针。

void print(int* ptr) {
if (ptr != nullptr) { // 安全检查
cout << *ptr;
} else {
cout << "无数据";
}
}

传引用(Pass by Reference)

void func(int& x) { x = 10; }  // 直接修改原始数据

特点:传递变量的别名,避免拷贝大对象,函数内直接操作原始数据

优势:1、语法简洁(无需 * 和 &);2、无空引用风险(比指针更安全)

缺点:无法表达“可选参数”(引用必须绑定有效对象,即不能传入空指针

void byRef(int& x) {}    // 必须传有效对象
void byPtr(int* x) {} // 可以传 nullptr

int main() {
int a = 10;
byRef(a); // 合法
// byRef(nullptr); // 错误!引用不能为空
byPtr(&a); // 合法
byPtr(nullptr); // 合法:指针可以为空
}

指针与内存

动态内存管理

使用 newdelete 分配/释放内存,避免内存泄漏

int* p = new int(5); // 动态分配
delete p; // 释放内存

内存

在C++中,内存主要分为以下几种类型:

1、栈内存(Stack Memory)

特点:由编译器自动分配和释放内存,分配速度快,大小有限(通常几MB),生命周期与作用域绑定

存储内容:局部变量,函数参数,函数调用信息(返回地址等)

void func() {
int x = 10; // x存储在栈上
} // 函数结束,x自动释放

2、堆内存(Heap Memory)

特点:由程序员手动分配(new/malloc)和释放(delete/free),分配速度较慢,可用空间大(受系统内存限制),生命周期由程序员控制

存储内容:动态分配的对象,大块内存需求

int* p = new int(10);  // 在堆上分配
delete p; // 必须手动释放

3、静态/全局存储区

特点:在程序编译时分配,生命周期贯穿整个程序,分为已初始化区和未初始化区

存储内容:全局变量,静态变量(static),常量

int globalVar;         // 未初始化全局变量(.bss段)
static int sVar = 1; // 已初始化静态变量(.data段)
const int cVar = 100; // 常量(可能存储在.rodata段)
+---------------------+
| 栈(stack) | ← 向下生长
+---------------------+
| ↓ |
| ↑ |
+---------------------+
| 堆(heap) | ← 向上生长
+---------------------+
| 未初始化数据(.bss) |
+---------------------+
| 已初始化数据(.data)|
+---------------------+
| 常量存储区(.rodata)|
+---------------------+
| 代码区(.text) |
+---------------------+
特性 栈内存 堆内存 静态/全局存储区
分配方式 自动 手动(new/malloc) 自动
释放方式 自动 手动(delete/free) 程序结束时
大小限制 较小(几MB) 很大(取决于系统内存) 较大
访问速度 较慢 中等
生命周期 作用域内 直到显式释放 整个程序运行期间

malloc/free

更准确来说,malloc/free所在的区域是自由存储区,它和new/delete是分开的

malloc/free属于 C 标准库,而new/delete是 C++ 的运算符

特性 malloc/free (C) new/delete (C++)
语言 C/C++ 通用 仅 C++
初始化 不初始化内存 调用构造函数(初始化对象)
类型安全 需手动类型转换(void* 自动类型推导
失败行为 返回 NULL 抛出 std::bad_alloc 异常
内存大小 需手动计算字节数 自动计算类型大小
适用场景 原始内存操作、C 兼容 面向对象编程

内存泄露

内存泄漏是指程序在 动态分配内存(如 new/malloc)后,未正确释放(delete/free),导致这部分内存无法被系统回收,最终可能耗尽可用内存,引发程序崩溃或性能下降。

内存泄漏的危害:

  • 短期影响:程序占用内存逐渐增加,运行变慢
  • 长期影响:系统内存耗尽,程序崩溃(尤其在长期运行的服务中)

多级指针与指针数组

二级指针(int**)是指向指针的指针,常用于动态二维数组或修改指针本身。

int x = 10;
int* p = &x; // 一级指针
int** pp = &p; // 二级指针,存储 p 的地址

cout << **pp; // 输出 10(通过二级指针访问 x)
**pp = 20; // 修改 x 的值

动态分配二维数组:

int** matrix = new int*[3];  // 分配行指针
for (int i = 0; i < 3; i++) {
matrix[i] = new int[4]; // 分配每行的列
}

如何使用这个二维数组:

// 赋值
matrix[0][0] = 1; // 第0行第0列
matrix[1][2] = 5; // 第1行第2列

// 访问
cout << matrix[1][2]; // 输出5

为什么不直接声明表格,而用指针来弄?

特性 直接声明 int matrix[3][4] 动态分配 int** matrix
内存位置 栈内存 堆内存
大小确定时间 编译时 运行时
是否支持动态大小
内存管理 自动(编译器) 手动(需 delete
访问速度 更快(连续内存) 稍慢(可能非连续)
适用场景 固定大小的小型数组 动态大小的中大型数组

函数指针与回调

函数指针

函数指针是指向函数的指针变量,允许通过指针调用函数,常用于实现动态行为(如策略模式、回调机制)。

基本语法:返回类型 (*指针名)(参数列表) = 函数名;

#include <iostream>

// 普通函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
// 定义函数指针并绑定到 add
int (*funcPtr)(int, int) = add;

// 通过指针调用函数
std::cout << funcPtr(3, 5); // 输出 8

// 切换指向 subtract
funcPtr = subtract;
std::cout << funcPtr(8, 3); // 输出 5

return 0;
}

回调(Callback)

回调是一种编程模式,通过将函数指针(或函数对象)传递给其他函数,允许在特定事件发生时被调用。常见于事件驱动、异步编程。

回调的实现方式

(1)函数指针回调

  1. processData 函数处理数据
  2. 处理完成后,通过回调函数 onComplete 通知调用方结果
// 回调函数类型
typedef void (*Callback)(int); // 定义一种函数指针类型,名叫 Callback,参数为int、返回void

// 接受回调的函数
void processData(int value, Callback callback) {
std::cout << "处理数据: " << value << "\n";
callback(value * 2); // 触发回调
}

// 实际回调函数
void onComplete(int result) {
std::cout << "回调结果: " << result << "\n";
}

int main() {
processData(10, onComplete); // 传递回调函数
return 0;
}

processData(int value, Callback callback)相当于我是传入了一个函数(具体是什么我不知道,但是方便修改),然后我定义了一个Callback类型来限制我这个传入的函数的样式

Callback相当于一个函数模板,明确约定所有符合这个格式的函数都能传给我,同时限制传入函数的形状

回调vs直接调用:

场景 直接函数调用 回调模式
调用关系 A → B(A 直接调用 B) A ⇄ B(A 提供接口,B 决定具体实现)
灵活性 固定逻辑,难以扩展 可动态替换行为
适用场景 流程固定的简单逻辑 需要灵活扩展的框架设计

(2)Lambda 表达式回调(C++11+)

#include <functional>

void asyncTask(std::function<void(int)> callback) {
std::cout << "异步任务开始...\n";
callback(42); // 模拟异步完成
}

int main() {
asyncTask([](int result) {
std::cout << "收到回调结果: " << result << "\n";
});
return 0;
}

std::function<void(int)>:这是一个通用函数包装器,可以装任何能调用的东西(普通函数、Lambda、类成员函数等)。这里表示:回调函数必须接受一个int参数,且不返回值(void)。

callback(42):任务”完成”后,调用回调函数并传回结果42

[](int x) { std::cout << x; }:[]:捕获列表(这里为空),(int x):参数列表,{ … }:函数体

(3)类成员函数回调

class Processor {
public:
void handleResult(int value) {
std::cout << "处理结果: " << value << "\n";
}
};

template<typename T>
void runTask(T& obj, void (T::*method)(int)) {
(obj.*method)(100); // 调用成员函数
}

int main() {
Processor p;
runTask(p, &Processor::handleResult); // 传递成员函数
return 0;
}

模板部分template<typename T>:表示这个函数可以接受任意类型T,具体的T类型是由传入的obj参数自动推导出来的

当调用 runTask(p, &Processor::handleResult) 时:

  • 编译器看到第一个参数 p 的类型是 Processor& → 推导出 T = Processor
  • 第二个参数 &Processor::handleResult 的类型是 void (Processor::*)(int),与 T = Processor 一致

&Processor::handleResult 中的 :: 是 作用域解析运算符,用于表示 handleResultProcessor 类的成员

void (T::*method)(int)传入&Processor::handleResult之后,*method指向handleResult

现代C++的简化写法

#include <functional>

template<typename T>
void runTask(T& obj, std::function<void(int)> method) {
method(100); // 直接调用函数对象
}

int main() {
Processor p;

// 用 Lambda 捕获对象并调用成员函数
runTask(p, [&p](int x) { p.handleResult(x); });
}

智能指针(C++11+)

智能指针是 C++11 引入的 自动化内存管理工具,用于替代裸指针(int* 等),通过 RAII(资源获取即初始化)机制 自动释放堆内存,从根本上解决内存泄漏和悬垂指针问题。

核心智能指针类型

类型 头文件 所有权语义 特点
std::unique_ptr <memory> 独占所有权(不可复制) 轻量高效,移动语义,适合替代 new/delete
std::shared_ptr <memory> 共享所有权(引用计数) 多个指针可共享同一对象,计数为 0 时自动释放
std::weak_ptr <memory> 弱引用(不增加引用计数) 解决 shared_ptr 循环引用问题,需通过 lock() 获取临时 shared_ptr

std::unique_ptr(独占所有权)

std::unique_ptr 是 C++11 引入的智能指针,核心特性是独占所指向对象的所有权,即同一时间只有一个 unique_ptr 可以拥有该对象。这种设计既保证了内存安全,又几乎无性能开销(与裸指针相当)

独占所有权的含义

  • 唯一性:一个对象只能被一个 unique_ptr 拥有,无法被复制。
  • 移动语义:所有权可以通过 std::move 转移给其他 unique_ptr。
  • 自动释放:当 unique_ptr 离开作用域时,自动销毁并释放其管理的对象。

(1)基本用法

#include <memory>

// 创建 unique_ptr(推荐 make_unique,C++14 起支持)
auto ptr = std::make_unique<int>(10); // 分配一个 int,初始值为 10

// 访问对象
std::cout << *ptr; // 输出 10

// 自动释放:ptr 离开作用域时,内存自动销毁

(2)所有权转移

auto ptr1 = std::make_unique<std::string>("Hello");
auto ptr2 = std::move(ptr1); // 所有权转移给 ptr2

// 此时 ptr1 为 nullptr
if (!ptr1) {
std::cout << "ptr1 已失去所有权";
}

std::cout << *ptr2; // 输出 "Hello"

(3)自定义删除器

std::unique_ptr 的自定义删除器(Custom Deleter)用于指定如何释放资源,而不仅仅是默认的 delete 操作。它允许你管理非内存资源(如文件、锁、网络连接等),或者实现特殊的内存释放逻辑(如内存池)。

// 自定义删除器(例如释放文件句柄)
auto FileDeleter = [](FILE* file) {
std::cout << "关闭文件";
if (file) fclose(file);
};

// 管理文件资源
std::unique_ptr<FILE, decltype(FileDeleter)> filePtr(fopen("data.txt", "r"), FileDeleter);

默认情况下,unique_ptr用delete释放内存,但以下场景需要自定义行为:

  • 管理非内存资源(如文件句柄、数据库连接)。
  • 特殊的内存释放需求(如内存池、调试日志)。
  • 兼容 C 接口(如 malloc/free)。

自定义删除器让 unique_ptr 不仅限于管理内存,还能安全管理任何需要释放的资源,让 unique_ptr 从内存管理工具升级为通用资源管理工具