C++ - 智能指针 - auto_ptr - unique_ptr - std::shared_ptr - weak_ptr

news2024/11/20 0:33:48

前言

C++当中的内存管理机制需要我们自己来进行控制,比如 在堆上 new 了一块空间,那么当这块空间不需要再使用的时候。我们需要手动 delete 掉这块空间,我们不可能每一次都会记得,而且在很大的项目程序当中,造成内存泄漏也是不少了。

C++ 不像 Java一样,有 gc,也就是垃圾回收站器,因为 Java 在操作系统之上还有一层虚拟机,这层虚拟机可以理解为运行的一个进程,所有的Java 程序都是在这个 虚拟机 之上运行的。在虚拟机当中就有 这个 gc 在运作。

gc 简单来说就是把所有动态开辟都记录起来,当不需要使用的时候就自动释放了。

但是 Java 在实现这个 虚拟机 是有消耗的,所以在特别需要效率的项目上很多都用的是 C++,比如在游戏开发,服务器当中。

而且,在C++ 当中 我们自己控制 delete 空间的话,就算是我们想起来 要控制,程序当中设计得复杂,我们都不能完全的控制,比如下述:
 

pair<int, int>* pa = new pair<int,int>;

func();

delete pa;

如果没有 func()函数的话,那么最后肯定是能释放掉的,但是,现在有一个不确定因素在 func()函数当中,我们不清楚在func()函数当中到底做了什么,我们就不能 100% 没问题。假设在func()当中抛异常,被主函数当中捕获了,那么就有可能会直接跳过这个 delete。

虽然,抛异常的跳过的问题,我们可以 先 delete 需要释放的空间,然后再抛出异常,解决很多场景。但是有一个场景是不能解决的:

new 也是可能会抛出异常的,虽然概率很低,但是也是有可能的,那么下面的场景:

 如果 p1 的new 抛出异常,那么还好,没有什么问题,如果是 p2 抛出异常的话,那么 p1 开的空间应该怎么解决呢?

如果此时我们再把 func()函数加上:

pair<int, int>* p1 = new pair<int,int>;
pair<int, int>* p2 = new pair<int,int>;

try{
    func();
}catch(...){
    delete p1;
    delete p2;
    throw ....................
}

delete pa;

 如果 func()函数抛出异常,那么就要 delete p1 和 p2。

那如果还不嫌麻烦,如果在多来几个呢? p3  p4  p5 p6 ····························

基于上述问题,就搞出了 智能指针。

智能指针

 智能指针虽然听上去很高端,但实际很简单。

就更之前在 各种库当中实现的迭代器是一样,把指针包装了一下,在其中实现各个函数,比如指针需要的 operator*()   ,  operator->() 函数等等,最重要的是实现 ~析构函数,他是我们自动 释放空间的核心,因为在创建类的对象之后,编译器会自动的调用这个对象的析构函数,用于析构这个对象和对象空间。

template<class T>
class SmartPtr
{
public:
	// RAII
	// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
	// 1、RAII管控资源释放
	// 2、像指针一样
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};

 这样的话,只需要用这个指针构造 一个指针指针对象,那么就会自动维护这个指针:
 

pair<int, int>* p1 = new pair<int,int>;
SmartPtr sp1(p1);

此时,p1 指针维护的空间我就不需要手动释放了,自己就会释放。

当然上述的实现,是分开的,我们可以不用 p1 传进去,直接把 new pair<int,int>; 当参数传进去就行,因为 new 本身返回的就是 这个 空间的指针。

SmartPtr<pair<string, string>> sp2(new pair<string, string>);
SmartPtr<pair<string, string>> sp3(new pair<string, string>);

所以说,虽然 C++ 没有 gc ,但是有智能指针,我们可以自己实现,自己实现的智能指针没有什么问题的话,这个智能指针是可以帮助我们解决绝大部分的内存泄漏问题的,异常也不用怕了。

 有人把这种机制叫做 RAII

RAII 

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

 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

