c++中智能指针与裸指针的实现
前言
现在有一个shared共享指针,通过.get方法赋值给一个裸指针,如果共享指针本身声明周期到了,裸指针还能继续使用吗?裸指针会影响智能指针生命周期吗
一、说明
在这种情况下,当shared_ptr的生命周期结束时,它所管理的资源会根据引用计数来决定是否销毁:
核心原理:
shared_ptr通过引用计数管理资源,当最后一个shared_ptr被销毁时,引用计数变为0,资源会被自动释放。裸指针的影响:通过
get()方法获取的裸指针只是资源的一个普通指针,它不会影响shared_ptr的引用计数。具体行为:
- 当
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 生命周期结束时,它做的事情是:
- 引用计数减为0,触发 资源释放(调用
delete销毁堆上的int对象); - 释放的本质是:告诉操作系统“这块堆内存可以被重新分配给其他代码使用了”,而不是“把这块内存的数据清零”或“标记为不可访问”。
简单说:堆内存被 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
关键原因:
- 第一个输出“垃圾值”:可能是堆内存被
delete后,数据没有被覆盖(比如还是42),也可能是被 操作系统/其他代码轻微修改(变成随机值)——本质是“读取了不属于你程序合法拥有的内存”,结果随机,所以叫“垃圾值”。 - 第二个输出10:因为在赋值
*raw_ptr = 10时,这块堆内存还没被其他代码复用(操作系统没把它分配给别的变量),所以你的程序“碰巧”还能操作这块内存(写入10),后续读取自然能拿到10。
这就像:你租了个房子(堆内存),到期后退租(delete),钥匙被房东收回(使用权回收),但你还能偷偷溜进去(非法访问)——房子里的家具(数据)可能还在(垃圾值),你甚至能换套家具(赋值10),只要房东还没把房子租给别人(内存未复用),就不会被发现。
第三步:为什么程序没崩溃?
崩溃的本质是:程序触发了操作系统的“内存保护机制”(比如访问了“完全不存在的内存地址”“已被其他进程占用的内存”“无权限访问的内存”)。
而你的代码中:
raw_ptr指向的是 曾经合法的堆内存地址(不是野指针指向的随机无效地址);- 这块内存还没被 操作系统分配给其他程序/线程,所以你的程序对它的“读/写”操作没有触发内存保护,自然不会崩溃。
但这是极端危险的“运气”——如果在 shared_ptr 销毁后、你访问 raw_ptr 之前,有其他代码(比如函数调用、动态内存分配)复用了这块堆内存,那么:
- 读取
*raw_ptr会拿到其他代码的数据(垃圾值); - 写入
*raw_ptr = 10会覆盖其他代码的数据,导致程序逻辑错乱(比如变量值莫名改变、函数返回错误结果); - 更糟的是:如果这块内存被 操作系统标记为“不可访问”(比如后续分配给了更高权限的进程),此时访问会直接触发 段错误(SIGSEGV),程序崩溃。
核心结论:未定义行为的“不可预测性”
你的代码能运行且输出10,不代表程序正确,只是UB的一种“温和表现”。UB的可能结果包括:
- 程序正常运行,输出预期值(如你的情况);
- 输出垃圾值;
- 程序崩溃(段错误);
- 程序逻辑错乱(后续代码运行异常);
- 甚至在不同编译器(GCC/Clang/MSVC)、不同优化等级(O0/O2)下表现完全不同。
比如:如果开启编译器优化(-O2),可能会直接优化掉 *raw_ptr 的访问,输出完全无意义的值,甚至程序崩溃——因为编译器会假设“程序不会执行UB”,从而进行激进优化。
如何避免这种问题?
永远不要用 shared_ptr.get() 获取裸指针后,在 shared_ptr 销毁后使用。正确做法:
- 让
shared_ptr的生命周期覆盖裸指针的使用(扩大shared_ptr的作用域); - 若需长期持有“弱引用”,用
std::weak_ptr(不会增加引用计数,可通过lock()安全判断资源是否存在); - 直接传递
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层讲清楚:
nullptr的本质:nullptr是C++11引入的“空指针常量”,它不指向任何合法的内存地址(通常对应底层的0x0地址,或操作系统预留的“无效地址区间”)。
它和你之前代码中“指向已释放堆内存的指针”完全不同:前者是“明确的无效地址”,后者是“曾经有效、现在已被回收的地址”。解引用
nullptr的后果:操作系统会对内存地址进行权限管理——nullptr指向的地址是“程序绝对无权访问”的(相当于“不存在的地址”)。
当你执行*raw_ptr时,程序试图读写这个无效地址,会直接触发 段错误(SIGSEGV) 或“访问违规”,操作系统会强制终止程序(也就是“崩溃”)。和你之前代码的对比:
- 之前的
raw_ptr指向“已释放但未复用的堆内存”:地址本身是“合法存在”的,只是程序没有使用权,所以可能不崩溃(运气好); - 现在的
raw_ptr = nullptr:地址本身是“非法无效”的,程序一访问就触发内存保护,必然崩溃。
- 之前的
简单比喻:
- 之前的情况:你溜进“已退租但未租给别人的房子”,可能没被发现;
- 现在的情况:你试图闯进“根本不存在的房子”(比如地址是0号),一靠近就被保安(操作系统)拦下,直接“崩溃”。
问题2:野指针是什么?int* p; 是野指针吗?
先明确:野指针的定义
野指针(Wild Pointer) 是指:指向“不确定地址”或“无效地址”,且没有明确指向合法对象的指针。
核心特征:指针的值是“随机的、未定义的”,或指向的内存已被释放/回收,访问它的行为是“未定义行为(UB)”。
再回答:int* p;是野指针吗?—— 是的,大概率是!
分两种场景看 int* p; 的行为:
局部变量场景(最常见):如果
p是函数内的局部变量(栈上分配),C++ 不会自动初始化栈上的指针——p的值是栈上的“垃圾值”(随机的二进制数据)。
这个随机值可能指向:- 无效地址(比如
0x123456这种不存在的地址); - 其他程序的内存地址(无权访问);
- 本程序其他变量的地址(可能覆盖其他数据)。
无论哪种,p都是“指向不确定地址的指针”,属于 典型的野指针。
- 无效地址(比如
全局/静态变量场景:如果
p是全局变量或static变量,C++ 会默认将其初始化为nullptr(或0),此时它是“空指针”,不是野指针(空指针是“明确的无效地址”,野指针是“不确定的无效地址”,两者有区别)。
关键补充:野指针的3种常见来源
int* p;(未初始化的局部指针)只是野指针的一种,还有两种更隐蔽的情况:
- 未初始化的局部指针(如
int* p;); - 指向已释放内存的指针(如你之前代码中,
shared_ptr销毁后的raw_ptr); - 指针指向的对象被
delete后,未置为nullptr(比如int* p = new int; delete p;,之后p还是指向原地址,变成野指针)。
容易混淆的概念:野指针 ≠ 空指针
| 类型 | 特点 | 访问后果 |
|---|---|---|
| 野指针 | 指针值随机、不确定 | 可能崩溃、可能乱改数据 |
空指针(nullptr) | 指针值明确(指向无效地址) | 几乎必然崩溃(可预测UB) |
比如:
int* p;(局部变量)→ 野指针;int* p = nullptr;→ 空指针(不是野指针);int* p = new int; delete p;→ 野指针(指向已释放地址)。
总结两个问题的核心
- 解引用
nullptr必崩溃:因为它指向“明确无效的地址”,触发内存保护; - 野指针是“指向不确定/无效地址的指针”:局部未初始化的
int* p;是野指针,空指针不是野指针; - 所有野指针的访问都是UB,但后果不同(可能崩溃、可能乱码、可能“看似正常”),但本质都是极端危险的。
补充:如何避免野指针?
- 指针声明时立即初始化(要么指向合法对象,要么置为
nullptr):int* p = nullptr;或int a = 10; int* p = &a;; - 动态内存释放后(
delete或shared_ptr销毁),避免再使用对应的指针; - 若需长期持有“可能失效的引用”,用
std::weak_ptr(而非裸指针); - 局部指针尽量缩短作用域,避免跨作用域传递裸指针。
总结
到此这篇关于c++中智能指针与裸指针的实现的文章就介绍到这了,更多相关c++ 智能指针与裸指针内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
c语言获取当前工作路径的实现代码(windows/linux)
这篇文章主要介绍了c语言获取当前工作路径的实现代码(windows/linux),需要的朋友可以参考下2017-09-09


最新评论