冰凝瞬间 發表於 2026-1-5 23:58:00

【C++】智能指针

<h2 id="前言">前言</h2>
<p>学习C++智能指针。</p>
<p><strong>指针(Pointer)</strong>就是一个变量,其存储的是<strong>另一个变量的内存地址</strong>,理解指针是掌握 C++ 内存管理、数组、对象以及底层操作的关键。</p>
<h2 id="为什么使用指针">为什么使用指针</h2>
<p><strong>1.动态内存管理</strong>:在运行时根据需要申请内存(使用 <code>new</code> 和 <code>delete</code>)。原生数组(如 <code>int a</code>)的大小在编译时就确定了,存储在栈(Stack)上。但很多时候,你并不知道程序运行过程中需要多少内存。</p>
<ul>
<li><strong>按需分配</strong>:指针允许你在程序运行时使用 <code>new</code> 关键字在堆(Heap)上申请内存。</li>
<li><strong>生命周期控制</strong>:栈上的变量在函数结束时会自动销毁,而指针指向的堆内存可以跨越函数生命周期存在,直到你手动释放它。</li>
</ul>
<p><strong>2.传递大型对象</strong>:通过指针传递参数(或引用)可以避免复制整个对象的开销,提高程序性能。</p>
<p><strong>3.实现复杂数据结构</strong>:如链表(每个节点通过指针指向下一个节点)、二叉树、图(父节点通过指针寻找子节点)等,必须依赖指针。</p>
<p><strong>4.底层操作</strong>:直接访问硬件或操作特定的内存区域。</p>
<h2 id="为什么需使用智能指针">为什么需使用智能指针</h2>
<h3 id="原生指针弊端">原生指针弊端</h3>
<p>原生指针存在的问题:内存泄漏、悬空指针(重复释放)、野指针、所有权不清晰。</p>
<h4 id="内存泄漏">内存泄漏</h4>
<p>用 <code>new</code> 分配了内存但忘记用 <code>delete</code> 释放!</p>
<ul>
<li>
<p><strong>指针重定向</strong></p>
<p>C++</p>
<pre><code class="language-C++">int* p = new int(10); // 申请了内存 A
p = new int(20);      // p 现在指向了内存 B,内存 A 的地址丢失了,再也找不回来。
</code></pre>
</li>
<li>
<p><strong>指针未释放</strong></p>
<pre><code class="language-C++">void func() {
    int* p = new int(10);
    if (true) return; // 为真,函数直接返回,delete 被跳过!
    delete p;
    p = nullptr;
}
</code></pre>
</li>
<li>
<p><strong>异常跳出:</strong> 程序运行中抛出异常,导致执行流直接跳转到 <code>catch</code> 块,没能执行到释放内存的代码。</p>
</li>
<li>
<p>内存泄漏的后果,内存泄漏通常不会立刻导致程序崩溃,它的危害是<strong>渐进式</strong>的:</p>
</li>
</ul>
<ol>
<li><strong>性能下降</strong> 随着可用内存变少,操作系统会频繁进行内存交换(Swap),系统变得越来越卡。</li>
<li><strong>内存不足而崩溃</strong> 当内存被耗尽,新的 <code>new</code> 请求会失败,抛出异常,程序被迫中止。</li>
<li><strong>隐蔽性极强</strong> 在本地测试可能跑得好好的,但在服务器上连续运行几天甚至几周后,程序会无预兆地突然倒下</li>
</ol>
<h4 id="悬空指针重复释放">悬空指针(重复释放)</h4>
<p>指向的内存已经被释放,但指针依然指向那个地址,<strong>重复释放</strong>指的是对同一块动态分配的内存进行<strong>多次释放操作</strong>。</p>
<pre><code class="language-C++">        int* original = new int(100);
    int* alias = original;// 两个指针指向同一内存
   
    delete original;// ✅ 释放内存
    // 此时 original 和 alias 都成为悬空指针
   
    delete alias;   // ❌ 重复释放!同一内存再次释放
</code></pre>
<h4 id="野指针">野指针</h4>
<p>指针未初始化,是指向“不可预知”内存区域的指针。它不是 <code>nullptr</code>,也不是指向有效的内存地址,它的值是<strong>随机的垃圾值</strong>。</p>
<pre><code class="language-c++">    //定义一个局部指针变量却不初始化时,它在栈上的值是上一次程序运行留下的残余数据
    int* p;      // 野指针!没有初始化,指向一个随机地址(如 0xCC123456)
    *p = 100;      // 极其危险!你可能正在修改系统关键数据或其它变量


    //指针跨越了作用域,返回局部变量的地址
    int* getPointer() {
      int x = 10;//x为栈上空间
      return &amp;x; // 错误!x 是局部变量,函数结束就被销毁了
    }

    int* p = getPointer(); // p 变成了野指针(或悬空指针)
</code></pre>
<h4 id="所有权不清晰">所有权不清晰</h4>
<p>当这个指针不再被需要时,究竟该由谁来负责 <code>delete</code> 它</p>
<pre><code class="language-C++">// 谁该负责释放返回的这个指针?
Data* fetchData() {
    return new Data();
}

void process() {
    Data* ptr = fetchData();
    // 如果我忘了写 delete,内存泄漏
    // 如果我 delete 了,但另一个地方也在用它,程序崩溃
}
</code></pre>
<h3 id="智能指针优点">智能指针优点</h3>
<ul>
<li>
<p><strong>自动化的生命周期管理</strong>(核心优点),智能指针遵循 <strong>RAII</strong>(资源获取即初始化)原则。不再需要手动写 <code>delete</code>。当智能指针对象在栈上被销毁(如函数返回、大括号结束、异常抛出)时,它会自动释放所指向的堆内存。</p>
</li>
<li>
<p><strong>防止野指针:</strong> 智能指针强制初始化,不会像原生指针那样默认指向随机地址。</p>
</li>
<li>
<p><strong>防止重复释放(Double Free):</strong> <code>std::unique_ptr</code> 通过禁止拷贝确保只有一个;<code>std::shared_ptr</code> 通过计数确保只在最后一次被使用时才释放。</p>
</li>
<li>
<p><strong>解决悬空指针:</strong> 使用 <code>std::weak_ptr</code> 可以在访问对象前先检查它是否还“活着”,从而避免访问已被释放的内存。</p>
</li>
<li>
<p><strong>所有权</strong><code>std::unique_ptr&lt;T&gt;</code>独占、<code>std::shared_ptr&lt;T&gt;</code>共享、<code>std::weak_ptr&lt;T&gt;</code>观察。</p>
</li>
<li>
<p>智能指针作为栈对象,即便发生异常,C++ 的“栈解旋(Stack Unwinding)”机制也会确保其析构函数被调用,从而安全地回收内存。这是编写健壮工业级代码的基础。</p>
</li>
<li>
<p><strong><code>std::unique_ptr</code></strong>:具有<strong>零开销(Zero-overhead)</strong>。它在内存大小和运行速度上与原生指针完全一致,编译器会将其优化为最高效的机器码。</p>
</li>
<li>
<p><strong><code>std::shared_ptr</code></strong>:虽然有引用计数的原子操作开销,但对于大多数业务逻辑来说,这种开销几乎可以忽略不计。</p>
</li>
</ul>
<h2 id="stdshared_ptr">std::shared_ptr</h2>
<p><code>shared_ptr</code> 实际上包含两个指针:</p>
<ol>
<li>
<p><strong>指向数据的指针</strong>:直接指向你申请的内存对象。</p>
</li>
<li>
<p><strong>指向控制块的指针</strong>:控制块是一个动态分配的内存区域,存放着:</p>
<ul>
<li>
<p><strong>引用计数(Shared Count)</strong>:有多少个 <code>shared_ptr</code> 拥有它。当你拷贝一个 <code>shared_ptr</code> 时,计数器加 1;当一个 <code>shared_ptr</code> 销毁时,计数器减 1。<strong>计数 &gt; 0</strong>:资源保持有效。<strong>计数 = 0</strong>:最后一个“拥有者”负责调用 <code>delete</code> 销毁资源。</p>
<blockquote>
<p>引用计数的增加和减少是原子操作(使用类似 <code>std::atomic</code> 的机制)。这样可以保证在多线程环境下,多个线程同时拷贝或销毁指向同一个对象的 <code>shared_ptr</code> 时,计数器不会乱掉,从而避免内存泄漏或重复释放。</p>
<p>它所指向的对象数据本身并不是线程安全的,多线程读写对象需要额外加锁。</p>
</blockquote>
</li>
<li>
<p><strong>弱引用计数(Weak Count)</strong>:有多少个 <code>weak_ptr</code> 正在观察它。</p>
</li>
<li>
<p><strong>自定义删除器(Deleter)</strong>:如果需要特殊的释放逻辑。</p>
</li>
</ul>
</li>
</ol>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;memory&gt;

