一篇文章带你了解C++智能指针详解

目录
  • 为什么要有智能指针?
  • 智能指针的使用及原理
    • RALL
    • shared_ptr的使用注意事项
      • 创建
      • 多个 shared_ptr 不能拥有同一个对象
      • shared_ptr 的销毁
      • shared_ptr 的线程安全问题
      • shared_ptr 的循环引用
      • unique_ptr的使用
  • unique_ptr
  • 总结

为什么要有智能指针?

因为普通的指针存在以下几个问题:

  • 资源泄露
  • 野指针
    • 未初始化
    • 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道
  • 异常安全问题
  • 如果在 malloc和free 或者 new和delete 之间如果存在抛异常,那么也会导致内存泄漏。

资源泄漏示例代码:

int main(){
	int *p = new int;
	*p = 1;
	p = new int; // 未释放之前申请的资源,导致内存泄漏
	delete p;

	return 0;
}

野指针示例代码:

int main(){
	int *p1 = new int;
	int *p2 = p1;
	delete p1;
	*p2 = 1; // 申请的内存已经被释放掉了,  

	return 0;
}
int main(){
	int *p;
	*p = 1; // 程序直接报错, 使用了未初始化的变量
	return 0;
}

解决方法:智能指针

智能指针的使用及原理

  • 具有RALL 特性
  • 重载了 operator* 和 operator ->,使其具有了指针一样的行为

RALL

