一文詳解C++智能指針的原理、分類及使用


1. 智能指針介紹

為解決裸指針可能導(dǎo)致的內(nèi)存泄漏問題。如:

a)忘記釋放內(nèi)存;

b)程序提前退出導(dǎo)致資源釋放代碼未執(zhí)行到。

就出現(xiàn)了智能指針,能夠做到資源的自動(dòng)釋放。

 

2. 智能指針的原理和簡單實(shí)現(xiàn)

2.1 智能指針的原理

將裸指針封裝為一個(gè)智能指針類,需要使用該裸指針時(shí),就創(chuàng)建該類的對(duì)象;利用棧區(qū)對(duì)象出作用域會(huì)自動(dòng)析構(gòu)的特性,保證資源的自動(dòng)釋放。

2.2 智能指針的簡單實(shí)現(xiàn)

代碼示例:

template class mysmartptr {
public:
  mysmartptr(t* ptr = nullptr):mptr(ptr) { // 創(chuàng)建該對(duì)象時(shí),裸指針會(huì)傳給對(duì)象
  }

  ~mysmartptr() {  // 對(duì)象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源
      delete mptr;
  }
  
  // *運(yùn)算符重載
  t& operator*() {  // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用
      return *mptr; 
  }

  // ->運(yùn)算符重載
  t* operator->() { // 即返回裸指針
      return mptr;
  }
private:
  t* mptr;
};

class obj {
public:
  void func() {
      cout << "obj::func" << endl;
  }
};

void test01() {

  /*創(chuàng)建一個(gè)int型的裸指針,
  使用mysmartptr將其封裝為智能指針對(duì)象ptr,ptr對(duì)象除了作用域就會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)。
  智能指針就是利用棧上對(duì)象出作用域自動(dòng)析構(gòu)這一特性。*/
  mysmartptr ptr0(new int);
  *ptr0 = 10;

  mysmartptr ptr1(new obj);
  ptr1->func();
  (ptr1.operator->())->func(); // 等價(jià)于上面

  /*  中間異常退出,智能指針也會(huì)自動(dòng)釋放資源。
  if (xxx) {
      throw "....";
  }
  
  if (yyy) {
      return -1;
  }
  */
}

 

3. 智能指針分類

3.1 問題引入

接著使用上述自己實(shí)現(xiàn)的智能指針進(jìn)行拷貝構(gòu)造:

void test02() {
  mysmartptr p1(new int); // p1指向一塊int型內(nèi)存空間
  mysmartptr p2(p1);      // p2指向p1指向的內(nèi)存空間
  
  *p1 = 10;   // 內(nèi)存空間的值為10
  *p2 = 20;   // 內(nèi)存空間的值被改為20
}

但運(yùn)行時(shí)出錯(cuò):

原因在于p1和p2指向同一塊int型堆區(qū)內(nèi)存空間,p2析構(gòu)將該int型空間釋放,p1再析構(gòu)時(shí)釋放同一塊內(nèi)存,則出錯(cuò)。

那可否使用如下深拷貝解決該問題?

mysmartptr(cosnt mysmartptr& src) {
  mptr = new t(*src.mptr);
}

不可以。因?yàn)榘凑章阒羔樀氖褂梅绞?,用戶本意是想將p1和p2都指向該int型堆區(qū)內(nèi)存,使用指針p1、p2都可改變?cè)搩?nèi)存空間的值,顯然深拷貝不符合此場景。

3.2 兩類智能指針

不帶引用計(jì)數(shù)的智能指針:只能有一個(gè)指針管理資源。

auto_ptr;

scoped_ptr;

unique_ptr;.

帶引用計(jì)數(shù)的智能指針:可以有多個(gè)指針同時(shí)管理資源。

shared_ptr;強(qiáng)智能指針。

weak_ptr: 弱智能指針。這是特例,不能控制資源的生命周期,不能控制資源的自動(dòng)釋放!

3.3 不帶引用計(jì)數(shù)的智能指針

只能有一個(gè)指針管理資源。

