当前位置: 首页 > news >正文

C++11:智能指针

文章目录

  • 1. 介绍
    • 1.1 动态内存与智能指针
  • 2. 使用
    • 2.1 创建
    • 2.2 使用
  • 3. 原理
    • 3.1 RAII
    • 3.2 像指针一样使用
    • 3.3 支持智能指针对象拷贝
        • auto_ptr
          • RAII
  • 4. 标准库中的智能指针
    • 4.1 unique_ptr
      • 模拟实现
    • 4.2 shared_ptr
      • 引用计数
      • 模拟实现
      • 定制删除器
    • 4.3 weak_ptr
      • shared_ptr造成的循环引用问题
      • 与shared_ptr的关系
      • 模拟实现
  • 5. 常见问题

1. 介绍

到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期:

  • 全局对象:程序启动时分配,在程序结束时销毁。
  • 局部对象:当我们进入其定义所在的程序块时被创建,在离开块时销毁。
  • 局部static对象:在第一次使用前分配,在程序结束时销毁。

我们的程序到目前为止只使用过静态内存或栈内存:

  • 静态内存:保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存:保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

1.1 动态内存与智能指针

除了局部和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。动态对象的正确释放是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

在C++中,动态内存的管理是通过一对运算符来完成的:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题;

  • 内存泄漏:有时我们会忘记释放内存,在这种情况下就会产生内存泄漏
  • 非法指针:有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针

C++11为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。==智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。==新标准库提供的这两种智能指针的区别在于管理底层指针的方式:

  • shared_ptr允许多个指针指向同一个对象;
  • unique_ptr则“独占”所指向的对象。

标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在<memory>头文件中。

智能指针是一个类,它能够管理(真正的)指针引用的内存,同时它还能像指针一样被使用,就像仿函数(函数对象)能像函数一样被使用一样。

2. 使用

下面以shared_ptr为例。

2.1 创建

类似vector等容器,智能指针也是模板。因此当我们创建一个智能指针时,必须提供额外的信息:指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字。

shared_ptr<int> p1; // 指向int的智能指针
shared_ptr<string> p2; // 指向string的智能指针

2.2 使用

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。

int main()
{shared_ptr<int> p1(new int(1));cout << p1 << endl;cout << *p1 << endl;return 0;
}

输出:

0x6000031d0030
1

虽然智能指针是一个类,但是其中重载了操作指针的运算符,用起来就像指针一样。还可以像指针一样访问类中的成员:

class Person
{
public:Person(const string& name): _name(name){}const string& GetName(){return _name;}
private:string _name;
};
int main()
{shared_ptr<Person> p(new Person("小明"));cout << p->GetName() << endl;return 0;
}

输出:

小明

使用上非常简单,就像真正的指针一样,下面将讨论智能指针管理资源的原理。

3. 原理

智能指针的实现,必须解决下面三个问题:

  • RAII,将资源交给对象的生命周期管理,即构造对象时开始获取(管理)资源,析构对象时释放资源;
  • 像真正的指针一样使用;
  • 支持智能指针对象的拷贝。

其中最容易实现的是像指针一样使用,只要重载使用指针的运算符即可。其次是实现RAII,最后是智能指针对象的拷贝。

3.1 RAII

RAII(Resource Acquisition Is Initialization),资源获取即初始化,由Bjarne Stroustrup(C++之父)提出。是一种将资源的生命周期绑定到对象的生命周期的 C++ 编程技术,它有助于避免资源泄漏并简化错误处理。这是设计智能指针核心思想。

RAII在智能指针上的体现:智能指针在构造时获取一个原始指针,并在析构时释放它:

int main()
{shared_ptr<int> p(new int(1)); // 构造时获取内存cout << *p << endl; // 访问内存// 出了作用域,自动释放内存return 0;
}

RAII是一种利用对象生命周期来管理资源的技术,它的意义在于:

  • 避免资源泄漏,因为对象在析构时会自动释放所占用的资源。
  • 简化代码,因为不需要手动管理资源的分配和释放。
  • 增强异常安全性,因为即使发生异常,对象也会被正常析构。

RAII是C++中一种非常重要和实用的编程范式,它体现了C++的设计哲学:让编译器帮助我们做更多的事情。

3.2 像指针一样使用

下面将用一个自定义智能指针SmartPtr为例:

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr) // 构造时接管内存: _ptr(ptr){}~SmartPtr()      // 析构时释放内存{cout << "delete:" << _ptr << endl; // 提示语句delete _ptr;}// 重载操作符T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
int main()
{SmartPtr<int> p(new int(1));cout << *p << endl;return 0;
}

其中重载了*->运算符,使得使用这个类就像使用指针一样。智能指针是一个模板类,以能够管理任何类型的指针引用的内存,如果模板参数是一个有公有成员的类,那么还能使用->访问其成员。

当智能指针未初始化时,赋予nullptr缺省值。

3.3 支持智能指针对象拷贝

上面实现的智能指针SmartPtr是极不完善的,如果想实现拷贝构造和拷贝赋值:

int main()
{SmartPtr<int> p1(new int(1));SmartPtr<int> p2(p1); // 拷贝构造SmartPtr<int> p3(new int(2));SmartPtr<int> p4(new int(2));p4 = p3; // 拷贝赋值return 0;
}

输出:

delete:0x600003c84030
delete:0x600003c84030