我们实际上把管理一份资源的责任托管给了一个对象。对象是给编译器进行管理。也就是利用了对象无论以什么方式离开了作用域都会调用其析构函数的特点。

也就是获取到资源的时候,马上初始化。

这样做的好处是

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
     

 智能指针像指针一样使用

 其实上述也说过了,智能指针的构造函数,是利用指针来构造这个智能指针的对象,析构函数是释放这个指针指向的空间。

那么,指针的使用还有 operator*()   operator->()  operator[]() 这样子的函数要支持。

实现就更迭代器的实现是一样的,可读可写。

template<class T>
class SmartPtr
{
public:
	// RAII
	// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
	// 1、RAII管控资源释放
	// 2、像指针一样
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

智能指针之间的赋值问题(拷贝问题)

 如下面两个智能指针:

	SmartPtr<string> sp1(new string("xxxxx"));
	SmartPtr<string> sp2(new string("yyyyy"));

我们把两个指针 delete 的地址打印一下,输出:

 两个空间都释放了。

但是,如果上述的两个智能指针对象,进行相互赋值操作的话,就会出现问题:
 

	SmartPtr<string> sp1(new string("xxxxx"));
	SmartPtr<string> sp2(new string("yyyyy"));

	sp1 =sp2;

此时输出:
 

 发现,两次释放的是一个空间,这就出问题了啊。

首先,是对一块空间析构了两次,我们说对同一块空间析构两次是可能会出现问题的。

其次是,原本是两个智能指针维护的是两块空间,但是现在我们只是放了一块,另一块空间没有释放就造成了内存泄漏。

其实是因为我们没有写 拷贝构造函数,如果我们没有写拷贝构造函数的话,编译器就会自己实现一个 默认的浅拷贝的 拷贝构造函数。按照之前我们应该自己用现代写法来实现了一下 拷贝构造函数。

但是,在智能指针当中我们就不能 用上述深拷贝的方式来解决,因为我们写的智能指针,本质上就是要模拟实现指针来实现的。而原生指针在赋值这一块,本来就是浅拷贝。


auto_ptr

 为了解决上述所说的 内存管理问题,在C++98 当中就提出了 auto_ptr 。它可以解决上述的问题,但是这个 auto_ptr 有一个非常大的坑,具体请看下述:

 为了演示 到底是没有释放空间,我们需要创建一个类来帮助我们,因为 ,如果是我们自己实现的智能指针,那么我们可以在 析构的时候打印一下,来提示我们此时发生了空间的释放,但是在库当中的实现的 auto_prt 我们没办法打印,但是, delete 一个对象就调用这个对象的析构函数,所以我们实现一个对象的析构函数,在其中打印这个 以下提示我们就行:
 

class A
{
public:
	A(int a = 1)
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a;
};

如何使用 auto_ptr 呢?如下所示:
 

std::auto_ptr<A> sp1(new A(1));

auto_ptr 是一个模版类,模版参数只需要给出 需要管理的类型即可,不需要给出指针(如上述的 int )。auto_ptr 的构造函数,可以直接传入这个 需要管理的空间的 地址,像上述我们就直接使用 new 开空间后返回这个空间的地址来实现。

 输出:
 

发现,没有显示释放这个空间我们都释放了这个空间。

 如果我们进行像上述在拷贝问题当中实现的赋值一样的,如下代码所示:
 

	auto_ptr<A> aptr1(new A(1));
	auto_ptr<A> aptr2(new A(1));

	aptr1 = aptr2;

输出:
 

A(int a = 1)
A(int a = 1)
~A()
~A()

发现,它也正常释放了。

 我们要发现他的问题,就得知道他做了什么,如下例子阐述:

 如上述所示,我们用 ap1 构造了 ap3,也是相当于是 赋值了,那么此时发生了什么呢?

其实 auto_ptr 指针在赋值这一块,做了一件事情 ----- 管理权转移

 如上述例子,他把 原本属于 ap1 的空间,转给了 赋值之后的 ap3 管理了,所以,我们看到上述的 ap1 指向的是 empty。