RALL(Resource Acquistion Is Initialization)是一种利用对象生命周期来控制程序资源(如内存,文件句柄,网络连接,互斥量等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。相当于利用 对象 管理了一份资源。这样的优势在于

1.不需要显式的释放资源(对象析构时,自动释放资源)

2.采用这种方式,对象所需的资源在其生命周期内始终保持有效。

智能指针就是一个实例出来的对象

C++98版本的库中就提供了auto_ptr的智能指针。但是 auto_ptr存在当对象拷贝或者赋值之后,前面的对象就悬空了。

C++11 提供更靠谱的并且支持拷贝的 shared_ptr

shared_ptr :

通过引用计数的方式实现多个shared_ptr 对象之间共享资源。

shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

unique_ptr :

确保一个对象同一时刻只能被一个智能指针引用,可以转移所有权(可以从一个智能指针转移到另一个智能指针)

auto_ptr :

C++11 已弃用, 与unique_ptr 类似

使用时,需包含头文件

 #include <memory>

shared_ptr的使用注意事项

创建

1.
shared_ptr<int> ptr{new int(3)};
2.
shared_ptr<int> ptr;
ptr.reset(new int(3));
3.
shared_ptr<int> ptr = make_shared<int>(3);

shared_ptr 支持使用比较运算符,使用时,会调用共享指针内部封装的原始指针的比较运算符。

支持

==、!=、<、<=、>、>=

使用 比较运算符 的前提 必须是 同类型

示例:

shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<int> p2 = make_shared<int>(2);
shared_ptr<int> p3;
shared_ptr<double> p4 = make_shared<double>(1);

bool b1 = p1 < p2; 		// true
bool b2 = p1 > p3;		// true, 非NULL 指针与 NULL 指针相比 ,都是大于
bool b3 = p3 == p3;		// true
bool b4 = p4 < p2		// 编译失败,类型不一致

shared_ptr 可以使用强制类型转换,但是不能使用普通的强制类型转换符

1.shared_ptr 强制类型转换符 允许将其中包含的指针强制转换为其它类型

2.不能使用普通的强制类型转换运算符,否则会导致未定义行为

3.shared_ptr 的强制类型转换运算符包括
static_pointer_cast
dynamic_pointer_cast
const_pointer_cast

示例:

shared_ptr<void> p(new int);	// 内部保留 void* 指针
static_pointer_cast<int*>(p);	// 正确的 强制类型转换方式
shared_ptr<int> p1(static_cast<int*>(p.get()));	// 错误的强制类型转换方式,未定义错误

多个 shared_ptr 不能拥有同一个对象

利用代码理解

示例:

class Mytest{
public:
	Mytest(const string& str)
	:_str(str){}
	~Mytest(){
		std::cout << _str << "destory" << std::endl;
	}
private:
	string _str;
};

int main(){
	Mytest* p = new Mytest("shared_test");
	shared_ptr<Mytest> p1(p); 	// 该对象可以正常析构
	shared_ptr<Mytest> p2(p); // 对象销毁时,错误,读取位置 0xDDDDDDDD 时发生访问冲突。
	return 0;
}

上述代码, 共享指针 p1 对象在程序 结束时,调用析构,释放了p 所指向的空间, 当 p2 进行析构的时候,又释放p所指向的空间, 但是由于已经释放过了, 重复释放已经释放过的内存,导致段错误。

可以使用 shared_from_this 避免这种问题

改进代码:

class Mytest:public enable_shared_from_this<Mytest> {
public:
    Mytest(const string& str)
        :_str(str) {}
    ~Mytest() {
        std::cout << _str << "destory" << std::endl;
    }
    shared_ptr<Mytest> GetSharedptr() {
        return shared_from_this();
    }
private:
    string _str;
};
int main() {
    Mytest* p = new Mytest("shared_test");
    shared_ptr<Mytest> p1(p);
    shared_ptr<Mytest> p2 = p->GetSharedptr(); // 正确做法

    return 0;
}

shared_ptr 的销毁

shared_ptr 在初始化的时候,可以定义删除器,删除器可以定义为 普通函数、匿名函数、函数指针等符合要求的可调用对象

示例代码:

void delFun(string* p) {
    std::cout << "Fun delete " << *p << endl;
    delete p;
}
int main() {

    std::cout << "begin" << std::endl;
    shared_ptr<string> p1;
    {
        shared_ptr<string> p2(new string("p1"), [](string* p) {
            std::cout << "Lamda delete " << *p << std::endl;
            delete p;
        });
        p1 = p2;
        shared_ptr<string> p3(new string("p3"), delFun);
    }
    std::cout << "end" << std::endl;
    return 0;
}

执行结果:

begin
Fun delete p3
end
Lamda deletep1

分析结果:

首先 ,p3在{ }作用域内 ,生命周期最先结束,调用delFun作为删除器

其次,p2 也在{ } 作用域内,生命周期也结束了,但是因为 p1 和 p2 指向了同一个对象,所以p2 销毁只是将其 对象 引用计数 -1。

最后,程序运行结束,p1销毁,其对象引用计数-1 变为0,调用 删除器,销毁对象。

shared_ptr<char> p(new char[10]); // 编译能够通过,但是会造成资源泄漏
// 正确做法
shared_ptr<char> p(new char[10], [](char* p){
	delete p[];
	});
// 正确做法
shared_ptr<char> p(new char[10], default_delete<char[]>());
  • 可以为数组创建一个shared_ptr ,但是这样会造成资源泄露。因为 shared_ptr 提供默认的删除调用的是 delete,而不是 delete[]
  • 可以使用自定义删除器,删除器中使用 delete[]
  • 可以使用 default_delete 作为删除器,因为它使用 delete[]

shared_ptr 存在的问题:

1.循环引用
不同对象相互引用,形成环路

2.想要共享但是不想拥有对象

shared_ptr 的线程安全问题

1. shared_ptr 对象中引用计数是多个shared_ptr对象共享的,两个线程中shared_ptr的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2 这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。

2.shared_ptr 管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

// 1.因为线程安全问题是偶现性问题,main函数的n改大一些概率就变大了,就
容易出现了。
void SharePtrFunc(shared_ptr<Date>& sp, size_t n)
{
	cout << sp.Get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
		copy->_year++;
		copy->_month++;
		copy->_day++;
	}
}
int main()
{
	shared_ptr<Date> p(new Date);
	cout << p.Get() << endl;
	const size_t n = 100;
	thread t1(SharePtrFunc, p, n);
	thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	return 0;
}

shared_ptr 的循环引用

struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}

循环引用代码分析:

node1和node2两个智能指针对象指向两个节点,引用计数变成1,不需要手动delete。

node1的_next指向node2,node2的_prev指向node1,引用计数变成2。

node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

也就是说_next析构了,node2就释放了。

也就是说_prev析构了,node1就释放了。

但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

原理:

node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加

node1和node2的引用计数。

struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

unique_ptr

  • 同一个对象,只能有唯一的一个 unique_ptr 指向它
  • 继承了自动指针 auto_ptr,
  • 有助于避免发生异常时导致的资源泄漏

unique_ptr的使用

unique_ptr 定义了*、-> 运算符,没有定义 ++ 之类的指针算法

unique_ptr 不允许使用赋值语法进行初始化,必须使用普通指针直接初始化

unique_ptr 可以为 空

unique_ptr 不能使用普通的复制语义赋值, 可以使用 C++11 的 move() 函数

unique_ptr 获得新对象时,会销毁之前的对象

unique_ptr 防止拷贝的原理:

// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
// C++11防拷贝的方式:delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C++学习之移动语义与智能指针详解

    移动语义 1.几个基本概念的理解 (1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在于栈上(短暂存在栈)上 (2)右值包括:临时对象.匿名对象.字面值常量 (3)const 左值引用可以绑定到左值与右值上面,称为万能引用.正因如此,也就无法区分传进来的参数是左值还是右值. const int &ref = a;//const左值引用可以绑定到左值 const int &ref1 = 10;//const左值引用可以绑定到右值 (4)右值引用:只能绑定到右值不能绑

  • c++ 智能指针基础详解

    简介 在现代 C++ 编程中,标准库包含了智能指针(Smart pointers). 智能指针用来确保程序不会出现内存和资源的泄漏,并且是"异常安全"(exception-safe)的. 智能指针的使用 智能指针定义在头文件 memory 里的命名空间 std 中.它对于资源获取即初始化(RAII, Resource Acquisition Is Initialization) 编程理念至关重要.该理念的目的是保证对象初始化的时候也是资源获取的时候,从而使对象的所有资源在单行代码中创建

  • C++11智能指针中的 unique_ptr实例详解

    在前面一篇文章中,我们了解了 C++11 中引入的智能指针之一 shared_ptr 和 weak_ptr ,今天,我们来介绍一下另一种智能指针 unique_ptr . 往期文章参考: [C++11新特性] C++11 智能指针之shared_ptr [C++11新特性] C++11智能指针之weak_ptr unique_ptr介绍 unique是独特的.唯一的意思,故名思议,unique_ptr可以"独占"地拥有它所指向的对象,它提供一种严格意义上的所有权. 这一点和我们前面介绍

  • c++11 新特性——智能指针使用详解

    c++11添加了新的智能指针,unique_ptr.shared_ptr和weak_ptr,同时也将auto_ptr置为废弃(deprecated). 但是在实际的使用过程中,很多人都会有这样的问题: 不知道三种智能指针的具体使用场景 无脑只使用shared_ptr 认为应该禁用raw pointer(裸指针,即Widget*这种形式),全部使用智能指针 初始化方法 class A { public: A(int size){ this->size = size; } A(){} void Sh

  • C++ STL 四种智能指针的用法详解

    0.前言 C++ 标准模板库 STL(Standard Template Library) 一共给我们提供了四种智能指针:auto_ptr.unique_ptr.shared_ptr 和 weak_ptr,其中 auto_ptr 是 C++98 提出的,C++11 已将其摒弃,并提出了 unique_ptr 替代 auto_ptr.虽然 auto_ptr 已被摒弃,但在实际项目中仍可使用,但建议使用更加安全的 unique_ptr,后文会详细叙述.shared_ptr 和 weak_ptr 则是

  • 一篇文章带你了解C++智能指针详解

    目录 为什么要有智能指针? 智能指针的使用及原理 RALL shared_ptr的使用注意事项 创建 多个 shared_ptr 不能拥有同一个对象 shared_ptr 的销毁 shared_ptr 的线程安全问题 shared_ptr 的循环引用 unique_ptr的使用 unique_ptr 总结 为什么要有智能指针? 因为普通的指针存在以下几个问题: 资源泄露 野指针 未初始化 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道 异常安全问题 如果在 malloc和free 或

  • 一篇文章带你了解C++模板编程详解

    目录 模板初阶 泛型编程 函数模板 函数模板概念 函数模板格式 函数模板的原理 函数模板的实例化 模板参数的匹配原则 类模板 类模板的定义格式 类模板的实例化 总结 模板初阶 泛型编程 在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术.什么是泛型呢?实质上就是不使用具体数据类型(例如 int.double.float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业

  • C++智能指针详解

    目录 一. unique_ptr独占指针 特点 创建方式 传递方式 简单使用 隐藏危险 二. shared_ptr 计数指针 特点 传递方式 隐藏危险 三. weak_ptr 优缺点: 智能指针由原始指针的封装,优点是可以自动分配内存,不用担心内存泄漏问题. 用于解决独占/共享所有权指针的释放,传输等问题. 但是没有原始指针方便. 一. unique_ptr独占指针 特点 都是围绕独占展开 特点一: 如其名,独占.也就是说同一个内存空间同时只能有一个指针来管理. int* pi = new in

  • C++ boost scoped_ptr智能指针详解

    目录 一.智能指针-唯一所有者 二.接口类分析 一.智能指针-唯一所有者 boost::scoped_ptr 是一个智能指针,它是动态分配对象的唯一所有者. boost::scoped_ptr 无法复制或移动.此智能指针在头文件 boost/scoped_ptr.hpp 中定义. 二.接口类分析 scoped_array 分析 scoped_array 的类部分原始代码如下: template<class T> class scoped_array // noncopyable { priva

  • C++ Boost PointerContainer智能指针详解

    目录 一.提要 二.智能指针Boost.PointerContainer 三.练习 一.提要 在 C++11 中,Boost.PointerContainer是另一个智能指针,一般是用来生成集合数据的,本文阐述这种指针的特点和用法. 二.智能指针Boost.PointerContainer 库 Boost.PointerContainer 提供专门用于管理动态分配对象的容器.例如,在 C++11 中,您可以使用 std::vector<std::unique_ptr<int>> 创

  • C++Smart Pointer 智能指针详解

    目录 一.为啥使用智能指针呢 二.shared_ptr智能指针 三.unique_ptr智能指针 四.weak_ptr智能指针 五.智能指针怎么解决交叉引用,造成的内存泄漏 5.1交叉引用的栗子: 5.2解决方案 六.智能指针的注意事项 总结 一.为啥使用智能指针呢 标准库中的智能指针: std::auto_ptr --single ownership (C++98中出现,缺陷较多,被摒弃) std::unique_ptr --single ownership (C++11替代std::auto

  • 一篇文章带你了解C语言指针进阶

    目录 1.字符指针 2.指针数组 3.数组指针 4.函数指针 5.数组传参 总结 1.字符指针 我们已经知道了数组名在大部分时候表示数组的地址,指针本质上也表示一个地址,那么我们能否用指针来创建一个字符串呢? int main() { char arr1[] = "abcdef"; char arr2[] = "abcdef"; const char* p1 = "abcdef"; const char* p2 = "abcdef&qu

  • 一篇文章带你弄清楚Redis的精髓

    目录 一.Redis的特性 1.1 Redis为什么快? 1.2 Redis其他特性 1.3 Redis高可用 二.Redis数据类型以及使用场景 2.1 String 2.1.1 基本指令 2.1.2 应用场景 2.2 Hash 2.2.1 基本指令 2.2.2 应用场景 2.3 List 2.3.1 基本指令 2.3.2 应用场景 2.4 Set 2.4.1 基本指令 2.4.2 应用场景 2.5 ZSet(SortedSet) 2.5.1 基本指令 2.5.2 应用场景 三.Redis的事

  • 一篇文章带你使用Typescript封装一个Vue组件(简单易懂)

    一.搭建项目以及初始化配置 vue create ts_vue_btn 这里使用了vue CLI3自定义选择的服务,我选择了ts.stylus等工具.然后创建完项目之后,进入项目.使用快捷命令code .进入Vs code编辑器(如果没有code .,需要将编辑器的bin文件目录地址放到环境变量的path中).然后,我进入编辑器之后,进入设置工作区,随便设置一个参数,这里比如推荐设置字号,点下.这里是为了生成.vscode文件夹,里面有个json文件. 我们在开发项目的时候,项目文件夹内的文件很

随机推荐