错误(Clion):

malloc: *** error for object 0x600003c84030: pointer being freed was not allocated

造成程序崩溃的原因是在这个类中没有实现拷贝构造函数和拷贝赋值函数,而编译器默认生成的全都是对内置类型的浅拷贝(值拷贝):相当于p1和p2、p3和p4共同管理同一块空间。当出了p1的作用域后,调用析构函数,释放空间,p2再次调用析构函数时导致这块已经被释放的空间再次被释放。p3和p4同理。

要解决浅拷贝造成的二次析构问题,就要实现深拷贝的拷贝构造函数和拷贝赋值函数吗?

答案是否定的,智能指针的功能需求是模拟指针的使用,本质是帮指针托管资源,那么指针的拷贝或赋值操作就相当于两个指针指向同一块内存空间。资源管理权转移,通过不负责任的拷贝,会导致被拷贝对象悬空。虽然资源能得到释放,但是会造成垂悬指针。智能指针将内存资源的管理和对象的生命周期绑定在一起,如果只是像上面一样简单地满足RAII,那么一定会发生二次析构的问题,因为创建的智能指针对象一定会调用析构函数,且不论程序是否正常结束。

  • 程序正常结束:对象出了作用域调用析构函数;

  • 程序不正常结束:例如抛异常,跳转到catch块相当于跳转到另一个函数的栈帧中,也相当于出了作用域,依然调用析构函数。

下面以标准库中(C++98)智能指针auto_ptr为例。

auto_ptr

出现多次析构问题的本质是同一块内存空间被多个对象通过管理,如果将资源的管理权只交给一个对象,就不会出现多次析构问题。

int main()
{auto_ptr<int> p1(new int(1));auto_ptr<int> p2(p1);auto_ptr<int> p3(new int(2));auto_ptr<int> p4(new int(2));p3 = p4;return 0;
}

然而,将一个对象对资源的管理权转移后,就意味着这个对象再对资源访问是一个非法操作,程序会因此崩溃。如果让不熟悉auto_ptr原理的人使用,因为拷贝操作而造成非法指针或内存泄漏是有可能的,而这也是致命的错误,因此许多公司明文规定禁止auto_ptr的使用,进而用C++11的unique_ptr和shared_ptr取代。

通过模拟实现auto_ptr理解其原理,以更好地理解unique_ptr和shared_ptr。同样地,实现auto_ptr也需要满足三点,其中像指针一样使用仍然不变。

RAII
  • 析构函数:需要对它管理的指针判空,只有指针非空时才能对其进行释放资源操作,释放资源以后对其置空。

  • 拷贝构造函数:用传入对象管理的内存资源来构造当前对象,并将传入对象管理资源的指针置空。

  • 拷贝赋值函数:先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。

namespace xy
{template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}~auto_ptr(){if (_ptr != nullptr){cout << "delete:" << _ptr << endl;delete _ptr;_ptr = nullptr;}}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr; // 管理权转移后置空ap}auto_ptr& operator=(auto_ptr<T>& ap){if (this != &ap){delete _ptr;       // 释放自己管理的资源_ptr = ap._ptr;    // 接管ap对象的资源ap._ptr = nullptr; // 管理权转移后置空ap}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

测试:

class A
{
public:A(const int a = 0): _a(a){}~A(){}int _a;
};
int main()
{xy::auto_ptr<A> ap1(new A);ap1->_a++;cout << ap1->_a << endl;xy::auto_ptr<A> ap2(ap1); // ap2接管ap1的资源ap2->_a++;//cout << ap1->_a << endl; // errorcout << ap2->_a << endl;xy::auto_ptr<A> ap3(new A);ap3 = ap2; // ap3接管ap2的资源// cout << ap2->_a << endl; // errorap3->_a++;cout << ap3->_a << endl;return 0;
}

输出:

1
2
3
delete: 0x6000014a0030

对于auto_ptr,当一个智能指针接管另一个智能指针管理的资源后,原来的指针已经解除对资源的引用,是一个悬垂指针。如果对其进行任何访问操作,会造成程序崩溃,例如main()中被注释的语句。

下面介绍标准库中的两种智能指针,并通过模拟实现理解它们的原理。

4. 标准库中的智能指针

库中的智能指针并不是凭空出世的,在此之前,boost社区对auto_ptr进行改良,推出了几种智能指针。

Boost 社区是一个提供 C++ 库的开源项目,Boost 大概是最重要的第三方C++ 库。其作者有很多是C++ 标准委员会的成员。Boost 的很多子库后来都成为C++ 的标准库。它包括了很多高质量和实用的库,其中就有智能指针库。

Boost 的智能指针库提供了六种智能指针模板,分别是 scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr 和 intrusive_ptr。它们和 C++11 的智能指针有一些相似之处,也有一些不同之处。比如,Boost 的 shared_ptr 和 weak_ptr 可以用于数组,而 C++11 的不可以;而 C++11 的 unique_ptr 可以使用移动语义,而 Boost 的 scoped_ptr 不可以。Boost 社区对 C++ 标准的发展也有一定的影响,比如 C++11 的 shared_ptr 和 weak_ptr 就是基于 Boost 的实现。

4.1 unique_ptr

C++98的auto_ptr因为拷贝和赋值操作而造成内存泄漏和悬垂指针的问题而饱受诟病,C++11引入的unique_ptr则粗暴地砍掉了它的拷贝和赋值功能。这通过C++11引入的关键字delete的新功能实现。