struct Widget {
    Widget() { std::cout &lt;&lt; "Widget Created\n"; }
    ~Widget() { std::cout &lt;&lt; "Widget Destroyed\n"; }
};

int main() {
    // 1. 推荐使用 std::make_shared,更安全且性能更好
    //在创建 shared_ptr 时,优先使用 std::make_shared&lt;T&gt;(args) 而不是 new,new 方式需要分两次申请内存(一次给对象,一次给控制块)
    //而make_shared只需要一次性申请一块大内存,减少了内存碎片。避免在构造过程中如果抛出异常导致内存泄漏。
    std::shared_ptr&lt;Widget&gt; sp1 = std::make_shared&lt;Widget&gt;();
   
    std::cout &lt;&lt; "Count: " &lt;&lt; sp1.use_count() &lt;&lt; std::endl; // 输出 1

    {
      std::shared_ptr&lt;Widget&gt; sp2 = sp1; // 拷贝,计数加 1
      std::cout &lt;&lt; "Count: " &lt;&lt; sp1.use_count() &lt;&lt; std::endl; // 输出 2
    } // sp2 离开作用域,计数减 1

    std::cout &lt;&lt; "Count: " &lt;&lt; sp1.use_count() &lt;&lt; std::endl; // 输出 1
   
    return 0;
} // sp1 离开作用域,计数变为 0,Widget 被销毁
</code></pre>
<p><strong>循环引用:如果 A 拥有 B,B 也拥有 A,两者的引用计数永远不会回零,导致内存泄漏。解决方法:其中一方使用 <code>std::weak_ptr</code>。</strong></p>
<h2 id="stdunique_ptr">std::unique_ptr</h2>
<p>独占所有权的</p>
<ul>
<li>禁用拷贝 (Deleted Functions)</li>
</ul>
<p>它显式地删除了拷贝构造函数和拷贝赋值操作符。</p>
<pre><code class="language-c++">unique_ptr(const unique_ptr&amp;) = delete;            // 禁止拷贝构造
unique_ptr&amp; operator=(const unique_ptr&amp;) = delete; // 禁止拷贝赋值
</code></pre>
<p>如果你尝试 <code>p2 = p1</code>,编译器会直接报错,在编译阶段就消灭了“多指针竞争同一资源”的可能性。</p>
<ul>
<li>实现移动语义 (Move Semantics)</li>
</ul>
<p>虽然不能拷贝,但它允许<strong>移动</strong>。它实现了移动构造函数,将内部指针的值“偷”给新对象,并将原对象的内部指针置为 <code>nullptr</code>。</p>
<pre><code class="language-c++">unique_ptr(unique_ptr&amp;&amp; other) noexcept {
    ptr = other.ptr;
    other.ptr = nullptr; // 原指针瞬间变为空
}
</code></pre>
<ul>
<li>自动释放机制 (RAII)</li>
</ul>
<p><code>unique_ptr</code> 的析构函数是自动管理内存的核心。当 <code>unique_ptr</code> 对象离开作用域(在栈上被销毁)时,析构函数会自动调用 <code>delete</code>:</p>
<pre><code class="language-C++">~unique_ptr() {
    if (ptr != nullptr) {
      delete ptr; // 自动销毁堆内存
    }
}
</code></pre>
<ul>
<li>示例</li>
</ul>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;memory&gt; // 包含头文件

struct Task {
    int id;
    Task(int i) : id(i) { std::cout &lt;&lt; "Task " &lt;&lt; id &lt;&lt; " created\n"; }
    ~Task() { std::cout &lt;&lt; "Task " &lt;&lt; id &lt;&lt; " destroyed\n"; }
};

int main() {
    // 1. 创建:推荐使用 std::make_unique (C++14)
    std::unique_ptr&lt;Task&gt; p1 = std::make_unique&lt;Task&gt;(101);

    // 2. 拷贝尝试(编译错误!)
    // std::unique_ptr&lt;Task&gt; p2 = p1;

    // 3. 移动:所有权从 p1 转移到 p3
    std::unique_ptr&lt;Task&gt; p3 = std::move(p1);
   
    if (p1 == nullptr) {
      std::cout &lt;&lt; "p1 is now empty.\n"; // p1 变成了空指针
    }

    // 4. 自动释放
    // 当 p3 离开作用域时,Task 101 会自动销毁
    return 0;
}
</code></pre>
<h2 id="stdweak_ptr">std::weak_ptr</h2>
<ul>
<li>解决循环引用(Memory Cycle)</li>
</ul>
<p>如果两个对象互相使用 <code>shared_ptr</code> 指向对方,它们的计数永远不会减到 0,导致内存泄漏。将其中一方改为 <code>weak_ptr</code> 即可打破僵局。</p>
<ul>
<li>解决“对象还活着吗”的监测问题</li>
</ul>
<p>原生指针无法知道它指向的内存是否已被释放。<code>weak_ptr</code> 可以安全地探测对象的状态,而不会延长对象的寿命</p>
<ul>
<li>
<p>只观察,不拥有</p>
<p><code>weak_ptr</code> 并不直接参与对象的生命周期管理。</p>
<ul>
<li><strong>不影响释放:</strong> 当所有强引用(<code>shared_ptr</code>)销毁时,对象就会被释放,即使还有 <code>weak_ptr</code> 指向它。</li>
<li><strong>不能直接访问:</strong> 你不能直接用 <code>*</code> 或 <code>-&gt;</code> 操作 <code>weak_ptr</code>。你必须先把它“提升”为一个 <code>shared_ptr</code>。</li>
</ul>
</li>
</ul>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;memory&gt;

int main() {
    std::shared_ptr&lt;int&gt; sp = std::make_shared&lt;int&gt;(42);
    std::weak_ptr&lt;int&gt; wp = sp; // wp 观察 sp,不增加计数

    std::cout &lt;&lt; "Count: " &lt;&lt; sp.use_count() &lt;&lt; std::endl; // 输出 1

    // 如何使用 wp 指向的数据?
    if (std::shared_ptr&lt;int&gt; tempSp = wp.lock()) { // 尝试锁定(提升)
      std::cout &lt;&lt; "Data: " &lt;&lt; *tempSp &lt;&lt; std::endl;
    } else {
      std::cout &lt;&lt; "Object is already destroyed." &lt;&lt; std::endl;
    }

    sp.reset(); // 手动释放强引用

    if (wp.expired()) { // 检查对象是否已失效
      std::cout &lt;&lt; "Object is gone!" &lt;&lt; std::endl;
    }

    return 0;
}
</code></pre>
<h2 id="c分析工具">C++分析工具</h2>
<p>静态分析工具cppcheck,动态分析工具valgrind</p>
<h3 id="cppcheck">cppcheck</h3>
<ul>
<li>安装</li>
</ul>
<blockquote>
<p>sudo apt install cppcheck</p>
</blockquote>
<h4 id="使用">使用</h4>
<ul>
<li>语法</li>
</ul>
<blockquote>
<p>cppcheck [选项] [文件/目录]</p>
</blockquote>
<ul>
<li>检查文件</li>
</ul>
<blockquote>
<p>//单个文件</p>
<p>cppcheck hello.cpp</p>
<p>//文件夹</p>
<p>cppcheck filepath</p>
</blockquote>
<ul>
<li>启用检测规则</li>
</ul>
<blockquote>
<p>启用所有检测规则   启用性能、代码风格等规则</p>
<p>cppcheck --enable==all [文件/目录]</p>
<pre><code class="language-bat">--enable=style       # 代码风格问题
--enable=warning   # 编译器警告级别问题
--enable=performance # 性能问题
--enable=portability # 可移植性问题
--enable=information # 信息性消息
--enable=unusedFunction # 未使用的函数
--enable=missingInclude # 缺失头文件
</code></pre>
<p>生成详细报告</p>
<p>cppcheck --verbose [文件/目录]</p>
<p>2&gt; 将其重定向到文件</p>
<p>cppcheck[文件/目录] 2&gt; result.txt</p>
<p>--std=c++17</p>
<p>--jobs=4</p>
<p>--suppress</p>
</blockquote>
<h3 id="valgrind">valgrind</h3>
<p>valgrind 会使程序运行速度显著变慢(通常 4-10 倍)</p>
<ul>
<li>安装</li>
</ul>
<blockquote>
<p>sudo apt install vargrind -y</p>
</blockquote>
<h4 id="使用-1">使用</h4>
<ul>
<li>语法</li>
</ul>
<blockquote>
<p>valgrind [选项] ./hello</p>
</blockquote>
<ul>
<li>参数</li>
</ul>
<blockquote>
<p>valgrind --tool=memcheck \         # 使用 memcheck 工具(内存错误检测)<br>
--leak-check=full \      # 进行完整的内存泄漏检测<br>
--show-leak-kinds=all \    # 显示所有类型的泄漏<br>
--track-origins=yes \      # 跟踪未初始化值的来源<br>
--verbose \                # 显示详细执行信息<br>
./hello        # 要检查的程序和参数</p>
</blockquote>
<ul>
<li>输出</li>
</ul>
<blockquote>
<p>Invalid read/write of size X    # 非法内存访问<br>
Use of uninitialised value      # 使用未初始化值<br>
Conditional jump/move depends on uninitialised value# 条件依赖于未初始化值<br>
Invalid free() / delete / delete[]# 非法释放<br>
Mismatched free() / delete / delete[]# 分配/释放不匹配<br>
Memory leak                     # 内存泄漏</p>
</blockquote>
<h2 id="思考">思考</h2>
<p>二级指针的使用场景</p>
<ol>
<li><strong>动态分配二维数组</strong>:使用指针的指针来表示二维数组,可以动态分配和释放内存。</li>
<li><strong>函数中修改指针</strong>:当需要在函数内部改变指针的指向(例如分配内存)并让改变在函数外部生效时,需要传递指针的指针(或指针的引用)。</li>
<li><strong>处理字符串数组(命令行参数)</strong>:例如<code>main</code>函数中的<code>char** argv</code>,表示字符串数组。</li>
<li><strong>实现数据结构</strong>:如链表、树等数据结构的某些操作中,需要修改指针的指向。</li>
</ol>
<ul>
<li>动态分配二维数组</li>
</ul>
<pre><code class="language-C++">#include &lt;iostream&gt;

int main() {
    int rows = 3, cols = 4;

    // 分配一个指针数组,每个指针指向一行
    int** matrix = new int*;
    for (int i = 0; i &lt; rows; ++i) {
      matrix = new int;
    }

    // 初始化并打印
    int count = 0;
    for (int i = 0; i &lt; rows; ++i) {
      for (int j = 0; j &lt; cols; ++j) {
            matrix = count++;
            std::cout &lt;&lt; matrix &lt;&lt; ' ';
      }
      std::cout &lt;&lt; std::endl;
    }

    // 释放内存
    for (int i = 0; i &lt; rows; ++i) {
      delete[] matrix;
    }
    delete[] matrix;

    return 0;
}
</code></pre>
<ul>
<li>函数中修改指针</li>
</ul>
<p>写一个函数,它分配内存并将指针指向这块内存。如果只传递指针(值传递),那么函数内部修改的是指针的副本,原始指针不会改变。因此需要传递指针的指针(或指针的引用)。</p>
<pre><code class="language-C++">#include &lt;iostream&gt;

void allocateMemory(int** ptr) {
    *ptr = new int(100);// 修改原始指针的指向
}

int main() {
    int* p = nullptr;
    allocateMemory(&amp;p);   // 传递指针的地址
    std::cout &lt;&lt; *p &lt;&lt; std::endl; // 输出100
    delete p;
    return 0;
}
</code></pre>
<ul>
<li>处理字符串数组</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(int argc, char** argv) {
    // 打印命令行参数
    for (int i = 0; i &lt; argc; ++i) {
      std::cout &lt;&lt; "argv[" &lt;&lt; i &lt;&lt; "] = " &lt;&lt; argv &lt;&lt; std::endl;
    }
    return 0;
}
</code></pre>
<p>out</p>
<pre><code class="language-bat">root1@ubuntu:~/work/hello/build$ ./hello_cmake_ga bcd ef a
argv = ./hello_cmake_g
argv = a
argv = bcd
argv = ef
argv = a
</code></pre>
<ul>
<li>数据结构</li>
</ul>
<p>以链表的删除节点为例,有时候我们需要修改头指针(当删除的是头节点时),因此传递二级指针可以方便地修改头指针。</p>
<pre><code class="language-cpp">struct Node {
    int data;
    Node* next;
};

// 使用二级指针删除链表中值为key的节点
void deleteNode(Node** head, int key) {
    Node* temp = *head;
    Node* prev = nullptr;

    // 如果头节点就是要删除的节点
    if (temp != nullptr &amp;&amp; temp-&gt;data == key) {
      *head = temp-&gt;next; // 修改头指针
      delete temp;
      return;
    }

    // 查找要删除的节点
    while (temp != nullptr &amp;&amp; temp-&gt;data != key) {
      prev = temp;
      temp = temp-&gt;next;
    }

    if (temp == nullptr) return;

    // 从链表中删除节点
    prev-&gt;next = temp-&gt;next;
    delete temp;
}
</code></pre><br><br>
来源:https://www.cnblogs.com/hjk-airl/p/19440157
頁: [1]
查看完整版本: 【C++】智能指针