c++中智能指针与裸指针的实现

 更新时间:2026年02月27日 16:17:17   作者:CSDN_RTKLIB  
本文主要介绍了c++中智能指针与裸指针的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

现在有一个shared共享指针,通过.get方法赋值给一个裸指针,如果共享指针本身声明周期到了,裸指针还能继续使用吗?裸指针会影响智能指针生命周期吗

一、说明

在这种情况下,当shared_ptr的生命周期结束时,它所管理的资源会根据引用计数来决定是否销毁:

  1. 核心原理shared_ptr通过引用计数管理资源,当最后一个shared_ptr被销毁时,引用计数变为0,资源会被自动释放。

  2. 裸指针的影响:通过get()方法获取的裸指针只是资源的一个普通指针,它不会影响shared_ptr的引用计数

  3. 具体行为

    • shared_ptr生命周期结束时,无论裸指针是否还在使用,shared_ptr都会正常递减引用计数
    • 如果此时引用计数变为0,资源会被销毁
    • 这会导致裸指针变成悬空指针(dangling pointer),继续使用会产生未定义行为

示例说明:

#include <memory>

int main() {
    int* raw_ptr = nullptr;
    
    {
        std::shared_ptr<int> shared_ptr = std::make_shared<int>(42);
        raw_ptr = shared_ptr.get();  // 获取裸指针
        // 此时引用计数为1
    }  // shared_ptr生命周期结束,引用计数减为0,资源被销毁
    
    // 此时raw_ptr成为悬空指针,访问它是危险的
    // *raw_ptr = 10;  // 未定义行为!
    return 0;
}

结论shared_ptr会正常销毁其管理的资源(当引用计数为0时),与通过get()获取的裸指针是否还在使用无关。这种用法存在风险,可能导致悬空指针问题,应尽量避免。

二、测试

执行下面代码

#include <memory>
#include <iostream>
int main() {
    int* raw_ptr = nullptr;
    {
        std::shared_ptr<int> shared_ptr = std::make_shared<int>(42);
        raw_ptr = shared_ptr.get();  
    }  
    std::cout << *raw_ptr << std::endl;
    *raw_ptr = 10; 
    std::cout << *raw_ptr << std::endl;
    return 0;
}

第一个输出为垃圾值,第二次输出是10,程序也没有崩溃,这是为什么?

要理解这个现象,核心是抓住 未定义行为(Undefined Behavior, UB)的本质——不是“必然崩溃”,而是“结果不可预测”。你的代码能输出10而非崩溃,只是一种“偶然的合法表象”,背后是内存管理的底层逻辑在起作用:

第一步:先理清内存的“销毁”到底是什么

shared_ptr 生命周期结束时,它做的事情是:

  1. 引用计数减为0,触发 资源释放(调用 delete 销毁堆上的 int 对象);
  2. 释放的本质是:告诉操作系统“这块堆内存可以被重新分配给其他代码使用了”,而不是“把这块内存的数据清零”或“标记为不可访问”。

简单说:堆内存被 delete 后,数据可能还留在原地,只是它的“使用权”被回收了——你后续再访问它,属于“非法侵占”,但操作系统不会立刻阻止(除非触发了内存保护机制)。

第二步:为什么第一个输出是“垃圾值”,第二个是10?

我们逐行拆解代码执行过程:

int* raw_ptr = nullptr;

{
    std::shared_ptr<int> shared_ptr = std::make_shared<int>(42);
    raw_ptr = shared_ptr.get();  // raw_ptr指向堆上的int(值为42)
}  // shared_ptr销毁:堆上的int被delete(使用权回收),但内存数据可能还是42

// 第一次访问*raw_ptr:非法访问已释放的堆内存
std::cout << *raw_ptr << std::endl;  // 输出“垃圾值”或42,都是UB的表现
// 第二次访问:给已释放的内存赋值10
*raw_ptr = 10;  // 非法赋值,但内存未被其他代码复用,所以能“成功”写入10
std::cout << *raw_ptr << std::endl;  // 读取刚写入的10,输出10