在C++11之前,可以通过将构造函数和拷贝赋值函数私有声明实现。

模拟实现

模拟实现的过程:去除auto_ptr中拷贝和赋值的函数。

namespace xy
{template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}~unique_ptr(){if (_ptr != nullptr){cout << "delete:" << _ptr << endl;delete _ptr;_ptr = nullptr;}}unique_ptr(auto_ptr<T>& ap) = delete;unique_ptr& operator=(auto_ptr<T>& ap) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
int main()
{xy::unique_ptr<int> uqp1(new int);xy::unique_ptr<int> uqp2(new int);uqp2 = uqp1; // errorreturn 0;
}

对于以上赋值语句,输出(Clion):

malloc: *** error for object 0x6000019a4030: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debug
delete:0x6000019a4030
delete:0x6000019a4030

C++98中,delete的意思是不让编译器自动生成默认函数,而C++11为了实现这个智能指针,赋予delete一个新功能:不允许调用。

4.2 shared_ptr

shared_ptr就是支持正常拷贝的auto_ptr。shared_ptr和其他智能指针的主要区别是它支持共享所有权,也就是说,多个shared_ptr对象可以拥有同一个资源,而不会造成内存泄漏或悬空指针。其他智能指针,如unique_ptr或weak_ptr,只能有一个所有者或不能控制资源的生命周期。

shared_ptr通过一个指针保持对一个对象的共享所有权。多个shared_ptr对象可以拥有同一个对象。当以下情况之一发生时,对象被销毁并释放其内存:

  • 拥有该对象的最后一个shared_ptr被销毁;
  • 通过reset()函数将shared_ptr赋值为另一个指针。

引用计数

auto_ptr转移资源后造成内存泄漏和悬垂指针的主要原因就是每个auto_ptr智能指针对象管理的资源是各自独立的,非此即彼。shared_ptr共享同一个资源,内存资源只在最后一个智能指针解除引用时释放,这样就不会造成资源被单方面地接管造成的问题。

image-20230306145422175

引用计数使得一个空间可以被多个对象管理,当引用计数为0时,说明已经没有智能指针管理这块内存空间了,此时才能释放资源,弥补了auto_ptr的缺陷。要知道引用计数的值,只需要调用shared_ptr的成员函数use_count()即可。

int main()
{shared_ptr<int> sp1(new int(1));shared_ptr<int> sp2(sp1);*sp1 = 10; // 拷贝后旧指针管理的资源依然能访问cout << sp1.use_count() << endl;shared_ptr<int> sp3(new int(0));shared_ptr<int> sp4(new int(2));sp3 = sp4;cout << sp3.use_count() << endl; // 赋值后旧指针管理的资源依然能访问return 0;
}

输出:

2
2

模拟实现

  • 增加count成员变量,表示引用计数;

  • 构造函数:当获取到资源则设置count=1,表示当前只有一个智能指针对象管理此资源;

  • 拷贝构造函数:将传入的智能指针对象中的count++,表示新增了一个管理者;

  • 拷贝赋值函数:将本智能指针的count--,表示解除对当前资源的引用,然后再将传入的智能指针对象中的count++,表示管理新的资源;

  • 析构函数:count--,表示解除对当前管理资源的引用,如果count=0则释放资源;

  • 重载*->运算符,使shared_ptr对象具有指针一样的行为。

其中,operator=的重载需要注意两个问题:

  • 内存泄漏:赋值时要把自己的引用计数给对方,赋值代表对方要共同接管自己管理的资源,所以对方的引用计数也要-1;
  • 自我赋值:本质也会造成内存泄漏,自我赋值后资源的管理权并未发生变化,但是引用计数却+1了,到真正最后一个对象时,引用计数仍不为0(如果自我赋值1次,那就是1),造成资源不能释放,内存泄漏。
namespace xy
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){if (_ptr != nullptr){cout << "delete:" << _ptr << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;}}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(shared_ptr<T>& sp){if (_ptr != sp._ptr)       // 管理同一资源的智能指针赋值无意义{if (--(*_pcount) == 0) // 将管理的资源的引用计数-1{cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}_ptr = sp._ptr;       // 与传入的智能指针共享资源_pcount = sp._pcount; // 将自己的引用计数和传入的智能指针同步(*_pcount)++;         // 引用计数+1,表示自己是新增的管理者}return *this;}// 获取引用计数int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount; // 引用计数};
}
int main()
{xy::shared_ptr<int> sp1(new int(1));cout << sp1.use_count() << endl;xy::shared_ptr<int> sp2(sp1);cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;xy::shared_ptr<int> sp3(new int(0));cout << sp3.use_count() << endl;xy::shared_ptr<int> sp4(new int(2));sp3 = sp4;cout << sp3.use_count() << endl;cout << sp4.use_count() << endl;return 0;
}

输出:

1
2
2
1
delete:0x6000007f4050
2
2
delete:0x6000007f4070
delete:0x6000007f4030

注意:

shared_ptr中的引用计数是存放在堆区的,因为这样可以让所有指向同一个对象的shared_p。如果引用计数在栈区,那么当一个shared_ptr改变指向或者离开作用域时,就无法通知其他shared_ptr更新引用计数了。因此,引用计数也不能是静态成员,每个类型实例化的智能指针对象时共用静态成员,这会导致管理相同资源的对象和管理不同资源的对象共用同一个引用计数。