3.3.1 auto_ptr (不推薦使用)

void test03() {
	auto_ptr ptr1(new int);
	auto_ptr ptr2(ptr1);
	*ptr2 = 20;
	// cout << *ptr2 << endl; // 可訪問*ptr2
	cout << *ptr1 << endl; //訪問*ptr1卻報(bào)錯(cuò)
}

如上代碼,訪問*ptr1為何報(bào)錯(cuò)?

因?yàn)檎{(diào)用auto_ptr的拷貝構(gòu)造將ptr1的值賦值給ptr2后,底層會(huì)將ptr1指向nullptr;即將同一個(gè)指針拷貝構(gòu)造多次時(shí),只讓最后一次拷貝的指針管理資源,前面的指針全指向nullptr。

不推薦將auto_ptr存入容器。

3.3.2 scoped_ptr (使用較少)

scoped_ptr已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete了。

scoped_ptr(const scoped_ptr&) = delete; // 刪除拷貝構(gòu)造
scoped_ptr& operator=(const scoped_ptr&) = delete;  // 刪除賦值重載

3.3.3 unique_ptr (推薦使用)

unique_ptr也已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete。

unique_ptr(const unique_ptr&) = delete; // 刪除拷貝構(gòu)造
unique_ptr& operator=(const unique_ptr&) = delete;  // 刪除賦值重載

但unique_ptr提供了帶右值引用參數(shù)的拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載,如下:

void test04() {
	unique_ptr ptr1(new int);
	// unique_ptr ptr2(ptr1);  和scoped_ptr一樣無法通過編譯
	unique_ptr ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值類型
  // *ptr1  也無法訪問
}

3.4 帶引用計(jì)數(shù)的智能指針

可以有多個(gè)指針同時(shí)管理資源。

原理:給智能指針添加其指向資源的引用計(jì)數(shù)屬性,若引用計(jì)數(shù) > 0,則不會(huì)釋放資源,若引用計(jì)數(shù) = 0就釋放資源。

具體來說:額外創(chuàng)建資源引用計(jì)數(shù)類,在智能指針類中加入該資源引用計(jì)數(shù)類的指針作為其中的一個(gè)屬性;當(dāng)使用裸指針創(chuàng)建智能指針對(duì)象時(shí),創(chuàng)建智能指針中的資源引用計(jì)數(shù)對(duì)象,并將其中的引用計(jì)數(shù)屬性初始化為1,當(dāng)后面對(duì)該智能指針對(duì)象進(jìn)行拷貝(使用其他智能指針指向該資源時(shí))或時(shí),需要在其他智能指針對(duì)象類中將被拷貝的智能指針對(duì)象中的資源引用計(jì)數(shù)類的指針獲取過來,然后將引用計(jì)數(shù)+1;當(dāng)用該智能指針給其他智能指針進(jìn)行賦值時(shí),因?yàn)槠渌悄苤羔槺毁x值后,它們就不指向原先的資源了,原先資源的引用計(jì)數(shù)就-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源;當(dāng)智能指針對(duì)象析構(gòu)時(shí),會(huì)使用其中的資源引用計(jì)數(shù)指針將共享的引用計(jì)數(shù)-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源。

shared_ptr:強(qiáng)智能指針;可改變資源的引用計(jì)數(shù)。

weak_ptr:弱智能指針;不可改變資源的引用計(jì)數(shù)。

帶引用計(jì)數(shù)的智能指針的簡單實(shí)現(xiàn):

/*資源的引用計(jì)數(shù)類*/
template class refcnt {
public:
  refcnt(t* ptr=nullptr):mptr(ptr) {
      if (mptr != nullptr) {
          mcount = 1; // 剛創(chuàng)建指針指針時(shí),引用計(jì)數(shù)初始化為1
      }
  }

  void addref() {  // 增加引用計(jì)數(shù)
      mcount++;
  }

  int delref() {   // 減少引用計(jì)數(shù)
      mcount--;
      return mcount;
  }
private:
  t* mptr;  // 資源地址
  int mcount; // 資源的引用計(jì)數(shù)
};