关键原因:

  1. 第一个输出“垃圾值”:可能是堆内存被 delete 后,数据没有被覆盖(比如还是42),也可能是被 操作系统/其他代码轻微修改(变成随机值)——本质是“读取了不属于你程序合法拥有的内存”,结果随机,所以叫“垃圾值”。
  2. 第二个输出10:因为在赋值 *raw_ptr = 10 时,这块堆内存还没被其他代码复用(操作系统没把它分配给别的变量),所以你的程序“碰巧”还能操作这块内存(写入10),后续读取自然能拿到10。

这就像:你租了个房子(堆内存),到期后退租(delete),钥匙被房东收回(使用权回收),但你还能偷偷溜进去(非法访问)——房子里的家具(数据)可能还在(垃圾值),你甚至能换套家具(赋值10),只要房东还没把房子租给别人(内存未复用),就不会被发现。

第三步:为什么程序没崩溃?

崩溃的本质是:程序触发了操作系统的“内存保护机制”(比如访问了“完全不存在的内存地址”“已被其他进程占用的内存”“无权限访问的内存”)。

而你的代码中:

  • raw_ptr 指向的是 曾经合法的堆内存地址(不是野指针指向的随机无效地址);
  • 这块内存还没被 操作系统分配给其他程序/线程,所以你的程序对它的“读/写”操作没有触发内存保护,自然不会崩溃。

但这是极端危险的“运气”——如果在 shared_ptr 销毁后、你访问 raw_ptr 之前,有其他代码(比如函数调用、动态内存分配)复用了这块堆内存,那么:

  • 读取 *raw_ptr 会拿到其他代码的数据(垃圾值);
  • 写入 *raw_ptr = 10 会覆盖其他代码的数据,导致程序逻辑错乱(比如变量值莫名改变、函数返回错误结果);
  • 更糟的是:如果这块内存被 操作系统标记为“不可访问”(比如后续分配给了更高权限的进程),此时访问会直接触发 段错误(SIGSEGV),程序崩溃。

核心结论:未定义行为的“不可预测性”

你的代码能运行且输出10,不代表程序正确,只是UB的一种“温和表现”。UB的可能结果包括:

  1. 程序正常运行,输出预期值(如你的情况);
  2. 输出垃圾值;
  3. 程序崩溃(段错误);
  4. 程序逻辑错乱(后续代码运行异常);
  5. 甚至在不同编译器(GCC/Clang/MSVC)、不同优化等级(O0/O2)下表现完全不同。

比如:如果开启编译器优化(-O2),可能会直接优化掉 *raw_ptr 的访问,输出完全无意义的值,甚至程序崩溃——因为编译器会假设“程序不会执行UB”,从而进行激进优化。

如何避免这种问题?

永远不要用 shared_ptr.get() 获取裸指针后,在 shared_ptr 销毁后使用。正确做法:

  1. shared_ptr 的生命周期覆盖裸指针的使用(扩大 shared_ptr 的作用域);
  2. 若需长期持有“弱引用”,用 std::weak_ptr(不会增加引用计数,可通过 lock() 安全判断资源是否存在);
  3. 直接传递 shared_ptr(而非裸指针),依赖其引用计数管理生命周期。

正确示例(扩大 shared_ptr 作用域):

int main() {
    std::shared_ptr<int> shared_ptr = std::make_shared<int>(42);
    int* raw_ptr = shared_ptr.get();  // 裸指针的使用被shared_ptr生命周期覆盖
    
    std::cout << *raw_ptr << std::endl;  // 合法,输出42
    *raw_ptr = 10;                       // 合法,修改的是shared_ptr管理的资源
    std::cout << *raw_ptr << std::endl;  // 合法,输出10
    
    return 0;
}  // shared_ptr销毁,资源正常释放,无悬空指针

最终总结