由于在堆区的引用计数和同一类型的智能指针是绑定在一起的,当智能指针释放资源时,也需要释放引用计数占用的内存。

定制删除器

实际上,不是所有的对象都是new出来的,也可能是new[],因此释放对象的资源也可能是delete[]。例如:

  • 当你想在释放对象时执行一些额外的操作,例如关闭文件、释放资源、记录日志等。
  • 当你想使用一个不同于delete的函数来销毁对象,例如free、fclose、Release等。
  • 当你想管理一个不是通过new分配的对象,例如一个栈上的对象或一个全局变量。
  • 当你想管理一个不是单个对象而是一个数组或容器的对象。

定制删除器可以让你更灵活地控制shared_ptr如何管理和释放它所指向的对象。

假设你想管理一个打开的文件,但是你不能使用delete来关闭它,而是使用fclose:

#include <iostream>
#include <memory>
#include <cstdio>int main()
{// 创建一个shared_ptr,管理一个打开的文件// 使用fclose作为定制删除器std::shared_ptr<FILE> file(fopen("test.txt", "w"), fclose);// 写入一些内容到文件fputs("Hello world", file.get());// 当file离开作用域时,会调用fclose来关闭文件
}

这样就可以避免使用delete来释放一个不是通过new分配的对象,从而导致危险行为。shared_ptr在实例化对象时,有两个参数:

template <class U, class D>
shared_ptr (U* p, D del);

其中:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

实际上,删除器就是一个被工具封装的动作,这个动作就是用特定的方式释放资源。

总的来说,当智能指针管理的资源不是通过new出来的时候,就需要用对象类型和定制删除器构造智能指针。

例如可以传入一个lambda表达式作为定制删除器:

int main()
{shared_ptr<int> sp1(new int[5], [](int* ptr){cout << "delete[]:" << ptr << endl;delete[] ptr;});shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){cout << "fclose: " << ptr << endl;fclose(ptr);});return 0;
}

lambda表达式在定制删除器中的应用,体现了lambda表达式的内联属性。

unique_ptr没办法用lambda表达式,因为它是一个对象,unique_ptr必须传模板参数(类型)。

库中的实现比较复杂,因为要实现模板以满足各种定制器的类型。但总而言之就是根据实际情况定制一个类按照特定方式释放资源,在创建对象的时候将这个特定的动作作为参数。

4.3 weak_ptr

shared_ptr造成的循环引用问题

shared_ptr解决了auto_ptr可能造成内存泄漏和悬垂指针的问题,但是在少数情况下,shared_ptr也会造成内存泄漏,即循环引用问题。

循环引用是指当一个对象或者一个单元格内的公式直接或间接地引用了自己或者另一个对象。这样会导致内存泄漏或者计算错误。也就是说,如果两个shared_ptr指针互相引用,那么它们的引用计数永远不会为零,也就无法释放内存。

例如:

struct Node
{std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;~Node(){cout << "~Node" << endl;}
};
int main()
{std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);// 循环引用n1->_next = n2;n2->_prev = n1;return 0;
}

为了share_ptr可以使用Node,所以Node的两个指针都是shared_ptr<Node>类型的。

其中,两个赋值语句造成了两个结点对象n1和n2循环引用:

image-20230306230632036

n1n2 两个 Node 对象通过 _next_prev 成员变量相互引用,导致它们的引用计数永远不为零,从而无法被销毁。

死循环:资源只有在引用计数为1时才能被销毁。左边资源只有当右边的_prev释放以后引用计数才为0,而右边资源只有当左边的_next释放以后引用计数才为0。

解决这个问题的一种方法是使用 std::weak_ptr 来代替其中任意一个方向上的 std::shared_ptr

std::weak_ptr 是一种智能指针,它用来解决 std::shared_ptr 循环引用的问题。它不会增加所指向对象的引用计数,因此不会影响对象的销毁。

对于上面的例子,造成问题的本质是引用计数永不为0,那么只要将其中一个智能指针改为weak_ptr即可:

struct Node
{std::shared_ptr<Node> _next;std::weak_ptr<Node> _prev; // 将_prev用weak_ptr管理~Node(){cout << "~Node" << endl;}
};

与shared_ptr的关系

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期。也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

也就是说,weak_ptr是为了弥补shared_ptr循环引用而生的,它没有RAII的特性,不直接管理资源,只是shared_ptr的跟班,这也是weak_ptr支持使用shared_ptr构造的原因。

在使用上,weak_ptr支持指针所有的操作。它不是一个功能型的智能指针,而是辅助型,它的使命是解决shared_ptr造成的循环引用问题。

shared_ptr 不同,weak_ptr 不能直接访问所指向的对象。要访问对象,需要先调用 lock() 方法将其转换为 shared_ptr。如果所指向的对象已经被销毁,则 lock() 方法返回空指针。

模拟实现

  • 构造函数:无参构造
  • 拷贝构造:支持参数是shared_ptr类型和本身类型构造,同时接管shared_ptr管理的资源,但不增加引用计数
  • 拷贝赋值:同上。智能指针一般有get()接口,所以返回指针时可以调用
  • 像指针一样

它是一个辅助型指针,在shared_ptr内部实现。

