C++智能指针


简介

接触Aria2项目已经有大半年多了,对于项目的源码实现思路都有更深入的了解,通过接触Aria2 也让我了解了C++ 11的更多高级的特性,比如智能指针,该项目大量的采用智能指针的方式,可以做到很巧妙的不用处理内存释放的问题,整个项目源码的修改都是由我完成的,由于对C++ 高级的特性不是很熟悉,导致每次写代码都写的非常小心,在写代码之前,都要在源码里面看看别人是怎么样写的,自己参考着写,导致对自己写的代码没有多大的信心,所以这篇文章会大致讲解下我对智能指针的理解,在了解什么智能指针之前要先明白什么是引用

什么是引用

引用的特性

引用:就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
如下:定义引用ra,它是变量a的引用,即别名。
int a;
int &ra=a;
(1)&在此不是求地址运算符,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量有两个名称即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。

引用的本质

引用的本质是占内存空间的,就是一个常量指针,所以必须要在定义的时候,分配内存空间的时候,完成赋值,之后就不允许再次的赋值,只是编译器帮我们做了处理,接下来会证明引用是占用内存空间的,并且本质是一个常量指针

下面是证明

#include <iostream>
using namespace std;

//定义一个全局的变量num
int num = 99;

class A
{
public:
    A();
public:
    int n;
    int &r;
};

//构造函数赋值,引用必须在初始化成员列表中赋值,并且赋值之后,就不允许改变了,因为初始化成员列表中才是定义这个变量,在类里面的只是声明,由于引用本质为常量指针,所以必须在定义的时候
//就要执行赋值操作,在初始化成员列表中赋值跟在构造函数里面赋值是完全不同的,在构造函数里面更像是一种计算操作
A::A() :n(0), r(num)
{

}

//从而可以退出,引用是占用内存空间的,他的本质是一个常量指针,不管是什么类型的指针,都是占用4个字节的地址,而且只有在初始化的时候赋值,本质是一个常量指针,之后就不允许赋值了
//其实引用只是对指针进行了简单的封装,它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度,是一样的,在32位环境下是4个字节,在64位环境下是8个字节,之所以不能获取引用的地址,是因为编译器进行了内部的转换
void main()
{
    A *a = new A();
    cout << sizeof(A) << endl;//输出A类型的大小 , 8
    printf("r == %x\n", (int *)a + 1);//输出引用的地址 , 7df394
    printf("a == %x\n", a);//输出a的地址 , 7df390
    printf("n == %x\n", &(a->n));//输出n的地址 , 7df390,这个先声明,所以先压栈
    printf("r == %x\n", *((int *)a + 1));//输出引用r的值  , 23d000
    printf("num == %x\n", &num);//输出num变量的地址  ,23d000

    //使用&r取地址时,编译器会对代码进行隐式的转换,使得代码输出的是 r 的内容(a 的地址),而不是 r 的地址,
    //这就是为什么获取不到引用变量的地址的原因。也就是说,不是变量 r 不占用内存,而是编译器不让获取它的地址。
    system("pause");
}

打印的结果为
结果显示

智能指针的由来

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

智能指针的作用

1.从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
2.智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
3.智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:

Animal a = new Animal();

Animal b = a;

你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,

Animal a;

Animal b = a;

这里却是就是生成了两个对象。

智能指针的使用

智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr

智能指针的本质

智能指针本质是一个普通的变量,他通过内部持有了你原有的对象,然后通过引用计数的方式来决定是否要删除这个原有的对象,比如当引用计数为0的时候,就要删除,当智能指针拷贝或者复制的时候会让他们的引用计数加一

智能指针的设计和实现