/*智能指針類*/
template class mysmartptr {
public:
  mysmartptr(t* ptr = nullptr) :mptr(ptr) { // 創(chuàng)建該對(duì)象時(shí),裸指針會(huì)傳給對(duì)象
      mprefcnt = new refcnt(mptr);
  }

  ~mysmartptr() {  // 對(duì)象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源
      if (0 == mprefcnt->delref()) {
          delete mptr;
          mptr = nullptr;
      }
  }

  // *運(yùn)算符重載
  t& operator*() {  // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用
      return *mptr;
  }

  // ->運(yùn)算符重載
  t* operator->() { // 即返回裸指針
      return mptr;
  }

  // 拷貝構(gòu)造
  mysmartptr(const mysmartptr& src):mptr(src.mptr),mprefcnt(src.mprefcnt) {
      if (mptr != nullptr) {
          mprefcnt->addref();
      }
  }

  // 賦值重載
  mysmartptr& operator=(const mysmartptr& src) {
      if (this == &src) // 防止自賦值
          return *this;

      /*若本指針改為指向src管理的資源,則本指針原先指向的資源的引用計(jì)數(shù)-1,
      若原資源的引用計(jì)數(shù)為0,就釋放資源*/
      if (0 == mprefcnt->delref()) {  
          delete mptr;
      }

      mptr = src.mptr;
      mprefcnt = src.mprefcnt;
      mprefcnt->addref();
      return *this;
  }
private:
  t* mptr;  // 指向資源的指針
  refcnt* mprefcnt; // 資源的引用計(jì)數(shù)
};

強(qiáng)智能指針原理圖:

比如有如下創(chuàng)建強(qiáng)智能指針的語句:

shared_ptr sp1(new int(10));

則如下所示:

(a)智能指針對(duì)象sp1中主要包括ptr指針指向其管理的資源,ref指針指向該資源的引用計(jì)數(shù),則顯然會(huì)開辟兩次內(nèi)存。

(b)uses為該資源的強(qiáng)智能指針的引用計(jì)數(shù),weaks為該資源的弱智能指針的引用計(jì)數(shù)。

3.4.1 shared_ptr

強(qiáng)智能指針。可改變資源的引用計(jì)數(shù)。

(1)強(qiáng)智能指針的交叉引用問題

class b;

class a {
public:
  a() {
      cout << "a()" << endl;
  }
  ~a() {
      cout << "~a()" << endl;
  }
  shared_ptr _ptrb;
};

class b {
public:
  b() {
      cout << "b()" << endl;
  }
  ~b() {
      cout << "~b()" << endl;
  }
  shared_ptr _ptra;
};

void test06() {
  shared_ptr pa(new a());
  shared_ptr pb(new b());

  pa->_ptrb = pb;
  pb->_ptra = pa;

  /*打印pa、pb指向資源的引用計(jì)數(shù)*/
  cout << pa.use_count() << endl;
  cout << pb.use_count() << endl;
}

輸出結(jié)果:

可見pa、pb指向的資源的引用計(jì)數(shù)都為2,因此出了作用域?qū)е聀a、pb指向的資源都無法釋放,如下圖所示:

解決:

建議定義對(duì)象時(shí)使用強(qiáng)智能指針,引用對(duì)象時(shí)使用弱智能指針,防止出現(xiàn)交叉引用的問題。

什么是定義對(duì)象?什么是引用對(duì)象?

定義對(duì)象:

使用new創(chuàng)建對(duì)象,并創(chuàng)建一個(gè)新的智能指針管理它。

引用對(duì)象:

使用一個(gè)已存在的智能指針來創(chuàng)建一個(gè)新的智能指針。

定義對(duì)象和引用對(duì)象的示例如下:

shared_ptr p1(new int());              // 定義智能指針對(duì)象p1
shared_ptr p2 = make_shared(10);  // 定義智能指針對(duì)象p2