namespace xy
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

5. 常见问题

为什么需要智能指针?

智能指针是一种用来管理在堆上分配的内存的工具。它将普通的指针封装为一个栈对象,当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。智能指针还可以防止忘记调用delete释放内存和程序异常进入catch块忘记释放内存。

C++11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个对象共享。

什么是引用计数?

是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。

RAII是什么?

RAII是Resource Acquisition Is Initialization的简称,中文翻译为“资源获取即初始化”。它是C++语言中的一种管理资源、避免泄漏的良好方法。它的原理是在构造函数中申请分配资源,在析构函数中释放资源。

智能指针的发展历史?

C++98中产生了第一个智能指针auto_ptr,但它存在较多问题。C++boost给出了更加实用的scoped_ptr和shared_ptr和weak_ptr。C++11标准引入了unique_ptr和shared_ptr和weak_ptr,其中unique_ptr对应的是boost中的scoped_ptr。

智能指针技术经过20多年的发展,特别是C++11标准引入shared_ptr和unique_ptr之后,趋于成熟。

C++有哪些智能指针?它们之前的区别和使用场景?

C++11中推出了三种智能指针,unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr置为废弃。

  • unique_ptr是独占资源所有权的指针,当我们独占资源的所有权的时候,可以使用unique_ptr对资源进行管理——离开unique_ptr对象的作用域时,会自动释放资源。这是很基本的RAII思想。

  • shared_ptr是共享资源所有权的指针。它使用引用计数来跟踪共享对象的引用数量。当引用计数变为0时,对象被销毁。

  • weak_ptr是共享资源的观察者,需要和shared_ptr一起使用,不影响资源的生命周期。它可以解决循环引用问题。

智能指针有一些自己的问题,比如循环引用、性能开销、处理数组、线程安全等,在使用智能指针前都需要详细了解。

模拟实现简易的智能指针,例如shared_ptr。

见4.2。

什么是循环引用,如何解决?原理?

循环引用是指两个或多个对象相互引用,导致它们之间形成一个循环。这会导致一些问题,比如在使用引用计数的垃圾回收机制中,循环引用的对象永远无法被释放。

解决循环引用的一种方法是使用弱引用,即不增加对象的引用计数,因此不会影响垃圾回收。例如,在C++中,可以使用weak_ptr来解决shared_ptr之间的循环引用问题。当判断是否为无用对象时仅考虑强引用计数是否为0,不关心弱引用计数的数量。

相关文章:

C++11:智能指针

文章目录1. 介绍1.1 动态内存与智能指针2. 使用2.1 创建2.2 使用3. 原理3.1 RAII3.2 像指针一样使用3.3 支持智能指针对象拷贝auto_ptrRAII4. 标准库中的智能指针4.1 unique_ptr模拟实现4.2 shared_ptr引用计数模拟实现定制删除器4.3 weak_ptrshared_ptr造成的循环引用问题与sh…...

ccc-pytorch-RNN(7)

文章目录一、RNN简介二、RNN关键结构三、RNN的训练方式四、时间序列预测五、梯度弥散和梯度爆炸问题一、RNN简介 RNN&#xff08;Recurrent Neural Network&#xff09;中文循环神经网络&#xff0c;用于处理序列数据。它与传统人工神经网络和卷积神经网络的输入和输出相互独立…...

docker安装(linux)

安装需要的软件包 yum install -y yum-utils 设置stable镜像仓库&#xff08;使用阿里云镜像&#xff09; yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 更新yum软件包索引 yum makecache fast 安装DOCKER 引擎 yum -y…...

【数据库概论】10.1 事务及其作用

事务是一系列的数据库操作&#xff0c;是数据库应用程序的基本逻辑单元 10.1 事务的基本概念 1.事务 事务是用户定义的一个数据库操作序列&#xff0c;是一个具有原子性的操作&#xff0c;不可再分&#xff0c;一个事务内的操作要么全做、要么都不做。一般来说&#xff0c;一…...

通讯录(C++实现)

系统需求通讯录是一个可以记录亲人、好友信息的工具。本章主要利用C来实现一个通讯录管理系统系统中需要实现的功能如下:添加联系人:向通讯录中添加新人&#xff0c;信息包括&#xff08;姓名、性别、年龄、联系电话、家庭住址&#xff09;最多记录1000人显示联系人:显示通讯录…...

轻松掌握C++的模板与类模板,将Tamplate广泛运用于我们的编程生活

C提高编程 本阶段主要针对C泛型编程和STL技术做详细讲解&#xff0c;探讨C更深层的使用 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。 模板 1.模板的概念 模板就是建立通用的模具&#xff0c;大大提高复用性 例如&#xff1a; 2.函数模板 C另一种编程思想称…...

pandas 数据预处理+数据概览 处理技巧整理(持续更新版)

这篇文章主要是整理下使用pandas的一些技巧&#xff0c;因为经常不用它&#xff0c;这些指令忘得真的很快。前段时间在数模美赛中已经栽过跟头了&#xff0c;不希望以后遇到相关问题的时候还去网上查&#xff08;主要是太杂了&#xff09;。可能读者跟我有一样的问题&#xff0…...

mmdetectionV2.x版本 训练自己的VOC数据集