在介绍 shared_ptr、unique_ptr 还是先大概的了解下他们是怎么实现的,下面是模拟智能指针的实现,智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
    //真实的指针对象
    T* _ptr;
    //用来做为引用计数
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) :
            _ptr(ptr) {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    SmartPointer(const SmartPointer& ptr) {
        if (this != &ptr) {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPointer& operator=(const SmartPointer& ptr) {
        if (this->_ptr == ptr._ptr) {
            return *this;
        }

        if (this->_ptr) {
            (*this->_count)--;
            if (this->_count == 0) {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++;
        return *this;
    }

    T& operator*() {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);

    }

    T* operator->() {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer() {
        (*this->_count)--;
        if (*this->_count == 0) {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count(){
        return *this->_count;
    }
};

int main() {
    {
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        std::cout << sp.use_count() << std::endl;
        std::cout << sp3.use_count() << std::endl;
    }
    //delete operator
}

shared_ptr

shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。hared_ptr内部的引用计数是安全的,但是对象的读取需要加锁。

#include "stdafx.h"
#include <iostream>
#include <future>
#include <thread>

using namespace std;
class Person
{
public:
    Person(int v) {
        value = v;
        std::cout << "Cons" <<value<< std::endl;
    }
    ~Person() {
        std::cout << "Des" <<value<< std::endl;
    }
    int value;
};

int main()
{
    std::shared_ptr<Person> p1(new Person(1));// Person(1)的引用计数为1

    std::shared_ptr<Person> p2 = std::make_shared<Person>(2);

    p1.reset(new Person(3));// 首先生成新对象,然后引用计数减1,引用计数为0,故析构Person(1)
                            // 最后将新对象的指针交给智能指针

    std::shared_ptr<Person> p3 = p1;//现在p1和p3同时指向Person(3),Person(3)的引用计数为2

    p1.reset();//Person(3)的引用计数为1
    p3.reset();//Person(3)的引用计数为0,析构Person(3)
    return 0;
}
reset()包含两个操作。当智能指针中有值的时候,调用reset()会使引用计数减1.当调用reset(new xxx())重新赋值时,智能指针首先是生成新对象,然后将就对象的引用计数减1
(当然,如果发现引用计数为0时,则析构旧对象),然后将新对象的指针交给智能指针保管。

unique_ptr的使用

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、s通过移动语义转移所有权,

#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放,因为智能指针本质是一个普通的对象,到了这里智能指针变量就会被释放了,导致他虚构函数的执行,从而释放他原有的真实的对象
}

可以看出来unique_ptr本质跟shared_ptr没有多大的区别,唯一的区别就是不能赋值,不能拷贝,通过move操作转换之后,就会转换所有权

实操

了解了引用的本质,跟智能指针的本质之后,写代码就不会有那么多的疑问了,比如


//检查执行Tracker请求
std::unique_ptr<AnnRequest> TrackerWatcherCommand::checkAndExuteTracke(DownloadEngine* e,std::shared_ptr<AnnounceTier>& annouceItem)
{
    //创造用于连接tracker的请求,是要按照规范来的
    std::string uri = btAnnounce_->getAnnounceUrl(annouceItem);
    LOGD("TrackerWatcherCommand::createAnnounce announceUrl %s", uri.c_str());
    uri_split_result res;
    memset(&res, 0, sizeof(res));
    if (uri_split(&res, uri.c_str()) == 0) {
        // Without UDP tracker support, send it to normal tracker flow  and make it fail.
        std::unique_ptr<AnnRequest> treq;
        //根据tracker的url类型,创建不同的类型的请求,比如udp,http
        if (udpTrackerClient_ && uri::getFieldString(res, USR_SCHEME, uri.c_str()) == "udp") {
            uint16_t localPort;
            //修改为UDP端口 这个主要是用来告诉tracker服务器收集当前连接用户的peer信息,好让下一个用户连接上tracker服务器之后,他可以返回当前已经连接tracker服务器peer信息
            localPort = e->getBtRegistry()->getUdpPort();
            //创建udp类型的Tracker连接
            treq = createUDPAnnRequest(uri::getFieldString(res, USR_HOST, uri.c_str()), res.port, localPort, annouceItem);
        } else {
            //创建Http类型的Tracker连接
            treq = createHTTPAnnRequest(uri,annouceItem);
        }
        return std::move(treq);
    }
    return nullptr;
}

比如这里的返回值为什么不是返回一个引用,因为返回值是来自局部变量, std::unique_ptr<AnnRequest> treq 当这个函数执行完毕之后,这个智能指针变量就会被销毁,如果返回一个引用的化,
引用的本质是一个指针,那么外部得到这个返回值进行访问的时候,肯定是闪退的,因为他访问了一个非法的地址,所以这里不能返回引用

再比如:
//当前还允许tracker 并发执行的数量没有达到最大的限度
if(btAnnounce_->getMaxConcurrentSize() > 0)
{
   //获取到当前可以执行的 tracker 请求的 AnnounceTier 集合,注意这里的needExuteAnnounceList 为临时变量
   std::deque<std::shared_ptr<AnnounceTier>> needExuteAnnounceList = btAnnounce_->getCanExeuteUrlList();
   //当前最多可以执行的tracker 请求的并发数不应该大于 当前种子最多的tracker 并发数
   int minSize = btAnnounce_->getMaxConcurrentSize() > needExuteAnnounceList.size() ? needExuteAnnounceList.size() : btAnnounce_->getMaxConcurrentSize();
   //遍历将每一个AnnounceTier,变成对应的AnnRequest
   for(int i = 0; i< minSize;i++)
   {
       std::shared_ptr<AnnounceTier> tierItem = needExuteAnnounceList.front();
       auto request = checkAndExuteTracke(e_,tierItem);
       //开始执行请求
       request->issue(e_);
       //设置并发数量减一处理
       btAnnounce_->setConcurrentSize((btAnnounce_->getMaxConcurrentSize() - 1));
       //设置为请求开始了
       tierItem->setAnnounceExute();
       //将当前的请求添加到发送队列中
       trakerRequestList_.push_back(std::move(request));
       //弹出
       needExuteAnnounceList.pop_front();
       LOGD("tracker request created trakerRequestList_ size %d",trakerRequestList_.size());
   }
}

std::unique_ptr<AnnRequest> TrackerWatcherCommand::checkAndExuteTracke(DownloadEngine* e,std::shared_ptr<AnnounceTier>& annouceItem)
{
    ....
}
这里的annouceItem 可以为一个引用,这里就应该要使用一个引用了,因为引用本质是一个地址,如果不使用引用,使用 std::shared_ptr<AnnounceTier> 那就相当于是重新的新建了一个变量
执行了浅拷贝的操作,至于为什么能使用引用,是因为tierItem 的作用域已经足够,虽然是局部变量


通过使用指针指针,很多事情都不用自己管理,比如 我们在类成员中定义这样的成员
//用于存储当前tracker 请求的集合
std::deque<std::unique_ptr<AnnRequest>> trakerRequestList_;

对这个集合的移除我们可以像普通变量来操作,因为智能指针本质就是一个普通的对象,从集合中移除了,也就释放了
trakerRequestList_.erase(std::remove_if(std::begin(trakerRequestList_), std::end(trakerRequestList_),
                                            [&](const std::unique_ptr<AnnRequest> &ent) {
                                                if(ent->stopped())
                                                {
                                                    //移除的时候,让并发数量加一
                                                    btAnnounce_->setConcurrentSize((btAnnounce_->getMaxConcurrentSize() + 1));
                                                }
                                                return ent->stopped();
                                            }),
                             std::end(trakerRequestList_));


对于能否使用引用,本质要看这个变量的声明周期,再看一个

 //当前还允许tracker 并发执行的数量没有达到最大的限度
  if(btAnnounce_->getMaxConcurrentSize() > 0)
  {
      //获取到当前可以执行的 tracker 请求的 AnnounceTier 集合
      std::deque<std::shared_ptr<AnnounceTier>> needExuteAnnounceList = btAnnounce_->getCanExeuteUrlList();
      //当前最多可以执行的tracker 请求的并发数不应该大于 当前种子最多的tracker 并发数
      int minSize = btAnnounce_->getMaxConcurrentSize() > needExuteAnnounceList.size() ? needExuteAnnounceList.size() : btAnnounce_->getMaxConcurrentSize();
      //遍历将每一个AnnounceTier,变成对应的AnnRequest
      for(int i = 0; i< minSize;i++)
      {
          std::shared_ptr<AnnounceTier> tierItem = needExuteAnnounceList.front();
          auto request = checkAndExuteTracke(e_,tierItem);
          //开始执行请求
          request->issue(e_);
          //设置并发数量减一处理
          btAnnounce_->setConcurrentSize((btAnnounce_->getMaxConcurrentSize() - 1));
          //设置为请求开始了
          tierItem->setAnnounceExute();
          //将当前的请求添加到发送队列中
          trakerRequestList_.push_back(std::move(request));
          //弹出
          needExuteAnnounceList.pop_front();
          LOGD("tracker request created trakerRequestList_ size %d",trakerRequestList_.size());
      }
  }

  //请求中,判断是否获取到了结果
  for (auto &requestItem : trakerRequestList_) {
     if (requestItem->stopped())//如果当前请求已经停止
     {
        LOGD("trakcr request stop url %s",requestItem->getAnnounceTier()->getUrl().c_str());
        if (requestItem->success()) {//当前请求成功
             if (requestItem->processResponse(btAnnounce_))
             {
                 requestItem->getAnnounceTier()->trackerSuccess();
                 //根据请求traker 返回的peer 执行连接
                 addConnection();
             } else {
                 //解析失败
                 requestItem->getAnnounceTier()->trackerFailer(requestItem->getCurrentTrackerEvent());
             }
        } else {
             //由BtAnnounce 告知哪个tracker 请求失败
            requestItem->getAnnounceTier()->trackerFailer(requestItem->getCurrentTrackerEvent());
        }
     }
  }            

  //获取当前请求对应的 AnnounceTier,这边不能返回一个引用,因为是局部变量
  virtual std::shared_ptr<AnnounceTier> getAnnounceTier() = 0;

  我原本想在getAnnounceTier 的时候返回一个引用,如果可行的化,性能会更加高,但是这是错误的,因为这个 AnnounceTier 是由 needExuteAnnounceList 局部的成员集合传递进去的,
  而且下面的getAnnounceTier() 跟上面的 needExuteAnnounceList 是异步的,所以会导致 等你获取这个引用的时候,其实这个引用已经被释放掉了,导致访问了非法的地址,导致闪退

总结

应该大量的使用智能指针,对于是否应该要使用引用,看这个变量的生命周期,如果生命周期允许的化,可以使用


文章作者: AheadSnail
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AheadSnail !
评论
  目录