第 18.9 節

智能指針

0瀏覽次數0訪問次數--跳出率--平均停留

本節解決什麼問題

new/delete 能手動管理動態內存,但它要求你在所有路徑上都記得釋放資源。一旦函數提前 return、拋異常,或者多個對象互相持有指針,就很容易出現內存泄漏、重複釋放、懸空指針。

智能指針把"誰負責釋放對象"寫進類型裏:

智能指針所有權含義典型場景
std::unique_ptr<T>獨佔所有權默認選擇,一個對象只有一個擁有者
std::shared_ptr<T>共享所有權對象確實需要多個擁有者共同延長生命週期
std::weak_ptr<T>弱引用,不擁有觀察 shared_ptr 管理的對象,常用於打破循環引用

智能指針本質上是 RAII:智能指針對象離開作用域時,它的析構函數會自動釋放所管理的對象。

C++ 標準版本

  • std::unique_ptr:C++11
  • std::shared_ptr / std::weak_ptr:C++11
  • std::make_unique:C++14
  • std::make_shared:C++11

需要的頭文件是 <memory>

學習順序

  1. 先看手動 new/delete 在提前返回時的問題。
  2. unique_ptr 解決單一所有權和自動釋放。
  3. std::move 轉移 unique_ptr 所有權。
  4. 只有確實共享所有權時,才用 shared_ptr
  5. weak_ptr 觀察對象並打破循環引用。
  6. 函數參數要表達真實語義:借用、轉移、共享是三件不同的事。

示例代碼

示例 1:舊寫法的問題和 unique_ptr 的 RAII

這個例子故意讓函數提前返回。舊寫法中,delete 沒有執行;unique_ptr 版本中,即使提前返回也會自動析構。

#include <iostream>
#include <memory>
#include <string>

class Resource
{
    std::string name_;

public:
    explicit Resource(const std::string& name) : name_(name)
    {
        std::cout << name_ << " created\n";
    }

    ~Resource()
    {
        std::cout << name_ << " destroyed\n";
    }

    void use() const
    {
        std::cout << name_ << " used\n";
    }
};

bool old_style()
{
    Resource* res = new Resource("old resource");
    res->use();

    std::cout << "old_style: early return\n";
    return false; // res 没有 delete,析构函数不会执行
}

bool modern_style()
{
    auto res = std::make_unique<Resource>("modern resource");
    res->use();

    std::cout << "modern_style: early return\n";
    return false; // res 离开作用域,自动 delete
}

int main()
{
    std::cout << std::boolalpha;

    bool old_ok = old_style();
    std::cout << "old_ok = " << old_ok << "\n";

    std::cout << "---\n";

    bool modern_ok = modern_style();
    std::cout << "modern_ok = " << modern_ok << "\n";

    return 0;
}

運行結果

old resource created
old resource used
old_style: early return
old_ok = false
---
modern resource created
modern resource used
modern_style: early return
modern resource destroyed
modern_ok = false

注意第一段輸出裏沒有 old resource destroyed,這就是手動 new/delete 在複雜路徑下最容易出的錯。

示例 2:unique_ptr 的獨佔所有權和轉移

unique_ptr 不能複製,只能移動。移動之後,原來的指針會變成空指針。

#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Student
{
    std::string name_;

public:
    explicit Student(const std::string& name) : name_(name)
    {
        std::cout << "Student " << name_ << " created\n";
    }

    ~Student()
    {
        std::cout << "Student " << name_ << " destroyed\n";
    }

    const std::string& name() const
    {
        return name_;
    }
};

// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_ownership(std::unique_ptr<Student> student)
{
    std::cout << "take ownership of " << student->name() << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto p = std::make_unique<Student>("Alice");

    auto owner = std::move(p);
    std::cout << "p is null? " << (p == nullptr ? "yes" : "no") << "\n";
    std::cout << "owner has " << owner->name() << "\n";

    take_ownership(std::move(owner));
    std::cout << "owner is null? " << (owner == nullptr ? "yes" : "no") << "\n";

    return 0;
}

運行結果

Student Alice created
p is null? yes
owner has Alice
take ownership of Alice
Student Alice destroyed
owner is null? yes

示例 3:shared_ptr 的共享所有權

shared_ptr 通過引用計數管理對象。最後一個 shared_ptr 銷燬或 reset 後,對象才會釋放。

#include <iostream>
#include <memory>
#include <string>

class File
{
    std::string name_;

public:
    explicit File(const std::string& name) : name_(name)
    {
        std::cout << "File " << name_ << " opened\n";
    }

    ~File()
    {
        std::cout << "File " << name_ << " closed\n";
    }

    void read() const
    {
        std::cout << "read from " << name_ << "\n";
    }
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // 智能指针负责管理对象生命周期,减少手动释放资源的风险。
    auto file1 = std::make_shared<File>("log.txt");
    std::cout << "count after create = " << file1.use_count() << "\n";

    {
        auto file2 = file1;
        std::cout << "count in scope = " << file1.use_count() << "\n";
        file2->read();
    }

    std::cout << "count after scope = " << file1.use_count() << "\n";