mmdetection目录下创建data文件夹&#xff0c;路劲如图所示&#xff0c;不带yololabels 修改配置文件 mmdet/datasets/voc.py 配置图片格式 mmdet/datasets/xml_style.py 如果图片是jpg则改成jpg&#xff0c;是png格式就改成png&#xff0c;这里我不需要改&#xff0c;本…...

Shell - crontab 定时 git 拉取并执行 maven 打包

目录 一.引言 二.踩坑与实践 1.原始代码 2.mvn package 未执行与解决 [导入环境变量] 3.git pull 未执行与解决 [添加绝对路径] 三.总结 一.引言 git 任务部署在通道机&#xff0c;每天6点需要定时更新 jar 包并打包上线&#xff0c;所以需要在 linux 服务器上&#xff…...

408考研计算机之计算机组成与设计——知识点及其做题经验篇目3:指令的寻址方式

上篇文章我们讲到&#xff0c;指令的基本格式&#xff0c;一条指令通常包括操作码字段和地址码字段两部分&#xff1a; 操作码字段地址码字段并且我们还讲到根据操作数地址码的数目不同&#xff0c;可将指令分为零一二三四地址指令。感兴趣的小伙伴们可以看看小编的上一篇文章…...

前端包管理工具:npm,yarn、cnpm、npx、pnpm

包管理工具npm Node Package Manager&#xff0c;也就是Node包管理器&#xff1b; 但是目前已经不仅仅是Node包管理器了&#xff0c;在前端项目中我们也在使用它来管理依赖的包&#xff1b; 比如vue、vue-router、vuex、express、koa、react、react-dom、axios、babel、webpack…...

推荐系统 FM因式分解

reference&#xff1a;知乎 FM算法解析 LR算法没有二阶交叉 如果是id类特征&#xff0c;这里的x是0/1&#xff0c;raw的特征输入就是float&#xff0c;当然&#xff0c;在我的理解里&#xff0c;一般会把raw的特征进行分桶&#xff0c;还是映射到0/1特征&#xff0c;不然这个w…...

Maven基础入门

文章目录Maven简介Maven 工作模式1.仓库2.坐标Maven的基本使用1.常用命令2.生命周期依赖管理1.依赖配置2.依赖传递3.可选依赖4.排除依赖5.依赖范围IDEA配置MavenMaven简介 Apache Maven 是一个项目管理和构建工具&#xff0c;它基于项目对象模型(POM)的概念&#xff0c;通过一…...

传输层协议 TCP UDP

目录 协议前菜 端口号 ​编辑端口号范围划分 认识知名端口号(Well-Know Port Number) netstat pidof 传输层协议 UDP协议 UDP协议端格式 UDP的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议 TCP协议 TCP协议概念 TCP协议段格式 标志…...

一点就分享系列(实践篇6——上篇)【迟到补发】Yolo-High_level系列算法开源项目融入V8 旨在研究和兼容使用【持续更新】

一点就分享系列&#xff08;实践篇5-补更篇&#xff09;[迟到补发]—Yolo系列算法开源项目融入V8旨在研究和兼容使用[持续更新] 题外话 去年我一直复读机式强调High-level在工业界已经饱和的情况&#xff0c;目的是呼吁更多人看准自己&#xff0c;不管是数字孪生交叉领域&#…...

buu RSA 1 (Crypto 第一页)

题目描述&#xff1a; 两个文件&#xff0c;都用记事本打开&#xff0c;记住用记事本打开 pub.key: -----BEGIN PUBLIC KEY----- MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMAzLFxkrkcYL2wch21CM2kQVFpY97 /AvKr1rzQczdAgMBAAE -----END PUBLIC KEY-----flag.enc: A柪YJ^ 柛x秥?y…...

Python 二分查找:bisect库的使用

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…...

性能优化之HBase性能调优

HBase是Hadoop生态系统中的一个组件&#xff0c;是一个分布式、面向列存储的内存型开源数据库&#xff0c;可以支持数百万列&#xff08;MySQL4张表在HBase中对应1个表&#xff0c;4个列&#xff09;、超过10亿行的数据存储。可用作&#xff1a;冷热数据分离HBase适合作为冷数据…...

图像金字塔,原理、实现及应用

什么是图像金字塔 图像金字塔是对图像的一种多尺度表达&#xff0c;将各个尺度的图像按照分辨率从小到大&#xff0c;依次从上到下排列&#xff0c;就会形成类似金字塔的结构&#xff0c;因此称为图像金字塔。 常见的图像金字塔有两类&#xff0c;一种是高斯金字塔&#xff0…...

08-Oracle游标管理(定义,打开、获取数据及关闭游标)

目标 1.确定何时需要显示游标2.声明、打开和关闭显示游标3.从显示游标中提取数据4.了解与游标有关的属性5.使用游标FOR循环检索游标中的数据6.在游标FOR循环的子查询中声明游标7.评估使用逻辑运算符结合在一起的布尔条件游标 1、在使用一个PL/SQL块来执行DML语句或只返回一行结…...

Python判断字符串是否包含特定子串的7种方法

目录1、使用 in 和 not in2、使用 find 方法3、使用 index 方法4、使用 count 方法5、通过魔法方法6、借助 operator7、使用正则匹配转自&#xff1a;https://cloud.tencent.com/developer/article/1699719我们经常会遇这样一个需求&#xff1a;判断字符串中是否包含某个关键词…...

aop实现接口访问频率限制