 在 auto_ptr 当中的 拷贝构造函数,相当于是实现了 移动构造的玩法,直接交换 ap1 和 ap3 当中的指针变量,把 ap1 指向 ap3 当中的空,把 ap3 当中的指针指向 ap1 原本维护的指针。

 但是只是相当于,这里并不是移动构造的玩法,移动构造是把 右值 当中的 将亡值(即将释放的值),把其中的资源直接转移到 新的 值 来维护。如果不是 将亡值 ,比如是 左值,我们是不敢直接转移的,因为 此时左值在后序程序当中很有可能会用到。

但是 auto_ptr 当中也就相当于是 强制进行 移动构造,不管是左值还是右值,这么左的风险,在上述也说过了,ap1 是左值,在后序是很有可能会用到了,这就是它最大的问题。

 举例:

如果是不懂的人,对 赋值之后的 ap1 进行操作的话,编译器甚至都不会报错,但是一运行程序就会出事

	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(1));

	auto_ptr<A> ap3(ap1);

	// 此时把 _a 成员变量修改为了 public:
	ap1->_a++;
	ap2->_a++;

 成功生成解决方案:

 出事儿:

 auto_ptr 其实是一段失败的代码,但是已经有人使用 autp_ptr 写了项目,委员会不敢删,所以延续至今,但是在一般实践,公司当中明确规定不能使用 auto_ptr 。

 auto_ptr 模拟实现

// C++98 管理权转移 auto_ptr
namespace bit
{
	template<class T>
	class auto_ptr
	{
	public:

		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
            // 一定要置空
            // 不然两个对象管理同一块空间
            // 在析构函数释放的时候就会释放两次
            // 就会报错
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

 unique_ptr

 在C++11 当中提供更加稳定的 unique_ptr 智能指针。

cplusplus.com/reference/memory/unique_ptr/

 unique_ptr 的解决方案的话,非常的简单粗暴,就是直接不允许你进行 unique_ptr 智能指针之间的赋值拷贝。

	unique_ptr<A> up1(new A(1));
	unique_ptr<A> up2(new A(1));

	up1 = up2;

 编译报错:
 

 error C2280: “std::unique_ptr<A,std::default_delete<A>> &std::unique_ptr<A,std::default_delete<A>>::operator =(const std::unique_ptr<A,std::default_delete<A>> &)”: 尝试引用已删除的函数

 unique_ptr的模拟实现

对于 unique_ptr 的模拟实现很简单,我们在类当中说过,要想某一个函数不给外部使用,右里那个三种方式,第一种是只声明不实现;第二种是 把这个函数用 private 修饰:第三种是在函数之后写上 "= delete" 意思就是把这个函数删除掉。

但是第一种,值声明不实现,别人可以在外部给你实现这个函数,所以建议用后面的两种方法。 

template<class T>
class unique_ptr
{
public:

	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

	// 像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	unique_ptr(const unique_ptr<T>&sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;

private:
	T* _ptr;
};

std::shared_ptr

当然,上述的 unique_ptr 版本的智能指针只是给我们手撕智能指针的模版,我们手撕一个 unique_ptr 非常简单。

但是 unique_ptr 比较不能解决 指针 赋值的问题。

所以,就有了 shared_ptr 智能指针的出现。

		shared_ptr<A> sp1(new A(1));
		shared_ptr<A> sp2(new A(1));

		sp1 = sp2;

		sp1->_a++;
		sp2->_a++;

		cout << sp1->_a << endl;
		cout << sp2->_a << endl;

 输出:
 

A(int a = 1)
A(int a = 1)
~A()
3
3
~A()

shared_ptr 模拟实现

 解决指针拷贝问题

