C++新特性系列三:智能指针

一、概述

  1. 为什么要有智能指针:直接使用new和delete运算符极其容易导致内存泄露,而且非常难以避免。于是人们发明了智能指针这种可以自动回收内存的工具。

  2. 智能指针一共就三种:普通的指针可以单独一个指针占用一块内存,也可以多个指针共享一块内存。

  • 共享型智能指针:shared_ptr,同一块堆内存可以被多个shared_ptr共享。
  • 独享型智能指针:unique_ptr,同一块堆内存只能被一个unique_ptr拥有。
  • 弱引用智能指针:weak_ptr,也是一种共享型智能指针,可以视为对共享型智能指针的一种补充
  1. (*)智能指针注意事项:智能指针和裸指针不要混用

二、shared_ptr

  1. shared_ptr的工作原理
  • 我们在动态分配内存时,堆上的内存必须通过栈上的内存来寻址。也就是说栈上的指针(堆上的指针也可以指向堆内存,但终究是要通过栈来寻址的)是寻找堆内存的唯一方式。
  • 所以我们可以给堆内存添加一个引用计数,有几个指针指向它,它的引用计数就是几,当引用计数为0是,操作系统会自动释放这块堆内存。
  1. Shared_ptr的常用操作
  • shared_ptr的初始化
    • 使用new运算符初始化
    • 一般来说不推荐使用new进行初始化,因为C++标准提供了专门创建shared_ptr的函数“make_shared”,该函数是经过优化的,效率更高,使用make_shared函数进行初始化:
    • 当然使用复制构造函数初始化也是没有问题的。
#include <iostream>
#include <memory>

int main()
{
int *pi = new int(100); //存放在堆内存上
delete pi;
std::shared_ptr<int> shareI(new int(100));//使用new初始化
std::shared_ptr<int> shareII=std::make_shared<int>(100);//make_share初始化
std::shared_ptr<int> share3(shareII);//使用拷贝构造函数 初始化
return 0;
}

注意:千万不要用裸指针初始化shared_ptr,容易出现内存泄露的问题。

  • shared_ptr的引用计数:

智能指针就是通过引用计数来判断释放堆内存时机的。
use_count()函数可以得到shared_ptr对象的引用计数。

#include <iostream>
#include <memory>

int main()
{
int *pi = new int(100); //存放在堆内存上
delete pi;
std::shared_ptr<int> shareI(new int(100));//使用new初始化
std::shared_ptr<int> shareII=std::make_shared<int>(100);//make_share初始化
std::cout<<shareII.use_count()<<std::endl; //1
std::shared_ptr<int> share3(shareII);//使用拷贝构造函数 初始化
std::cout<<shareII.use_count()<<std::endl; //2
share3.reset();
std::cout<<shareII.use_count()<<std::endl;//1
return 0;
}
  1. 智能指针可以像普通指针那样使用,”share_ptr”早已对各种操作进行了重载,就当它是普通指针就可以了。

  2. Shared_ptr的常用函数

  • unique函数:判断该shared_ptr对象是否独占若独占,返回true。否则返回false。
#include <iostream>
#include <memory>

int main()
{
int *pi = new int(100); //存放在堆内存上
delete pi;
std::shared_ptr<int> shareI(new int(100));//使用new初始化
std::cout<<shareI.unique()<<std::endl;//返回1
std::shared_ptr<int> shareII=std::make_shared<int>(100);//make_share初始化
std::cout<<shareI.unique()<<std::endl;//返回1
std::cout<<shareII.use_count()<<std::endl; //1
std::shared_ptr<int> share3(shareII);//使用拷贝构造函数 初始化
std::cout<<shareII.unique()<<std::endl;//返回0
std::cout<<shareII.use_count()<<std::endl; //2
share3.reset();
std::cout<<shareII.use_count()<<std::endl;//1
std::cout<<*shareII<<std::endl;
return 0;
}
  • reset函数:
    • 当reset函数有参数时,改变此shared_ptr对象指向的内存。
    • 当reset函数无参数时,将此shared_ptr对象置空,也就是将对象内存的指针设置为nullptr。
#include <iostream>
#include <memory>

int main()
{
std::shared_ptr<int> shareI=std::make_shared<int>(100);
std::cout<<shareI.unique()<<std::endl;//返回1
shareI.reset();//将该指针对象置为空
shareI.reset(new int(1000));//指向新的对象
return 0;
}
  • get函数,强烈不推荐使用:如果一定要使用,那么一定不能delete返回的指针。
  • swap函数:交换两个智能指针所指向的内存
    • std命名空间中全局的swap函数
    • shared_ptr类提供的swap函数
#include <iostream>
#include <memory>

int main()
{
std::shared_ptr<int> shareI = std::make_shared<int>(100);

std::shared_ptr<int> shareI2 = std::make_shared<int>(1000);

shareI.swap(shareI2);//swap交换
std::cout << *shareI << std::endl; // 1000
std::cout << *shareI2 << std::endl; // 100

std::swap(shareI,shareI2);//也可以
return 0;
}
  1. 关于智能指针创建数组的问题。
#include <iostream>
#include <memory>

int main()
{
std::shared_ptr<int> share1(new int[100]());
std::cout<<share1.get()[10]<<std::endl;
return 0;
}
  1. 用智能指针作为参数传递时直接值传递就可以了。shared_ptr的大小为固定的8或16字节(也就是两倍指针的的大小,32位系统指针为4个字节,64位系统指针为8个字节,shared_ptr中就两个指针),所以直接值传递就可以了。
#include <iostream>
#include <memory>

void myFunc(const std::shared_ptr<int> shareI){

}