引言 项目开发中我们有时会用到一些第三方付费的接口&#xff0c;这些接口的每次调用都会产生一些费用&#xff0c;有时会有别有用心之人恶意调用我们的接口&#xff0c;造成经济损失&#xff1b;或者有时需要对一些执行时间比较长的的接口进行频率限制&#xff0c;这里我就简…...

Hive---窗口函数

Hive窗口函数 其他函数: Hive—Hive函数 文章目录Hive窗口函数开窗数据准备建表导入数据聚合函数window子句LAG(col,n,default_val) 往前第 n 行数据LEAD(col,n, default_val) 往后第 n 行数据ROW_NUMBER() 会根据顺序计算RANK() 排序相同时会重复&#xff0c;总数不会变DENSE…...

JavaSe第7次笔记

1. C语言里面&#xff0c;NULL是0地址。Java中null和0地址没关系。 2.数组可以做方法的返回值。 3.可以使用变量作为数组的个数开辟空间。 4.断言assert&#xff0c;需要设置。 5.排序&#xff1a;Arrays. sort(array); 6.查找&#xff1a; int index Arrays. binarySea…...

什么是 Service 以及描述下它的生命周期。Service 有哪些启动方法,有 什么区别,怎样停用 Service?

在 Service 的生命周期中,被回调的方法比 Activity 少一些,只有 onCreate, onStart, onDestroy, onBind 和 onUnbind。 通常有两种方式启动一个 Service,他们对 Service 生命周期的影响是不一样的。 1. 通过 startService Service 会经历 onCreate 到 onStart,然后处于运行…...

Redis部署

JAVA安装 mkdir /usr/local/javacd /usr/local/java/wget --no-check-certificate --no-cookies --header "Cookie: oraclelicenseaccept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u13…...

AT32F437制作Bootloader然后实现Http OTA升级

首先创建一个AT32F437的工程&#xff0c;然后发现调试工程配置这里的型号和创建工程选的型号不一致&#xff0c;手动更改一下&#xff0c;使用PW Link下载程序的话还要配置一下pyocd.exe的路径。 打开drv_clk.c文件的调试功能看下系统时钟频率。 项目使用的是AT32F437VMT7芯片&…...

Springboot项目启动初始化数据缓存

1.从Java EE5规范开始&#xff0c;Servlet中增加了两个影响Servlet生命周期的注解&#xff0c; PostConstruct和PreDestroy&#xff0c;这两个注解被用来修饰一个非静态的void&#xff08;&#xff09;方法&#xff0c;被PostConstruct修饰的方法会在服务器加载Servlet的时候运…...

深度学习必备知识——模型数据集Yolo与Voc格式文件相互转化

在深度学习中&#xff0c;第一步要做的往往就是处理数据集,尤其是学习百度飞桨PaddlePaddle的小伙伴&#xff0c;数据集经常要用Voc格式的&#xff0c;比如性能突出的ppyolo等模型。所以学会数据集转化的本领是十分必要的。这篇博客就带你一起进行Yolo与Voc格式的相互转化&…...

数据、数据资源及数据资产管理的区别

整理不易&#xff0c;转发请注明出处&#xff0c;请勿直接剽窃&#xff01; 点赞、关注、不迷路&#xff01; 摘要&#xff1a;数据、数据资源、数据资产 数据、数据资源及数据资产的区别 举例 CRM系统建设完成后会有很多数据&#xff0c;这些数据就是原始数据&#xff0c;业务…...

标度不变性(scale invariance)与无标度(scale-free)概念辨析

文章目录标度标度种类名义标度序级标度等距标度比率标度常用标度方法不足标度不变性标度不变&#xff08;Scale-invariant&#xff09;曲线和自相似性&#xff08;self-similarity&#xff09;射影几何分形随机过程中的标度不变性标度不变的 Tweedie distribution普适性&#x…...

WMS仓库管理系统解决方案,实现仓库管理一体化

仓库是企业的核心环节&#xff0c;若没有对库存的合理控制和送货&#xff0c;将会造成成本的上升&#xff0c;服务品质的难以得到保证&#xff0c;进而降低企业的竞争能力。WMS仓库管理系统包括基本信息&#xff0c;标签&#xff0c;入库&#xff0c;上架&#xff0c;领料&…...

css常见定位、居中方案_css定位居中

一、 定位分类 1、静态定位 position:static;&#xff08;默认&#xff0c;具备标准流条件&#xff09; 2、相对定位 position:relative; 通过 top 或者 bottom 来设置 Y 轴位置 通过 left 或者 right 来设置 X 轴位置 特点&#xff1a; 相对定位不会脱离文档流相对于自…...

【微信小程序】-- 自定义组件 -- 创建与引用 样式(三十二)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…...

ArangoDB——AQL编辑器

AQL 编辑器 ArangoDB 的查询语言称为 AQL。AQL与关系数据库管理系统 (RDBMS)区别在于其更像一种编程语言&#xff0c;更自然地适合无模式模型&#xff0c;并使查询语言非常强大&#xff0c;同时保持易于读写。数据建模概念 数据库是集合的集合。集合存储记录&#xff0c;称为文…...

Lesson 9.1 集成学习的三大关键领域、Bagging 方法的基本思想和 RandomForestRegressor 的实现