你看到的“第一个输出垃圾值、第二个输出10且不崩溃”,是 未定义行为下的偶然结果

  • 堆内存被释放后未被复用,导致你能非法读写;
  • 程序未触发内存保护机制,所以没崩溃。

但这种代码是“定时炸弹”——任何微小的改动(比如增加函数调用、开启优化、多线程环境)都可能让程序崩溃或逻辑错乱。永远不要依赖UB的表现,遵守智能指针的使用规则,避免悬空指针

三、问题

int* raw_ptr = nullptr;
std::cout << *raw_ptr << std::endl;

1.上面这样打印raw_ptr会程序崩溃,为什么
2.讲下野指针

问题1:int* raw_ptr = nullptr; std::cout << *raw_ptr << std::endl; 为什么会崩溃?

核心原因:nullptr 指向的是“明确无效的内存地址”,解引用会触发操作系统的内存保护机制,直接导致程序崩溃。

我们分3层讲清楚:

  1. nullptr 的本质nullptr 是C++11引入的“空指针常量”,它不指向任何合法的内存地址(通常对应底层的 0x0 地址,或操作系统预留的“无效地址区间”)。
    它和你之前代码中“指向已释放堆内存的指针”完全不同:前者是“明确的无效地址”,后者是“曾经有效、现在已被回收的地址”。

  2. 解引用 nullptr 的后果:操作系统会对内存地址进行权限管理——nullptr 指向的地址是“程序绝对无权访问”的(相当于“不存在的地址”)。
    当你执行 *raw_ptr 时,程序试图读写这个无效地址,会直接触发 段错误(SIGSEGV) 或“访问违规”,操作系统会强制终止程序(也就是“崩溃”)。

  3. 和你之前代码的对比

    • 之前的 raw_ptr 指向“已释放但未复用的堆内存”:地址本身是“合法存在”的,只是程序没有使用权,所以可能不崩溃(运气好);
    • 现在的 raw_ptr = nullptr:地址本身是“非法无效”的,程序一访问就触发内存保护,必然崩溃。

简单比喻:

  • 之前的情况:你溜进“已退租但未租给别人的房子”,可能没被发现;
  • 现在的情况:你试图闯进“根本不存在的房子”(比如地址是0号),一靠近就被保安(操作系统)拦下,直接“崩溃”。

问题2:野指针是什么?int* p; 是野指针吗?

先明确:野指针的定义

野指针(Wild Pointer) 是指:指向“不确定地址”或“无效地址”,且没有明确指向合法对象的指针
核心特征:指针的值是“随机的、未定义的”,或指向的内存已被释放/回收,访问它的行为是“未定义行为(UB)”。

再回答:int* p;是野指针吗?—— 是的,大概率是!

分两种场景看 int* p; 的行为:

  1. 局部变量场景(最常见):如果 p 是函数内的局部变量(栈上分配),C++ 不会自动初始化栈上的指针——p 的值是栈上的“垃圾值”(随机的二进制数据)。
    这个随机值可能指向:

    • 无效地址(比如 0x123456 这种不存在的地址);
    • 其他程序的内存地址(无权访问);
    • 本程序其他变量的地址(可能覆盖其他数据)。
      无论哪种,p 都是“指向不确定地址的指针”,属于 典型的野指针
  2. 全局/静态变量场景:如果 p 是全局变量或 static 变量,C++ 会默认将其初始化为 nullptr(或 0),此时它是“空指针”,不是野指针(空指针是“明确的无效地址”,野指针是“不确定的无效地址”,两者有区别)。

关键补充:野指针的3种常见来源

int* p;(未初始化的局部指针)只是野指针的一种,还有两种更隐蔽的情况:

  1. 未初始化的局部指针(如 int* p;);
  2. 指向已释放内存的指针(如你之前代码中,shared_ptr 销毁后的 raw_ptr);
  3. 指针指向的对象被 delete 后,未置为 nullptr(比如 int* p = new int; delete p;,之后 p 还是指向原地址,变成野指针)。

容易混淆的概念:野指针 ≠ 空指针

类型特点访问后果
野指针指针值随机、不确定可能崩溃、可能乱改数据
空指针(nullptr指针值明确(指向无效地址)几乎必然崩溃(可预测UB)

比如:

  • int* p;(局部变量)→ 野指针;
  • int* p = nullptr; → 空指针(不是野指针);
  • int* p = new int; delete p; → 野指针(指向已释放地址)。

总结两个问题的核心

  1. 解引用 nullptr 必崩溃:因为它指向“明确无效的地址”,触发内存保护;
  2. 野指针是“指向不确定/无效地址的指针”:局部未初始化的 int* p; 是野指针,空指针不是野指针;
  3. 所有野指针的访问都是UB,但后果不同(可能崩溃、可能乱码、可能“看似正常”),但本质都是极端危险的。

补充:如何避免野指针?

  1. 指针声明时立即初始化(要么指向合法对象,要么置为 nullptr):
    int* p = nullptr;int a = 10; int* p = &a;
  2. 动态内存释放后(deleteshared_ptr 销毁),避免再使用对应的指针;
  3. 若需长期持有“可能失效的引用”,用 std::weak_ptr(而非裸指针);
  4. 局部指针尽量缩短作用域,避免跨作用域传递裸指针。

总结

到此这篇关于c++中智能指针与裸指针的实现的文章就介绍到这了,更多相关c++ 智能指针与裸指针内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++实现截图截屏的示例代码

    C++实现截图截屏的示例代码

    本文主要介绍了C++实现截图截屏的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • OpenCV鼠标绘制矩形和截取矩形区域图像

    OpenCV鼠标绘制矩形和截取矩形区域图像

    这篇文章主要为大家详细介绍了OpenCV鼠标绘制矩形和截取矩形区域图像,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • 解读构造函数的调用规则、深拷贝与浅拷贝

    解读构造函数的调用规则、深拷贝与浅拷贝

    本文主要介绍了C++中的默认构造函数、拷贝构造函数以及深拷贝和浅拷贝的概念,并通过实际代码示例进行了详细讲解
    2024-11-11
  • C语言与C++内存管理超详细分析

    C语言与C++内存管理超详细分析

    C 语言内存管理指对系统内存的分配、创建、使用这一系列操作。在内存管理中,由于是操作系统内存,使用不当会造成毕竟麻烦的结果。本文将从系统内存的分配、创建出发,并且使用例子来举例说明内存管理不当会出现的情况及解决办法
    2022-05-05
  • C语言数据结构系列篇二叉树的概念及满二叉树与完全二叉树

    C语言数据结构系列篇二叉树的概念及满二叉树与完全二叉树

    在上一章中我们正式开启了对数据结构中树的讲解,介绍了树的基础。本章我们将学习二叉树的概念,介绍满二叉树和完全二叉树的定义,并对二叉树的基本性质进行一个简单的介绍。本章附带课后练习
    2022-02-02
  • C语言字符函数和字符串函数示例详解

    C语言字符函数和字符串函数示例详解

    本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字符串处理技巧,感兴趣的朋友一起看看吧
    2025-03-03
  • C语言宏定义结合全局变量的方法实现单片机串口透传模式

    C语言宏定义结合全局变量的方法实现单片机串口透传模式

    今天小编就为大家分享一篇关于C语言宏定义结合全局变量的方法实现单片机串口透传模式,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • C++中memcpy函数的使用以及模拟实现

    C++中memcpy函数的使用以及模拟实现

    memcpy是c和c++使用的内存拷贝函数,本文主要介绍了C++中memcpy函数的使用以及模拟实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • C++中的模板类继承和成员访问问题

    C++中的模板类继承和成员访问问题

    这篇文章主要介绍了C++中的模板类继承和成员访问问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • c语言获取当前工作路径的实现代码(windows/linux)

    c语言获取当前工作路径的实现代码(windows/linux)

    这篇文章主要介绍了c语言获取当前工作路径的实现代码(windows/linux),需要的朋友可以参考下
    2017-09-09

最新评论