int main()
{
std::shared_ptr<int> share1=std::make_shared<int>(100);
myFunc(share1);
return 0;
}
  1. shared_ptr总结:在现代程序中,当想要共享一块堆内存时,优先使用shared_ptr,可以极大的减少内存泄露的问题。

三、(*)weak_ptr

  1. weak_ptr介绍:
  • 这个智能指针是在C++ 11的时候引入的标准库,它的出现完全是为了弥补shared_ptr天生有缺陷的问题,其实shared_ptr可以说近乎完美。
  • 只是通过引用计数实现的方式也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++ 11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。
  1. shared_ptr的循环引用问题:
int main()
{
std::shared_ptr<int> share1 = std::make_shared<int>(100);
std::cout << share1.use_count() << std::endl;

std::weak_ptr<int> weak1(share1); //不会增加share_ptd的引用计数
std::cout << share1.use_count() << std::endl;
return 0;
}

循环引用问题:

#include <iostream>
#include <memory>

class B;
class A
{
public:
std::shared_ptr<B> shareB;
};

class B
{
public:
std::shared_ptr<A> shareA;
};

int main()
{
std::shared_ptr<A> shareA=std::make_shared<A>();
std::shared_ptr<B> shareB=std::make_shared<B>();
//互相在堆上指向,导致内存无法释放
shareA->shareB=shareB;
shareB->shareA=shareA;
return 0;
}

解决方法:使用weak_ptr

#include <iostream>
#include <memory>

class B;
class A
{
public:
std::weak_ptr<B> weakB;
};

class B
{
public:
std::shared_ptr<A> shareA;
};

int main()
{
std::shared_ptr<A> shareA=std::make_shared<A>();
std::shared_ptr<B> shareB=std::make_shared<B>();
//互相在堆上指向,导致内存无法释放
shareA->weakB=shareB;
shareB->shareA=shareA;
return 0;
}

  1. weak_ptr的作用原理:weak_ptr的对象需要绑定到shared_ptr对象上,作用原理是weak_ptr不会改变shared_ptr对象的引用计数。只要shared_ptr对象的引用计数为0,就会释放内存,weak_ptr的对象不会影响释放内存的过程。

4.weak_ptr的总结:weak_ptr使用较少,就是为了处理shared_ptr循环引用问题而设计的。

四、unique_ptr

  1. uniqe_ptr介绍:独占式智能指针,在使用智能指针时,我们一般优先考虑独占式智能指针,因为消耗更小。如果发现内存需要共享,那么再去使用“shared_ptr”。
  2. unique_ptr的初始化:和shared_ptr完全类似
  • 使用new运算符进行初始化
  • 使用make_unique函数进行初始化
#include <iostream>
#include <memory>

int main()
{
std::unique_ptr<int> uniqueI(new int(100));
std::unique_ptr<int> uniqueII = std::make_unique<int>(1000);
return 0;
}
  1. unique_ptr的常用操作
  • unque_ptr禁止复制构造函数,也禁止赋值运算符的重载。否则独占便毫无意义。
  • unqiue_ptr允许移动构造,移动赋值。移动语义代表之前的对象已经失去了意义,移动操作自然不影响独占的特性。
#include <iostream>
#include <memory>

int main()
{
std::unique_ptr<int> uniqueII = std::make_unique<int>(1000);

std::unique_ptr<int> uniqueII2(std::move(uniqueII)); //允许移动构造

std::unique_ptr<int> uniqueIII = std::make_unique<int>(1000);
uniqueIII = std::move(uniqueII); //允许移动赋值

return 0;
}
  • reset函数:
    • 不带参数的情况下:释放智能指针的对象,并将智能指针置空。
    • 带参数的情况下:释放智能指针的对象,并将智能指针指向新的对象。
#include <iostream>
#include <memory>

int main()
{
std::unique_ptr<int> uniqueII = std::make_unique<int>(1000);
std::unique_ptr<int> uniqueIII = std::make_unique<int>(1000);
uniqueIII.reset(); //直接置空
uniqueIII.reset(new int(10000));
return 0;
}
  1. 将unque_ptr的对象转化为shared_ptr对象,当unique_ptr的对象为一个右值时,就可以将该对象转化为shared_ptr的对象。
#include <iostream>
#include <memory>

void myFunc(std::unique_ptr<int> unique)
{
//得保证不再使用unique
std::shared_ptr<int> shareI(std::move(unique));
}

int main()
{
std::unique_ptr<int> uniqueII = std::make_unique<int>(1000);
std::unique_ptr<int> uniqueIII = std::make_unique<int>(1000);
uniqueIII.reset(); //直接置空
uniqueIII.reset(new int(10000));
return 0;
}

这个使用的并不多,需要将独占式指针转化为共享式指针常常是因为先前设计失误。
注意:shared_ptr对象无法转化为unique_ptr对象。

五、智能指针使用范围

  1. 能使用智能指针就尽量使用智能指针,那么哪些情况属于不能使用智能指针的情况:

有些函数必须使用C语言的指针,这些函数又没有替代,这种情况下,才使用普通的指针,其它情况一律使用智能指针。

必须使用C语言指针的情况包括:

  • 网络传输函数,比如windows下的send,recv函数,只能使用c语言指针,无法替代.
  • c语言的文件操作部分。这方面C++ 已经有了替代品,C++ 的文件操作完全支持智能指针,所以在做大型项目时,推荐使用C++ 的文件操作功能

除了以上两种情况,剩下的均推荐使用智能指针。

  1. 我们应该使用哪个智能指针呢?
  • 优先使用unique_ptr,内存需要共享时再使用shared_ptr。
  • 当使用shared_ptr时,如果出现了循环引用的情况,再去考虑使用weak_ptr。
Author: CinKate
Link: http://renxingkai.github.io/2022/08/22/cpp-ptr/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.