文章目录一、 集成学习的三大关键领域二、Bagging 方法的基本思想三、RandomForestRegressor 的实现在开始学习之前&#xff0c;先导入我们需要的库&#xff0c;并查看库的版本。 import numpy as np import pandas as pd import sklearn import matplotlib as mlp import sea…...

basic1.0链码部署(基于test-network 环境ubuntu20.04腾讯云)

解决了官方示例指令需要科学上网才能运行的问题&#xff08;通过手动下载二进制文件和拉取官方fabric-samples&#xff09;。具体的将bootstrap.sh脚本解读了一遍 具体可以参照我的博客 fabric中bootstrap.sh到底帮助我们干了什么&#xff1f;&#xff08;curl -sSL https://bi…...

Android---系统启动流程

目录 Android 系统启动流程 init 进程分析 init.rc 解析 Zygote 概叙 Zygote 触发过程 Zygote 启动过程 什么时Runtime&#xff1f; System Server 启动流程 Fork 函数 总结 面试题 Android 是 google 公司开发的一款基于 Linux 的开源操作系统。 Android 系统启动…...

【网络】http协议

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【网络】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文章…...

Thread::interrupted() 什么意思? 如何中断线程?

1、答&#xff1a; Thread::interrupted() 是一个静态方法&#xff0c;用于判断当前线程是否被中断&#xff0c;并清除中断标志位。 具体来说&#xff0c;当一个线程被中断后&#xff0c;它的中断状态将被设置为 true。如果在接下来的某个时间点内调用了该线程的 interrupted…...

Oracle OCP 19c 考试(1Z0-083)中关于Oracle不完全恢复的考点(文末附录像)

欢迎试看博主的专著《MySQL 8.0运维与优化》 下面是Oracle 19c OCP考试&#xff08;1Z0-083&#xff09;中关于Oracle不完全恢复的题目: A database is configured in ARCHIVELOG mode A full RMAN backup exists but no control file backup to trace has been taken A media…...

一起来学习配置Combo接口吧!

Combo接口是一个光电复用的逻辑接口&#xff0c;一个Combo接口对应设备面板上一个GE电接口和一个GE光接口。电接口与其对应的光接口是光电复用关系&#xff0c;两者不能同时工作&#xff08;当激活其中一个接口时&#xff0c;另一个接口就自动处于禁用状态&#xff09;&#xf…...

C++模拟实现红黑树

目录 介绍----什么是红黑树 甲鱼的臀部----规定 分析思考 绘图解析代码实现 节点部分 插入部分分步解析 ●父亲在祖父的左&#xff0c;叔叔在祖父的右&#xff1a; ●父亲在祖父的右&#xff0c;叔叔在祖父的左&#xff1a; 测试部分 整体代码 介绍----什么是红黑树 红…...

HTTPS协议之SSL/TLS详解(下)

目录 前言&#xff1a; SSL/TLS详解 HTTP协议传输安全性分析 对称加密 非对称加密 证书 小结&#xff1a; 前言&#xff1a; 在网络世界中&#xff0c;存在着运营商劫持和一些黑客的攻击。如果明文传输数据是很危险的操作&#xff0c;因为我们不清楚中间传输过程中就被哪…...

OLE对象是什么?为什么要在CAD图形中插入OLE对象?

OLE对象是什么&#xff1f;OLE对象的意思是指对象连接与嵌入。那为什么要在CAD图形中插入OLE对象&#xff1f;一般情况下&#xff0c;在CAD图形中插入OLE对象&#xff0c;是为了将不同应用程序的数据合并到一个文档中。本节内容小编就来给大家分享一下在CAD图形中插入OLE对象的…...

【微信小程序】-- 自定义组件 -- 数据、方法和属性(三十三)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…...

【Spring 深入学习】AOP的前世今生之代理模式

AOP的前世今生之代理模式1. 概述 什么是代理模式呢&#xff1f;&#xff1f;&#xff1f; 在不修改原有代码 或是 无法修改原有代码的情况下&#xff0c;增强对象功能&#xff0c;替代原来的对象去完成功能&#xff0c;从而达成了拓展的目的。 先给大家看下 JavaScript中实现方…...

操作系统复试

2017软学 给出操作系统的定义&#xff0c;分别从资源管理&#xff0c;任务调度&#xff0c;用户接口等三个方面论述操作系统的职能 操作系统是位于硬件层之上、所有其他系统软件层之下的一个系统软件&#xff0c;使得管理系统中的各种软件和硬件资源得以充分利用&#xff0c;方…...

藏经阁(五)温湿度传感器 SHT3x-DIS 手册 解析

文章目录芯片特性芯片内部框图芯片引脚定义芯片温湿度范围芯片寄存器以及时序讲解信号转换公式芯片特性 湿度和温度传感器完全校准&#xff0c;线性化温度补偿数字输出供电电压范围宽&#xff0c;从2.4 V到5.5 VI2C接口通讯速度可达1MHz和两个用户可选地址典型精度 2% RH和 0.…...

PCB焊盘设计基本原则

SMT的组装质量与PCB焊盘设计有直接的关系&#xff0c;焊盘的大小比例十分重要。如果PCB焊盘设计正确&#xff0c;贴装时少量的歪斜可以再次回流焊纠正(称为自定位或自校正效应)&#xff0c;相反&#xff0c;如果PCB焊盘设计不正确&#xff0c;即使贴装位置十分准确&#xff0c;…...