 上述几种不同的 智能指针不同就不同在 拷贝构造函数 和 operator=() 两个函数的实现不同,

所以我们主要实现 shared_ptr 的拷贝构造函数,operator=()当中的 可以服用拷贝构造函数。

用一个shared_ptr 指针给另一个 shared_ptr  赋值和 拷贝构造,其中肯定是要 两个 智能指针管理两块空间的,主要是要解决 两个对象析构两次会报错的问题。

我们选择使用 引用计数的方式来解决:

也就是记录一下有多少 引用 引用了当前空间。在 某一个 智能指针 对象析构的时候,不先释放空间,先判断 当前引用计数是不是 0 ,如果是 大于 0的,说明当前 不止当前智能指针 指向这块空间,那么就 --引用计数;如果当前引用计数 是 0 ,那么说明当前就本 智能指针 指向这块空间,就可以 释放这块空间。

 但是,在类当中 应该使用什么 成员变量来 记录这个 引用计数呢?普通的变量可能是不能满足的,如果是 非静态的变量,每一个是存储在各个类当中的,我们要实现统一计数的话,非常的麻烦。

所以,我们使用静态的成员变量来存储这个 引用计数,因为 静态的成员变量不单单属于某一个对象,而是属于这个整个类,你可以理解为静态的成员变量是属于每一个对象的,每一个对象共用一个 静态成员变量。

 类似这样:

 代码实现:
 

public:
        // 构造函数
        shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
    
        // 析构函数
		~shared_ptr()
		{
            // -- 引用计数 之后 如果 == 0 ,就可以释放空间了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

        // 拷贝构造函数
        shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}

        // operator=() 函数实现
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
            // 先判断 赋值和被赋值的两个指针是否是重复的
            // 是就直接返回
			if (_ptr == sp._ptr)
				return *this;
            
            // 判断当前 赋值指针在赋值出去之前
            // 是否是所维护空间的唯一指针
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
            
            // 开始赋值
			_ptr = sp._ptr;
			_pcount = sp._pcount;
            
            // 引用计数++
			++(*_pcount);

			return *this;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

如上所示,operator=()函数才是最难实现的,我们要判断当前对象,也就是被赋值 的对象 在当前赋值之前,是否 右引用其他对象,如果引用了其他的对象,那么要先 -- 引用计数,看-- 之后的引用计数是否 == 0,如果是 0 ,就要把原有的 空间给释放掉,然后才能进行赋值操作,赋值就简单了,直接把 赋值对象 当中的 两个成员赋值过来,然后在 ++ 引用计数即可。