shared_ptr p3 = p1;  // 引用智能指針p1,并使用p3來共享它
weak_ptr p4 = p2;    // 引用智能指針p2,并使用p4來觀察它

如上述代碼,因?yàn)樵趖est06函數(shù)中使用pa對(duì)象的_ptrb引用pb對(duì)象,使用pb對(duì)象的_ptra引用pa對(duì)象,因此需要將a類、b類中的_ptrb和_ptra的類型改為弱智能指針weak_ptr即可,這樣就不會(huì)改變資源的引用計(jì)數(shù),能夠正確釋放資源。

3.4.2 weak_ptr

弱智能指針。不能改變資源的引用計(jì)數(shù)、不能管理對(duì)象生命周期、不能做到資源自動(dòng)釋放、不能創(chuàng)建對(duì)象,也不能訪問資源(因?yàn)閣eak_ptr未提供operator->和operator*運(yùn)算符重載),即不能通過弱智能指針調(diào)用函數(shù)、不能將其解引用。只能從一個(gè)已有的shared_ptr或weak_ptr獲得資源的弱引用。

弱智能指針weak_ptr若想用訪問資源,則需要使用lock方法將其提升為一個(gè)強(qiáng)智能指針,提升失敗則返回nullptr。(提升的情形常使用于多線程環(huán)境,避免無效的訪問,提升程序安全性)

注意:弱智能指針weak_ptr只能觀察資源的狀態(tài),但不能管理資源的生命周期,不會(huì)改變資源的引用計(jì)數(shù),不能控制資源的釋放。

weak_ptr示例:

void test07() {
  shared_ptr boy_sptr(new boy());
  weak_ptr boy_wptr(boy_sptr);
  // boy_wptr->study(); 錯(cuò)誤!無法使用弱智能指針訪問資源
  cout << boy_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù)

  shared_ptr i_sptr(new int(99));
  weak_ptr i_wptr(i_sptr);
  // cout << *i_wptr << endl; 錯(cuò)誤!無法使用弱智能指針訪問資源
  cout << i_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù)

  /*弱智能指針提升為強(qiáng)智能指針*/
  shared_ptr boy_sptr1 = boy_wptr.lock();
  if (boy_sptr1 != nullptr) {
      cout << boy_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2
      boy_sptr1->study(); // 可以調(diào)用
  }

  shared_ptr i_sptr1 = i_wptr.lock();
  if (i_sptr1 != nullptr) {
      cout << i_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2
      cout << *i_sptr1 << endl; // 可以輸出
  }  
}

 

4. 智能指針與多線程訪問共享資源的安全問題

現(xiàn)要實(shí)現(xiàn)主線程創(chuàng)建子線程,讓子線程執(zhí)行打印hello的函數(shù),有如下兩種方式:

方式1:主線程調(diào)用test08函數(shù),在test08函數(shù)中啟動(dòng)子線程執(zhí)行線程函數(shù),如下:

void handler() {
	cout << "hello" << endl;
}

void func() {
	thread t1(handler);
}

int main(int argc, char** argv) {
	func();
	this_thread::sleep_for(chrono::seconds(1));
	system("pause");
	return 0;
}

運(yùn)行報(bào)錯(cuò):

方式2:主線程中直接創(chuàng)建子線程來執(zhí)行線程函數(shù),如下:

void handler() {
	cout << "hello" << endl;
}

int main(int argc, char** argv) {
  thread t1(handler);
	this_thread::sleep_for(chrono::seconds(1));
	system("pause");
	return 0;
}

運(yùn)行結(jié)果:無報(bào)錯(cuò)

上面兩種方式都旨在通過子線程調(diào)用函數(shù)輸出hello,但為什么方式1報(bào)錯(cuò)?很簡單,不再贅述。

回歸本節(jié)標(biāo)題的正題,有如下程序:

class c {
public:
  c() {
      cout << "c()" << endl;
  }

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

  void funcc() {
      cout << "c::funcc()" << endl;
  }
private:

};