    file1.reset();
    std::cout << "file1 is empty? " << (file1 == nullptr ? "yes" : "no") << "\n";

    return 0;
}

運行結果

File log.txt opened
count after create = 1
count in scope = 2
read from log.txt
count after scope = 1
File log.txt closed
file1 is empty? yes

shared_ptr 很方便,但不是"更安全的默認指針"。它有引用計數開銷,也會讓所有權關係變複雜。默認優先考慮 unique_ptr

示例 4:weak_ptr 打破 shared_ptr 循環引用

如果兩個對象互相用 shared_ptr 指向對方,它們的引用計數可能永遠不會歸零。常見做法是:主要方向用 shared_ptr,反向觀察關係用 weak_ptr

#include <iostream>
#include <memory>
#include <string>

class Node
{
public:
    std::string name;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;

    explicit Node(const std::string& n) : name(n)
    {
        std::cout << "Node " << name << " created\n";
    }

    ~Node()
    {
        std::cout << "Node " << name << " destroyed\n";
    }
};

int main()
{
    auto a = std::make_shared<Node>("A");
    auto b = std::make_shared<Node>("B");

    a->next = b; // A 拥有下一个节点
    b->prev = a; // B 只观察前一个节点,不增加 A 的引用计数

    std::cout << "a count = " << a.use_count() << "\n";
    std::cout << "b count = " << b.use_count() << "\n";

    if (auto p = b->prev.lock())
    {
        std::cout << "B prev is " << p->name << "\n";
    }

    return 0;
}

運行結果

Node A created
Node B created
a count = 1
b count = 2
B prev is A
Node A destroyed
Node B destroyed

weak_ptr 不能直接訪問對象,需要先調用 lock()。如果對象還活着,lock() 返回一個臨時 shared_ptr;如果對象已經釋放,返回空指針。

示例 5:函數參數不要濫用 shared_ptr

函數參數應該表達真實語義:

  • 只是臨時使用對象:用 T&const T&
  • 函數可能沒有對象:用 T*const T*
  • 函數要接管對象:用 std::unique_ptr<T>
  • 函數要共同擁有對象:用 std::shared_ptr<T>
#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Robot
{
    std::string name_;

public:
    explicit Robot(std::string name) : name_(std::move(name)) {}

    const std::string& name() const
    {
        return name_;
    }
};

void print_robot(const Robot& robot)
{
    std::cout << "borrow: " << robot.name() << "\n";
}

// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_robot(std::unique_ptr<Robot> robot)
{
    std::cout << "take: " << robot->name() << "\n";
}

void share_robot(std::shared_ptr<Robot> robot)
{
    std::cout << "share count inside = " << robot.use_count() << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto robot = std::make_unique<Robot>("R1");
    print_robot(*robot);
    take_robot(std::move(robot));
    std::cout << "robot is null? " << (robot == nullptr ? "yes" : "no") << "\n";

    auto shared_robot = std::make_shared<Robot>("R2");
    std::cout << "share count before = " << shared_robot.use_count() << "\n";
    share_robot(shared_robot);
    std::cout << "share count after = " << shared_robot.use_count() << "\n";

    return 0;
}

運行結果

borrow: R1
take: R1
robot is null? yes
share count before = 1
share count inside = 2
share count after = 1

關鍵語法解釋

寫法含義注意事項
std::make_unique<T>(...)創建獨佔所有權對象C++14 起可用
std::move(p)unique_ptr 所有權轉出去移動後原指針通常為空
std::make_shared<T>(...)創建共享所有權對象引用計數有開銷
sp.use_count()查看共享引用計數教學調試可用,業務邏輯不要依賴它
std::weak_ptr<T>弱引用,不增加引用計數訪問前必須 lock()
p.reset()釋放當前管理的對象shared_ptr 是減少一次引用計數

常見錯誤

  1. unique_ptr 當普通指針複製。unique_ptr 只能移動,不能複製。
  2. 明明只是借用對象,卻把參數寫成 shared_ptr。這會暗示函數也參與共享所有權。
  3. 用一個 shared_ptr 的原始指針創建另一個 shared_ptr。這樣會產生兩個控制塊,最終可能重複釋放。
  4. 兩個對象互相用 shared_ptr 持有對方,導致循環引用。
  5. 用智能指針管理棧對象。例如把局部變量地址交給 unique_ptr,離開作用域時會錯誤釋放棧內存。

使用建議

  1. 能不用指針就不用指針,普通對象和引用優先。
  2. 需要動態所有權時,優先用 unique_ptr
  3. 只有確實有多個所有者時,才用 shared_ptr
  4. 只觀察 shared_ptr 管理的對象時,用 weak_ptr
  5. 創建智能指針優先使用 make_uniquemake_shared
  6. 函數參數要表達語義:借用、轉移、共享不要混用。

小結

  • 智能指針是 RAII 的典型應用。
  • unique_ptr 表達獨佔所有權,是默認首選。
  • shared_ptr 表達共享所有權,不要因為方便就濫用。
  • weak_ptr 不擁有對象,常用於解決循環引用。
  • 智能指針最重要的不是"自動 delete",而是把所有權關係寫清楚。
音乐页