 而且,还需要注意,自己给自己赋值的情况:比如 sp1 和 sp2 都指向了 同一块空间,那么对于 :
sp1 = sp1 和 sp2 =sp1 两者实际上都是自己给自己赋值,在上述的代码当中不进行判断的话,是没有什么问题的,但是,在上述判断之前空间,和赋值新的空间的操作都是多余做的了,所以我们可以 在函数的开口就判断,是不是自己给自己赋值的情况。

如果 有一个 sp3 委会一块空间,这块空间没有被其他指针来维护的话, 假设现在有 sp3 = sp3 这样的赋值的话,如果没有特殊判断来终止赋值的话,sp3 维护的资源 就会在 operator=() 当中 被释放掉,然后在把 sp3 赋值给 sp3 的话,就是一个已经被释放了的空间地址,当我们再次使用这个 sp3 的时候就会出问题。

可以利用 资源来判断是不是 同一块空间,也可以用 计数来判定也是可以的:

// 判断当前对象当中的原生指针指向的空间是否和 
// 当前要赋值的指针指向的空间是相同的
if(_ptr == sp._ptr_
    return *this;

 shared_ptr 完整代码实现:

template<class T>
	class shared_ptr
	{
	public:
		// RAII
		// 像指针一样
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// sp3(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}

		// sp1 = sp5
		// sp6 = sp6
		// sp4 = sp5
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
            // 先判断 赋值和被赋值的两个指针是否是重复的
            // 是就直接返回
			if (_ptr == sp._ptr)
				return *this;
            
            // 判断当前 赋值指针在赋值出去之前
            // 是否是所维护空间的唯一指针
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
            
            // 开始赋值
			_ptr = sp._ptr;
			_pcount = sp._pcount;
            
            // 引用计数++
			++(*_pcount);

			return *this;
		}

        // 返回引用计数
		int use_count() const
		{
			return *_pcount;
		}
        
        // 拿到原生指针
		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

循环引用问题(用 weak_ptr 指针)

 shared_ptr 指针几乎没缺点,但是也不是意味着 完全没有缺点的,如下所示:
 

struct Node
{
	A _val;
	Node* _next;
	Node* _prev;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;

	return 0;
}

向上述的结点的指针链接 是非常 常规的操作,但是上述的操作就有问题,虽然上述能够自动释放空间,但是 以为 _next  和 _prev 两个指针的类型但是 Node*,不是 shared_ptr 智能指针类型的,所以这里是经典的类型不匹配。

有人就想到,把 _next  和 _prev 的指针类型改为 shared_ptr<Node> 的不就行了吗?确实可以:

struct Node
{
	A _val;
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
};

但是,当我们把调用的函数都输出一遍,输出:

 只调用了 构造函数,但是 析构函数是没有 调用的,也就是说此时发生了 内存泄漏,而且,我们惊讶的发现,当只调用     sp1->_next = sp2; 的时候,就没有问题,正常释放两个空间当中的内容:
 

A(int a = 1)
A(int a = 1)
~A()
~A()

 所以,问题就出在     sp1->_next = sp2;     sp2->_prev = sp1; 这两句当中当中和,我们先把两个结点 链接关系画一下:

如上述所示,因为在 sp1._next 和 sp2._prev 各自都链接上了   对方的结点空间,所以 ,两个结点空间的 引用计数 静态变量 就会 ++ ,现在两个空间的 静态空间变量都是 2 了。

当主函数当中执行完毕之后,因为 sp1 比 sp2 先声明,所以 sp2 要先析构;sp2 析构,sp2 空间上的 引用计数就 -- 到1;然后 sp1 析构,也是一样的过程;sp2 和 sp1 析构之后如下所示:

 此时,就出现了 循环引用 的场景 ,_prev 是在 sp2 这个空间当中的 ,_next 是在 sp1 这个空间当中的,那么此时 不管谁先释放了,谁都不愿先调用自己的析构函数,因为:

  • 如果想要析构 _prev ,那么就要析构 第二个结点空间 的时候才会析构到 _prev ;
  • 但是,要想析构到 第二个结点空间,就得先析构 _next 所在的第一个结点空间,而第一个 结点所在空间要析构,又要 _prev 先析构,这不就死锁了吗?

 两个都不能先析构,那么就不能析构了。

 循环引用的场景介绍:

在外部有两个 智能指针,维护这两个不同的空间;在这两个空间当中,又各自有一个指针,你的指针管理着我的空间,我的指针管理着你的空间。


所以,为了解决循环引用问题,专门写了一个指针叫做 weak_ptr

weak_ptr 不是智能指针,而是 为了专门解决 循环引用问题,而专门写出来的一个 指针。

 所以我们发现,在官方库当中  weak_ptr 都没有 指针类型传参的构造函数:
 

 weak_ptr 实现,就是不使用 引用计数,不参与资源释放的管理,但是可以访问其中的资源。

 如果上述的 sp1 和 sp2 当中的空间都没有使用引用计数来管理的话,当 第一步 sp2 和 sp1 释放的时候,就会直接把这两个空间给释放了。就没有有后续 _next 和 _prev 两个指针的事情了。

 所以,weak_ptr 只是拿到智能指针当中的 指针,可以访问这个原生指针,但是对于这个指针维护的空间,和引用计数, 我 weak_ptr 是完全不管的,我值管访问,对于空间的维护是其他 智能指针管理的。

而且,虽然 weak_ptr 不支持用原生指针来构造对象,但是支持用 shared_ptr 智能指针来构造对象,所以,我们可像下面一样写:

struct Node
{
	A _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;

	return 0;
}

输出:

A(int a = 1)
A(int a = 1)
~A()
~A()

 

 weak_ptr 完整代码:

template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
        
        // 下述的拷贝构造函数 和 operator=()函数
        // 都不管其中的 空间释放 引用计数等等
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
        
        // 下述是指针操作
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
        // 不使用引用计数
	};
}

 boost库简介

 Boost库_百度百科 (baidu.com)

他其实是在 C++11 出来之前,由 C++委员会当中的一些成员,创建了 Boost 库社区,在这个社区当中探索出了很多 有用的语法,比如:右值引用,等等。

在 boost 库当中也诞生了 更好的 智能指针:

 C++11 当中相当于是沿用了 boost 库当中的一些智能指针,进行了一些细节上的修改,基本上属于是 cv了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1081561.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【合集】Java进阶——Java深入学习的笔记汇总 JVM底层、多线程、类加载 ...

前言 spring作为主流的 Java Web 开发的开源框架&#xff0c;是Java 世界最为成功的框架&#xff0c;持续不断深入认识spring框架是Java程序员不变的追求&#xff1b;而spring的底层其实就是Java&#xff0c;因此&#xff0c;深入学习Spring和深入学习Java是硬币的正反面&…

[代码随想录]二叉树篇

文章目录 1. 二叉树之层序遍历1.1 144-二叉树的前序遍历1.2 94-二叉树的中序遍历1.3 145-二叉树的后序遍历1.4 102-二叉树的层序遍历1.5 107-二叉树的层序遍历II1.6 199-二叉树的右视图1.7* 637-二叉树的层平均值1.8* 429-N叉树的层序遍历1.9 515-在每个树行中找最大值1.10* 11…

【算法挨揍日记】day14——724. 寻找数组的中心下标、238. 除自身以外数组的乘积

724. 寻找数组的中心下标 724. 寻找数组的中心下标 题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;请计算数组的 中心下标 。 数组 中心下标 是数组的一个下标&#xff0c;其左侧所有元素相加的和等于右侧所有元素相加的和。 如果中心下标位于数组最左端&#…

客户成功体系如何构建?请看这7步

⭐简单说两句⭐ 作者&#xff1a;后端小知识 CSDN个人主页&#xff1a;后端小知识 &#x1f50e;GZH&#xff1a;后端小知识 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 客户成功体系如何构建&#xff1f;请看这7步 在中国企业服务领域的…

js获取当前月第一天最后一天

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 目录 …

C++ DAY 5

#include <iostream>using namespace std;class Sofa { private:string sit; public:Sofa(string s "-") :sit(s){cout << "sofa 构造函数" << endl;}void show (){cout << sit << endl;} }; class Bed { private:string sl…

Spring框架是什么Spring框架的体系结构

Spring框架是什么 Spring是为企业Java最流行的应用程序开发框架。数以百万计的世界各地的开发人员使用Spring框架来创建高性能&#xff0c;易于测试的&#xff0c;可重用的代码。 Spring框架是一个开源的Java平台&#xff0c;它最初是由Rod Johnson编写并在2003年6月在Apache2…

Logo设计教程:从入门到精通的全程指导

如果你想制作一个专业的Logo标识&#xff0c;但是又缺乏设计技能&#xff0c;那么乔拓云可以帮助你轻松完成这个任务。以下是通过乔拓云制作Logo标识的简单步骤&#xff1a; 1. 注册并登录乔拓云账号 访问乔拓云官网&#xff0c;注册并登录你的账号。登录后&#xff0c;你将进…

麒麟系统加密/麒麟系统防泄密

​深信达网络科技有限公司自主研发的深信达主机加固系统软件V2.0、深信达沙盒防泄密系统软件V5.0&#xff0c;与麒麟软件完成兼容认证&#xff0c;并被纳入麒麟软件安全生态联盟成员之一。 麒麟软件主要面向通用和专用领域打造安全创新操作系统产品和相应解决方案&#xff0c;以…

Linux 中如何安全地抹去磁盘数据?

哈喽大家好&#xff0c;我是咸鱼 离过职的小伙伴都知道&#xff0c;离职的时候需要上交公司电脑&#xff0c;但是电脑里面有许多我们的个人信息&#xff08;聊天记录、浏览记录等等&#xff09; 所以我们就需要先把这些信息都删除&#xff0c;确保无法恢复之后才上交 即有些…

Etsy店铺爆单的7个技巧

2023年跨境电商行业趋势愈发旺盛&#xff0c;目前正处于红利期&#xff0c;而作为近年来的电商网红“Etsy”&#xff0c;以其低成本低竞争高回报的优势吸引了大批的跨境电商玩家。但仅仅入驻照搬其他平台经验很难出单&#xff0c;如果你正烦恼这个问题&#xff0c;那么接下来的…

人工智能在教育上的应用2-基于大模型的未来数学教育的情况与实际应用

大家好&#xff0c;我是微学AI ,今天给大家介绍一下人工智能在教育上的应用2-基于大模型的未来数学教育的情况与实际应用&#xff0c;随着人工智能(AI)和深度学习技术的发展&#xff0c;大模型已经开始渗透到各个领域&#xff0c;包括数学教育。本文将详细介绍基于大模型在数学…

User Account Status 在CDB 和PDB不一致的情况 OPEN IN ROLLOVER

PDB&#xff1a; OPEN & IN ROLLOVER CDB&#xff1a; OPEN 解决办法 alter user C## expire password rollover period; select pdb.NAME, pdb.CREATION_TIME pdbcreated,du.username,du.account_status,du.LOCK_DATE,du.EXPIRY_DATE,du.CREATED,du.LAST_LOGIN,du.pro…

第十二章-系统调用

Ⅰ.Linux系统调用原理 Linux系统调用都通过中断号int 0x80完成&#xff0c;不同的系统调用函数对应eax中不同的子功能号&#xff0c;因此系统调用包括int 0x80; mov %sub_num,%%eax两个部分。 Linux系统调用包括两种方式&#xff1a;1.宏调用_syscall。2.库函数调用syscall。…

拼多多怎么引流商家?建议收藏的几个方法,拼多多引流脚本详细使用教学分享

大家好我是你们的小编一辞脚本&#xff0c;今天给大家分享新的知识&#xff0c;很开心可以在CSDN平台分享知识给大家,很多伙伴看不到代码我先录制一下视频 在给大家做代码&#xff0c;几个关于商家引流的知识今天给大家分享一下拼多多引流脚本的知识和视频演示 不懂的小伙伴可…

【微服务治理】Spring Cloud 断路器Hystrix实战应用

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

es elasticsearch 基础

es https://www.elastic.co/guide/en/elasticsearch/reference/6.8/getting-started.html 倒排索引&#xff1a; 正排–>从目录到文章内容 倒排–>内容到目录文章标题 if we dont need full text search ,we dont need to create 倒排 using text type 分词&#xff1a…

优雅而高效的JavaScript——解构赋值

&#x1f643;博主&#xff1a;小猫娃来啦 &#x1f643;文章核心&#xff1a;优雅而高效的JavaScript——解构赋值 文章目录 什么是解构赋值数组解构赋值基本用法默认值剩余参数 对象解构赋值基本用法默认值剩余参数 解构赋值的优势和应用场景代码简化和可读性提高交换变量值函…

硬件基本功--过流、过压保护电路

1.简介 过流保护(OCP)&#xff1a;当电路电流超过预定最大值时&#xff0c;使保护装置动作的一种保护方式。不允许超过预定最大值电流&#xff0c;不然会烧坏电路的器件。过压保护(OVP)&#xff1a;被保护电路电压超过预定的最大值时&#xff0c;使电源断开或使受控设备电压降低…

企业如何实现财务无纸化?票档一体化建设势在必行

随着“大智移云物区”等信息技术的发展&#xff0c;传统的财务管理开始向信息化、数字化转型&#xff0c;电子发票、电子凭证也逐渐取代传统的纸质档案资料&#xff0c;促使企业转型升级并逐步实现财务无纸化&#xff0c;助力降本增效。 同时&#xff0c;在政策强力推动下&…