/*子線程執(zhí)行函數(shù)*/
void threadhandler(c* c) {
  this_thread::sleep_for(chrono::seconds(1));
  c->funcc();
}

/* 主線程 */
int main(int argc, char** argv) {
  c* c = new c();
  thread t1(threadhandler, c);
  delete c;
	t1.join();
	return 0;
}

運(yùn)行結(jié)果:

結(jié)果顯示c指向的對(duì)象被析構(gòu)了,但是仍然使用該被析構(gòu)的對(duì)象調(diào)用了其中的funcc函數(shù),顯然不合理。

因此在線程函數(shù)中,使用c指針訪問a對(duì)象時(shí),需要觀察a對(duì)象是否存活。

使用弱智能指針weak_ptr接收對(duì)象,訪問對(duì)象之前嘗試提升為強(qiáng)智能指針shared_ptr,提升成功則訪問,否則對(duì)象被析構(gòu)。

情形1:對(duì)象被訪問之前就被析構(gòu)了:

class c {
public:
  c() {
      cout << "c()" << endl;
  }

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

  void funcc() {
      cout << "c::funcc()" << endl;
  }
private:

};

/*子線程執(zhí)行函數(shù)*/
void threadhandler(weak_ptr pw) {  // 引用時(shí)使用弱智能指針
  this_thread::sleep_for(chrono::seconds(1));
  shared_ptr ps = pw.lock();  // 嘗試提升
  if (ps != nullptr) {
      ps->funcc();
  } else {
      cout << "對(duì)象已經(jīng)析構(gòu)!" << endl;
  }
}

/* 主線程 */
int main(int argc, char** argv) {
  {
      shared_ptr p(new c());
      thread t1(threadhandler, weak_ptr(p));
      t1.detach();
  }
  this_thread::sleep_for(chrono::seconds(5));
	return 0;
}

運(yùn)行結(jié)果:

情形2: 對(duì)象訪問完才被析構(gòu):

class c {
public:
  c() {
      cout << "c()" << endl;
  }

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

  void funcc() {
      cout << "c::funcc()" << endl;
  }
private:

};

/*子線程執(zhí)行函數(shù)*/
void threadhandler(weak_ptr pw) {  // 引用時(shí)使用弱智能指針
  this_thread::sleep_for(chrono::seconds(1));
  shared_ptr ps = pw.lock();  // 嘗試提升
  if (ps != nullptr) {
      ps->funcc();
  } else {
      cout << "對(duì)象已經(jīng)析構(gòu)!" << endl;
  }
}

/* 主線程 */
int main(int argc, char** argv) {
  {
      shared_ptr p(new c());
      thread t1(threadhandler, weak_ptr(p));
      t1.detach();
      this_thread::sleep_for(chrono::seconds(5));
  }  
	return 0;
}

運(yùn)行結(jié)果:

可見shared_ptr與weak_ptr結(jié)合使用,能夠較好地保證多線程訪問共享資源的安全。

 

5.智能指針的刪除器deleter

刪除器是智能指針釋放資源的方式,默認(rèn)使用操作符delete來釋放資源。

但并非所有智能指針管理的資源都可通過delete釋放,如數(shù)組、文件資源、數(shù)據(jù)庫連接資源等。

有如下智能指針對(duì)象管理一個(gè)數(shù)組資源:

unique_ptr ptr1(new int[100]);

此時(shí)再用默認(rèn)的刪除器則會(huì)造成資源泄露,因此需要自定義刪除器。

一些為部分自定義刪除器的示例:

/* 方式1:類模板 */
template class mydeleter {
public:
  void operator()(t* ptr) const {
      cout << "數(shù)組自定義刪除器1." << endl;
      delete[] ptr;
  }
};

/* 方式2:函數(shù) */
void mydeleter(int* p) {
  cout << "數(shù)組自定義刪除器2." << endl;
  delete[] p;
}

void test09() {
  unique_ptr                
相關(guān)文章
亚洲国产精品第一区二区,久久免费视频77,99V久久综合狠狠综合久久,国产免费久久九九免费视频