【C++ Primer】第十二章:动态内存管理

动态内存管理是 C++ 编程中非常重要且容易出错的部分。


1. 动态内存与智能指针

为什么需要动态内存?

  • 程序在运行时才知道需要多少对象
  • 程序需要在多个对象间共享数据
  • 生存期需要跨越函数调用

智能指针类型

C++11 引入了三种智能指针,都在 <memory> 头文件中:

std::shared_ptr<T> - 共享所有权
cpp 复制代码
#include <memory>

std::shared_ptr<std::string> p1 = std::make_shared<std::string>("hello");
std::shared_ptr<std::string> p2 = p1; // 拷贝,引用计数+1

std::cout << p1.use_count(); // 输出引用计数:2

特点

  • 多个 shared_ptr 可以指向同一个对象
  • 使用引用计数管理内存
  • 当最后一个 shared_ptr 被销毁时,对象自动释放
std::unique_ptr<T> - 独占所有权
cpp 复制代码
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // 错误!不能拷贝

std::unique_ptr<int> p3 = std::move(p1); // 可以移动

特点

  • "独占"所指向的对象
  • 不支持普通的拷贝和赋值
  • 支持移动语义
  • 开销小,效率高
std::weak_ptr<T> - 弱引用
cpp 复制代码
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 创建弱引用

if (std::shared_ptr<int> np = wp.lock()) { // 尝试提升为 shared_ptr
    // 使用 np
    std::cout << *np << std::endl;
}

用途

  • 解决 shared_ptr 的循环引用问题
  • 不控制对象生存期
  • 需要调用 lock() 来获取可用的 shared_ptr

2. 直接内存管理(原始指针)

newdelete 操作符

cpp 复制代码
// 动态分配单个对象
int *pi = new int{};          // 值初始化为0
int *pi2 = new int(1024);     // 直接初始化
std::string *ps = new std::string(10, '9');

// 动态分配数组
int *pia = new int[10]();     // 10个值初始化为0的int

// 一定要配对释放!
delete pi;
delete pi2;
delete ps;
delete[] pia;

// 释放后设为nullptr避免悬空指针
pi = nullptr;
pi2 = nullptr;
ps = nullptr;
pia = nullptr;

常见陷阱

cpp 复制代码
// 1. 忘记delete导致内存泄漏
void leak() {
    int* p = new int(42);
    // 忘记 delete p;
}

// 2. 使用已经delete的内存
int* p = new int(42);
delete p;
*p = 10; // 未定义行为!

// 3. 对同一块内存delete两次
delete p;
delete p; // 未定义行为!

3. 智能指针的实现原理

引用计数机制

cpp 复制代码
// shared_ptr 的简化实现概念
template<typename T>
class shared_ptr {
private:
    T* ptr;
    int* count;  // 引用计数
    
public:
    // 构造函数
    shared_ptr(T* p) : ptr(p), count(new int(1)) {}
    
    // 拷贝构造函数
    shared_ptr(const shared_ptr& other) 
        : ptr(other.ptr), count(other.count) {
        ++(*count);
    }
    
    // 析构函数
    ~shared_ptr() {
        if (--(*count) == 0) {
            delete ptr;
            delete count;
        }
    }
};

4. 动态数组

使用 newdelete[]

cpp 复制代码
// 分配动态数组
int* arr = new int[size];

// 初始化动态数组
int* arr2 = new int[size]{1, 2, 3}; // 前三个元素初始化,其余值初始化

// 释放数组
delete[] arr;
delete[] arr2;

使用 std::unique_ptr 管理动态数组

cpp 复制代码
#include <memory>

// unique_ptr 管理数组
std::unique_ptr<int[]> up(new int[10]);

// 可以直接使用下标访问
for (size_t i = 0; i < 10; ++i) {
    up[i] = i;
}

// 自动调用 delete[],不需要手动释放

使用 std::vector(通常更好)

cpp 复制代码
#include <vector>

// 通常比动态数组更好
std::vector<int> vec(10);  // 10个元素,值初始化为0

// 更安全,功能更丰富,自动管理内存

5. allocator

为什么需要 allocator

  • new 将内存分配和对象构造绑定在一起
  • delete 将内存释放和对象析构绑定在一起
  • 有时我们需要分离这两个操作

使用 allocator

cpp 复制代码
#include <memory>

std::allocator<std::string> alloc;

// 分配未构造的内存
auto const p = alloc.allocate(10); // 分配10个string的内存

// 在内存中构造对象
auto q = p;
alloc.construct(q++);           // 构造空string
alloc.construct(q++, 10, 'c');  // 构造 "cccccccccc"
alloc.construct(q++, "hi");     // 构造 "hi"

// 使用对象
std::cout << *p << std::endl; // 输出第一个string

// 析构对象
while (q != p) {
    alloc.destroy(--q);
}

// 释放内存
alloc.deallocate(p, 10);

6. 文本查询程序示例

这一章最后通过一个文本查询程序综合运用了动态内存管理的知识:

程序功能

  • 读取文本文件
  • 允许用户查询单词出现的行
  • 使用 shared_ptr 共享数据

关键设计点

cpp 复制代码
class QueryResult; // 前向声明

class TextQuery {
public:
    using line_no = std::vector<std::string>::size_type;
    TextQuery(std::ifstream&);
    QueryResult query(const std::string&) const;
    
private:
    // 使用 shared_ptr 让多个 QueryResult 共享数据
    std::shared_ptr<std::vector<std::string>> file;
    std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};

7. 重点与难点

必须掌握的概念

  1. RAII(Resource Acquisition Is Initialization):资源获取即初始化
  2. 引用计数的原理和实现
  3. 所有权语义:独占 vs 共享
  4. 移动语义在智能指针中的应用

常见错误

cpp 复制代码
// 错误:混合使用智能指针和原始指针
void process() {
    int* x = new int(10);
    std::shared_ptr<int> sp1(x);
    std::shared_ptr<int> sp2(x); // 错误!两个独立的shared_ptr管理同一内存
    
    // 正确做法:
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);
    std::shared_ptr<int> sp2 = sp1; // 共享所有权
}

最佳实践

  1. 优先使用智能指针而不是原始指针
  2. 优先使用 std::make_shared 而不是直接 new
  3. 使用 std::unique_ptr 默认情况下
  4. 只有需要共享所有权时才使用 std::shared_ptr
  5. 使用 std::weak_ptr 打破循环引用

这一章是 C++ 现代编程风格的基础,掌握好动态内存管理对于写出安全、高效的 C++ 程序至关重要!

相关推荐
_extraordinary_3 小时前
Java Spring日志
java·开发语言·spring
liu****3 小时前
8.list的模拟实现
linux·数据结构·c++·算法·list
保持低旋律节奏4 小时前
C++ stack、queue栈和队列的使用——附加算法题
c++
初圣魔门首席弟子4 小时前
【C++ 学习】单词统计器:从 “代码乱炖” 到 “清晰可品” 的复习笔记
开发语言·c++
lsx2024064 小时前
SQL UPDATE 语句详解
开发语言
十五年专注C++开发4 小时前
CFF Explorer: 一款Windows PE 文件分析的好工具
c++·windows·microsoft
郝学胜-神的一滴4 小时前
计算机图形学中的光照模型:从基础到现代技术
开发语言·c++·程序人生·图形渲染
lly2024065 小时前
PostgreSQL 表达式
开发语言
LXMXHJ5 小时前
php开发
开发语言·php