智能指针(Newbie Note)

智能指针专题

  • 1.普通指针的问题
  • 2.智能指针是什么
  • 3.智能指针三个好处:
  • 4.C++11提供的智能指针
    • [4.1 shared_ptr(共享所有权指针)](#4.1 shared_ptr(共享所有权指针))
      • [4.1.1 分配内存](#4.1.1 分配内存)
      • [4.1.2 成员函数](#4.1.2 成员函数)
      • [4.1.3 计数情况汇总:](#4.1.3 计数情况汇总:)
      • [4.1.4 示例代码(计数)](#4.1.4 示例代码(计数))
      • [4.1.5 示例代码(reset)](#4.1.5 示例代码(reset))
      • [4.1.6 自定义删除器](#4.1.6 自定义删除器)
      • [4.1.7 shared_ptr的陷阱](#4.1.7 shared_ptr的陷阱)
    • [4.2 unique_ptr(独占所有权指针)](#4.2 unique_ptr(独占所有权指针))
      • [4.2.1 分配内存](#4.2.1 分配内存)
      • [4.2.2 成员函数](#4.2.2 成员函数)
      • [4.2.3 拷贝赋值](#4.2.3 拷贝赋值)
      • [4.2.4 release](#4.2.4 release)
      • [4.2.5 reset](#4.2.5 reset)
      • [4.2.6 自定义删除器](#4.2.6 自定义删除器)
      • [4.2.7 unique_ptr的陷阱](#4.2.7 unique_ptr的陷阱)
  • 5.性能
  • 6.选择指针
    • [6.1 unique_ptr场景示例:](#6.1 unique_ptr场景示例:)
      • [6.1.1 作为参数](#6.1.1 作为参数)
      • [6.1.2 作为返回值](#6.1.2 作为返回值)
    • [6.2 shared_ptr场景示例:](#6.2 shared_ptr场景示例:)
        • [6.2.1 作为参数](#6.2.1 作为参数)
  • 参考资料

1.普通指针的问题

  • 1.访问失败:如果一块内存被多个指针引用,但其中的一个指针释放且其余的指针并不知道,这样的情况下,就发生了访问失败。

  • 2.内存泄漏:从堆中申请了内存后不释放回去,就会引发内存泄漏。

2.智能指针是什么

在构造的时候分配内存 ,当离开作用域的时候,自动释放分配的内存,这样的话开发人员就可以从手动动态管理内存的繁杂内容中解放出来。

每种智能指针都是以类模板的方式实现的,shared_ptr 也不例外。

什么是所有权

https://blog.csdn.net/weixin_39722329/article/details/96301534

  • 单一所有权:所有权拥有着有义务去释放或转移所有权,同一时刻只会有一个所有权拥有者

  • 共享所有权:和使用裸指针一样,所有权的使用者不必释放内存,引用计数器会负责释放内存,同一时刻可以有多个所有权拥有者。

3.智能指针三个好处:

  • 1.明确资源的所属权

  • 2.避免忘记delete,这种比较容易犯错误

  • 3.更好的处理异常

c++ 复制代码
//函数结束后shared_ptr自动释放内存
void f(){
    shared_ptr<int> sp(new int(11));
    //假设抛出了异常,而且在f中未捕获
}

//函数结束后ip所指向的内存没有被释放。
void f1(){
    int* ip = new int(12);
    //假设delete语句前抛出了异常,而且在f中未捕获
    delete ip;
}

重要性:1>>2>3

4.C++11提供的智能指针

4.1 shared_ptr(共享所有权指针)

共享指针shared_ptr是具有共享所有权语义的智能指针。 每当共享指针shared_ptr的最后一个所有者被销毁时,关联对象都将被删除(或关联资源被清除)。

4.1.1 分配内存

std::make_shared

c++ 复制代码
// make_shared<int>分配一块int类型大小的内存,并值初始化为100(返回值是shared_ptr类型,因此可以直接赋值给sp)
shared_ptr<int> sp = std::make_shared<int>(100);

new 接受指针参数的智能指针构造函数是explicit的,直接初始化形式

c++ 复制代码
// 错误! 不会进行隐式转换,类型不符合
shared_ptr<int> sp1 = new int(100);
// 正确,直接初始化调用构造函数
shared_ptr<int> sp2(new int(100000));

4.1.2 成员函数

  • p.get()
    p.get()的返回值就相当于一个裸指针的值,使用遵守以下几个约定
    1.不要保存p.get()的返回值
    2.无论是保存为裸指针还是shared_ptr都是错误的
    3.保存为裸指针不知什么时候就会变成空悬指针
    4.保存为shared_ptr则产生了独立指针
    5.不要delete p.get()的返回值
    6.会导致对一块内存delete两次的错误
  • swap(p,q)
    交换p、q中保存的指针
  • shared_ptr<T> p(q)
    p是q的拷贝,它们指向同一块内存,互相关联
  • p = q
    用q为p赋值,之后p、q指向同一块内存,q引用计数+1,p(原来内存空间的)引用计数-1
  • p.use_count()
    返回与p共享对象的智能指针数量
  • shared_ptr<T> p(q,d)
    q是一个可以转换为T*的指针,d是一个可调用对象(作为删除器),p接管q所指对象的所有权,用删除器d代替delete释放内存
  • p.reset()
    将p重置为空指针
  • p.reset(p)
    将p重置为p(的值)
  • p.reset(p,d)
    将p重置为p(的值)并使用d作为删除器
  • shared_ptr为什么没有release()
    对于shared_ptr,是可能存在多个shared_ptr指向同一块内存,如果提供了release可能会造成错误的释放,导致其他shared_ptr出现错误。

4.1.3 计数情况汇总:

赋值(增加)

auto sp = make_shared<int>(1024); // sp的引用计数为1
c 复制代码
#include <iostream>
#include <memory>

using namespace std;
// compile:g++ test.cpp -o a.exe -std=c++11

int main()
{
    {
        auto sp1 = make_shared<string>("obj1");
        auto sp2(sp1);
        auto sp3 = make_shared<string>("obj2");
        cout << "before sp2->use_count() = " << sp2.use_count() << '\n';
        cout << "before sp3->use_count() = " << sp3.use_count() << '\n';
        sp1 = sp3; // 该操作会减少sp2的引用计数,增加sp3的引用计数
        cout << "after sp2->use_count() = " << sp2.use_count() << '\n';
        cout << "after sp3->use_count() = " << sp3.use_count() << '\n';
    }
    return 0;
}

该操作会减少sp2的引用计数,增加sp3的引用计数。(sp1、sp2指向对象obj1,sp3指向对象obj2,那么赋值之后,sp1也会指向obj2,那就是说指向obj1的就少了,指向obj2的就会多。)

拷贝(增加)

auto sp2 = make_shared<int>(1024);
auto sp1(sp2);

该操作会使得sp1和sp2都指向同一个对象。

传参(拷贝)(增加)

而关于拷贝比较容易忽略的就是作为参数传入函数:

auto sp2 = make_shared<int>(1024);
func(sp2); // func的执行会增加其引用计数

reset(减少)

释放sp指向的对象( 而如果sp是唯一指向该对象的,则该对象被销毁 )

sp.reset()

4.1.4 示例代码(计数)

参考:https://en.cppreference.com/w/cpp/memory/shared_ptr

c++ 复制代码
#include <iostream>
#include <memory>
 
// g++ test.cpp -o a.exe -std=c++11 

class Base
{
public:
    Base() { std::cout << "  Base::Base()\n"; }
    ~Base() { std::cout << "  Base::~Base()\n"; }
};
 
class Derived: public Base
{
public:
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};
 
void test(std::shared_ptr<Base> p)增加引用计数
{   
    std::cout << "local pointer in a function:\n"
                  << "  p.get() = " << p.get()
                  << ", p.use_count() = " << p.use_count() << '\n';
    
    std::shared_ptr<Base> lp = p; 
                                  
    std::cout << "local pointer in a function:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
}//销毁ptr,减少引用计数
 
int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();
 
    std::cout << "func before:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';

    test(p);

    std::cout << "func after:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
}

运行结果

shell 复制代码
Base::Base()
Derived::Derived()
func before:
  p.get() = 0x1028c30, p.use_count() = 1
local pointer in a function:
  p.get() = 0x1028c30, p.use_count() = 2
local pointer in a function:
  lp.get() = 0x1028c30, lp.use_count() = 3
func after:
  p.get() = 0x1028c30, p.use_count() = 1
Derived::~Derived()
Base::~Base()

4.1.5 示例代码(reset)

重置共享指针,减少计数

c++ 复制代码
#include<memory>
#include<iostream>
// 编译: g++ test.cpp -o a.exe -std=c++11

using namespace std;
class A {
public:
	int i ;
	A() { cout << "construct\n"; }
	~A() { cout << "delete "<< i <<"\n"; }
};
int main()
{
	// 共享指针a,b,c都指向堆内存new A的位置
	shared_ptr<A> a( new A);
	shared_ptr<A> b(a);
	shared_ptr<A> c(b);
	shared_ptr<A> d(new A);
	a->i = 10;
	cout << a.use_count() << endl;
	cout << b.use_count() << endl;
	d->i = 30;

    // 错:不要用p.get()的返回值为shared_ptr赋值,因为返回的是裸指针,很容易被共享指针重复释放,造成错误
    //a.reset(d.get())
    
	// 令a释放指向的空间 A
	//a.reset();
	
	// 令a释放指向的空间 A,指向新空间
	a.reset(new A);
    	a->i = 20;
	
	cout << b.use_count() << endl;
	cout << "end" <<endl;
	return 0;
}

4.1.6 自定义删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器,原理是:当删除器的指针Deleter传给shared_ptr/unique_ptr时,shared_ptr/unique_ptr不会使用默认的delete val来释放其管理的资源,而是使用Deleter(val)来释放资源,这样就调用了Deleter来释放管理的资源。

1.普通删除函数定义类似于:

c 复制代码
void Deleter(T *val){
    // 其他代码
    // 释放val的内存
    delete val;
    // 或者(如果val是数组)
    delete[] val;
}
c++ 复制代码
#include <iostream>
#include <memory>
#include <string>
// 编译: g++ test.cpp -o a.exe -std=c++11
using namespace std;

class Connection{
public:
    string _name;

    explicit Connection(string name):_name(name){
    }
    
    string get_name() const {
        return _name;
    }
};

void close(Connection* connection){
    cout << string("关闭") + connection->get_name() + "管理的连接中..." << endl;
    // 关闭连接的代码
    // .....
    cout << "关闭完成。" << endl;
}

// 函数式删除器
void Deleter(Connection *connection){
    close(connection);
    delete connection;
}

int main(){
    // 新建管理连接Connection的智能指针
    shared_ptr<Connection> sp(new Connection("shared_ptr"), Deleter);
    sp->_name = "hello";
}
  1. 自定义释放规则,用于申请的动态数组

对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。释放规则可以使用 C++11 标准中提供的 default_delete 模板类,我们也可以自定义释放规则:

c++ 复制代码
//1.指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());

//2.自定义释放规则
void deleteInt(int*p) {
    delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

//3.lambda方式构造和释放
std::shared_ptr<int> p7(new int[10], [](int* p) {delete[]p; });  

4.1.7 shared_ptr的陷阱

1.不要与裸指针混用

错误场景1:

c++ 复制代码
int *x(new int(10));
shared_ptr<int> sp1(x);
shared_ptr<int> sp2(x);

//x随时可能变成空悬指针而无从知晓

错误场景2:

c++ 复制代码
int *x(new int(10));
//创建了一个指向x指针所指内存的共享指针,引用计数为1,是引用这块内存的唯一共享指针

func(shared_ptr<int> (x));
//离开函数即离开共享指针的作用域,这块内存即被删除

2.谨慎使用p.get()的返回值

c++ 复制代码
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1), sp3;
sp3 = sp1;
    
//一个典型的错误用法
shared_ptr<int> sp4(sp1.get()); 
cout << sp1.use_count() << " " 
     << sp2.use_count() << " " 
     << sp3.use_count() << " " 
     << sp4.use_count() << endl;

//输出:
3  
3 
3  
1(独立)

sp1,sp2,sp3是相互关联的共享指针,共同控制所指内存的生存期,sp4虽然指向同样的内存,却是与sp1,sp2,sp3独立的,sp4按自己的引用计数来关联内存的释放。

4.2 unique_ptr(独占所有权指针)

两个unique_ptr不能指向同一个对象,不能进行复制操作,只能进行移动操作。

4.2.1 分配内存

​ 与shared_ptr不同,unique_ptr没有定义类似make_shared的操作,因此只可以使用new来分配内存,并且由于unique_ptr不可拷贝和赋值,初始化unique_ptr必须使用直接初始化的方式。

c++ 复制代码
unique_ptr<int> up1(new int());    // okay:直接初始化

std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(std::move(p4)); // okay:调用移动构造函数,p5 将获取 p4 所指堆空间的所有权,而 p4 将变成空指针(nullptr)

unique_ptr<int> up2 = new int();   // error! 避免隐式转换
unique_ptr<int> up3(up1);          // error! 不允许拷贝

4.2.2 成员函数

  • up.release()

    ​ up放弃对它所指对象的控制权,并返回保存的指针,将up置为空,不会释放内存

  • up.reset()

    参数可以为空,内置指针,先将up所指对象释放,然后重置up的值

4.2.3 拷贝赋值

前面说了unique_ptr不可拷贝和赋值,那要怎样传递unique_ptr参数和返回unique_ptr呢? 事实上不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr

c++ 复制代码
// 从函数返回一个unique_ptr
unique_ptr<int> func1(int a)
{
    return unique_ptr<int> (new int(a));
}
 
// 返回一个局部对象的拷贝
unique_ptr<int> func2(int a)
{
    unique_ptr<int> up(new int(a));
    return up;
}

或者是引用

c++ 复制代码
void func1(unique_ptr<int> &up){
    cout<<*up<<endl;
}

unique_ptr<int> func2(unique_ptr<int> up){
    cout<<*up<<endl;
    return up;
}
 
// 使用up作为参数
unique_ptr<int> up(new int(10));
 
// 传引用,不拷贝,不涉及所有权的转移
func1(up);

// 暂时转移所有权,函数结束时返回拷贝,重新收回所有权
up = func2(unique_ptr<int> (up.release()));
// 如果不用up重新接受func2的返回值,这块内存就泄漏了

4.2.4 release

释放方法:注意!注意!注意!这里的释放并不会摧毁其指向的对象,而且将其指向的对象释放出去。

c++ 复制代码
#include <iostream>
#include <memory>
 
//编译:g++ test.cpp -o a.exe -std=c++11

int main () {
  std::unique_ptr<int> auto_pointer(new int);
  int * manual_pointer;
  *auto_pointer=10;
  manual_pointer = auto_pointer.release();
  // (auto_pointer is now empty)
  std::cout << "manual_pointer points to " << *manual_pointer << '\n';
  delete manual_pointer;
  return 0;
}

执行结果为:

c++ 复制代码
manual_pointer points to 10

4.2.5 reset

重置方法,销毁由该智能指针管理的任何可能存在的对象,该智能指针被指为空

c++ 复制代码
#include <iostream>
#include <memory>
 
//编译:g++ test.cpp -o a.exe -std=c++11

int main () {
  std::unique_ptr<int> up;  // empty
  up.reset (new int);       // takes ownership of pointer
  *up=5;
  std::cout << *up << '\n';
  up.reset (new int);       // deletes managed object, acquires new pointer
  *up=10;
  std::cout << *up << '\n';
  up.reset();               // deletes managed object
  return 0;
}

执行结果:

5
10

4.2.6 自定义删除器

c++ 复制代码
#include <iostream>
#include <memory>
#include <string>
// 编译: g++ test.cpp -o a.exe -std=c++11
using namespace std;

class Connection{
public:
    string _name;

    explicit Connection(string name):_name(name){
    }
    
    string get_name() const {
        return _name;
    }
};

void close(Connection* connection){
    cout << string("关闭") + connection->get_name() + "管理的连接中..." << endl;
    // 关闭连接的代码
    // .....
    cout << "关闭完成。" << endl;
}

// 函数式删除器
void Deleter(Connection *connection){
    close(connection);
    delete connection;
}

int main(){
    // 新建管理连接Connection的智能指针
    unique_ptr<Connection, decltype(Deleter)*> up(new Connection("unique_ptr"), Deleter);
    up->_name = "hello";
}

4.2.7 unique_ptr的陷阱

不要与裸指针混用

错误做法

c++ 复制代码
int *x(new int());
unique_ptr<int> up1,up2;
// 会使up1 up2指向同一个内存
up1.reset(x);
up2.reset(x);

unique_ptr不允许两个独占指针指向同一个对象,在没有裸指针的情况下,我们只能用release获取内存的地址,同时放弃对对象的所有权,这样就有效避免了多个独占指针同时指向一个对象。
正确做法

c++ 复制代码
unique_ptr<int> up1(new int());    // okay,直接初始化
unique_ptr<int> up2;
up2.reset(up1.release());

5.性能

  1. 内存占用高
    shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
    因此相比于 unique_ptr, shared_ptr 的内存占用更高
  2. 原子操作性能低
    考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
  3. 使用移动优化性能
    shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。
    如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

6.选择指针

不是说任何地方都要使用智能指针,比如说你想传递一个对象到一个函数里,那你就可以使用引用或者普通指针(raw ptr), 这里的引用和普通指针体现的是没有所属权(ownership),也就是说函数本身不负责这个对象的生命周期。只有需要体现所属权(ownership)的创立和变动的时候,采用智能指针。 参考

选择条件:
在使用智能指针的时候,优先选用unique_ptr,原因如下:

1.语义简单,即当你不确定使用的指针是不是被分享所有权的时候,默认选unique_ptr独占式所有权,当确定要被分享的时候可以转换成shared_ptr;

2.unique_ptr效率比shared_ptr高,不需要维护引用计数和背后的控制块;

3.unique_ptr用起来更顺畅,选择性更多,可以转换成shared_ptr和通过get和release定制化智能指针(custom smart pointer)。

如果有多个指针指向同一对象的话,你应该使用shared_ptr;

如果一个对象只需要一个智能指针,那你应该是用unique_ptr,它非常适合于返回值类型为unique_ptr的函数

6.1 unique_ptr场景示例:

6.1.1 作为参数

因为不能被拷贝,所以传递裸指针或者引用或者外部不需要了直接转移,但是要注意不能传递值(拷贝)

c++ 复制代码
// 裸指针
#include<iostream>
#include<memory>
void test(int *p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(up.get());//传入裸指针作为参数
    std::cout<<*up<<std::endl;//输出10
    return 0;
}


// 引用
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> &p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(up);
    std::cout<<*up<<std::endl;//输出10
    return 0;
}

// 转移
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(std::unique_ptr<int>(up.release()));
    //test(std::move(up));//这种方式也可以
    return 0;
}

6.1.2 作为返回值

返回可以用unique_ptr(伪代码),这里可以理解为返回值是拷贝了指向的地方,相当于std::move,获取唯一的所有权

unique_ptr<int> make_init(int n)
{
	return unique_ptr<int> (new int(n));
}

int main()
{
···
	vector<unique_ptr<int>> vp(size);
	for(int i = 0; i < vp.size(); i++)
		vp[i] = make_init(rand() % 1000)
···
}

6.2 shared_ptr场景示例:

6.2.1 作为参数
#include<iostream>
#include<memory>
void func0(std::shared_ptr<int> sp)
{
    std::cout<<"fun0:"<<sp.use_count()<<std::endl;
}

void func1(std::shared_ptr<int> sp)
{
    std::cout<<"fun1:"<<sp.use_count()<<std::endl;
}

void func2(std::shared_ptr<int> &sp)
{
    std::cout<<"fun1:"<<sp.use_count()<<std::endl;
}

int main()
{
    auto sp = std::make_shared<int>(1024);
    func0(sp);   // 拷贝方式(这种方式unique不可以)
    func1(sp);	 // 拷贝方式(这种方式unique不可以)
    func2(sp);   // 引用方式
    return 0;
}

这里建议传参使用引用,免拷贝。

参考资料

主要参考:https://www.yanbinghu.com/categories/Cpp/

C++11智能指针: https://www.jianshu.com/p/e4919f1c3a28

C++11 shared_ptr智能指针:http://c.biancheng.net/view/7898.html

shared_ptr官方讲解:http://www.cplusplus.com/reference/memory/shared_ptr/shared_ptr/

unique_ptr官方讲解:http://www.cplusplus.com/reference/memory/unique_ptr/unique_ptr/

智能指针的选择:https://blog.csdn.net/qq_22533607/article/details/82318595

相关推荐
A懿轩A10 分钟前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导15 分钟前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香15 分钟前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Ronin3051 小时前
11.vector的介绍及模拟实现
开发语言·c++
✿ ༺ ོIT技术༻1 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
字节高级特工1 小时前
【C++】深入剖析默认成员函数3:拷贝构造函数
c语言·c++
唐诺7 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨8 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客9 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin9 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin