C++學(xué)習(xí)之智能指針中的unique_ptr與shared_ptr

 

為什么需要智能指針

在上一講《01 c++如何進(jìn)行內(nèi)存資源管理》中,提到了對于堆上的內(nèi)存資源,需要我們手動分配和釋放。管理這些資源是個技術(shù)活,一不小心,就會導(dǎo)致內(nèi)存泄漏。

我們再給兩段代碼,切身體驗(yàn)下原生指針管理內(nèi)存的噩夢。

void foo(int n) {
  int* ptr = new int(42);
  ...
  if (n > 5) {
	    return;
  }
  ...
  delete ptr;
}
void other_fn(int* ptr) {
	...
};
void bar() {
  int* ptr = new int(42);
  other_fn(ptr);
  // ptr == ?
}

在foo函數(shù)中,如果入?yún)> 5, 則會導(dǎo)致指針ptr的內(nèi)存未被正確釋放,從而導(dǎo)致內(nèi)存泄漏。

在bar函數(shù)中,我們將指針ptr傳遞給了另外一個函數(shù)other_fn,我們無法確定other_fn有沒有釋放ptr內(nèi)存,如果被釋放了,那ptr將成為一個懸空指針,bar在后續(xù)還繼續(xù)訪問它,會引發(fā)未定義行為,可能導(dǎo)致程序崩潰。

上面由于原生指針使用不當(dāng)導(dǎo)致的內(nèi)存泄漏、懸空指針問題都可以通過智能指針來輕松避免。

c++智能指針是一種用于管理動態(tài)分配內(nèi)存的指針類。基于raii設(shè)計理念,通過封裝原生指針實(shí)現(xiàn)的。可以在資源(原生指針對應(yīng)的對象)生命周期結(jié)束時自動釋放內(nèi)存。

c++標(biāo)準(zhǔn)庫中,提供了兩種最常見的智能指針類型,分別是std::unique_ptr和std::shared_ptr。
接下來我們分別詳細(xì)展開介紹。

 

吃獨(dú)食的unique_ptr

std::unique_ptr是 c++11 引入的智能指針,用于管理動態(tài)分配的內(nèi)存。每個std::unique_ptr實(shí)例都擁有對其所包含對象的唯一所有權(quán),并在其生命周期結(jié)束時自動釋放對象。

創(chuàng)建unique_ptr對象

我們可以std::unique_ptr的構(gòu)造函數(shù)或std::make_unique函數(shù)(c++14支持)來創(chuàng)建一個unique_ptr對象,在超出作用域時,會自動釋放所管理的對象內(nèi)存。示例代碼如下:

#include  #include  class myclass {
public:
  myclass() {
      std::cout << "myclass constructed" << std::endl;
  }
  ~myclass() {
      std::cout << "myclass destroyed" << std::endl;
  }
};
int main() {
	std::unique_ptr ptr1(new myclass);
	// c++14開始支持std::make_unique
  std::unique_ptr ptr2 = std::make_unique(10);
  return 0;
}

代碼輸出:

myclass constructed
myclass destroyed

訪問所管理的對象

我們可以像使用原生指針的方式一樣,訪問unique_ptr所指向的對象。也可以通過get函數(shù)獲取到原生指針。

myclass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 輸出 10

釋放/重置所管理的對象

使用reset函數(shù)可以釋放unique_ptr所管理的對象,并將其指針重置為nullptr或指定的新指針。reset`大概實(shí)現(xiàn)原理如下

template 
void unique_ptr::reset(pointer ptr = pointer()) noexcept { 
	// 釋放指針指向的對象
	delete ptr_; 
	// 重置指針
	ptr_ = ptr;
}

該函數(shù)主要完成兩件事:

  • 釋放std::unique_ptr所管理的對象,以避免內(nèi)存泄漏。
  • 將std::unique_ptr重置為nullptr或管理另一個對象。

code show time:

#include  #include  class myclass {
public:
  myclass() {
      std::cout << "myclass constructed" << std::endl;
  }
  ~myclass() {
      std::cout << "myclass destroyed" << std::endl;
  }
};
int main() {
  // 創(chuàng)建一個 std::unique_ptr 對象,指向一個 myclass 對象
  std::unique_ptr ptr(new myclass);
  // 調(diào)用 reset,將 std::unique_ptr 重置為管理另一個 myclass 對象
  ptr.reset(new myclass);
  return;
}

移動所有權(quán)

一個對象資源只能同時被一個unique_ptr管理。當(dāng)嘗試把一個unique_ptr直接賦值給另外一個unique_ptr會編譯報錯。

#include  int main() {
  std::unique_ptr p1 = std::make_unique(42);
  std::unique_ptr p2 = p1; // 編譯報錯
  return 0;
}

為了把一個std::unique_ptr對象的所有權(quán)移動到另一個對象中,我們必須配合std::move移動函數(shù)。

#include  #include  int main() {
  std::unique_ptr p1 = std::make_unique(42);
  std::unique_ptr p2 = std::move(p1); // ok
  std::cout << *p2 << std::endl; // 42
  std::cout << (p1.get() == nullptr) << std::endl; // true
  return 0;
}

這個例子中, 我們把p1通過std::move將其管理對象的所有權(quán)轉(zhuǎn)移給了p2, 此時p2接管了對象,而p1不再擁有管理對象的所有權(quán),即無法再操作到該對象了。

 

樂于分享的shared_ptr

shared_ptr是c++11提供的另外一種常見的智能指針,與unique_ptr獨(dú)占對象方式不同,shared_ptr是一種共享式智能指針,允許多個shared_ptr指針共同擁有同一個對象,采用引用計數(shù)的方式來管理對象的生命周期。當(dāng)所有的shared_ptr對象都銷毀時,才會自動釋放所管理的對象。

創(chuàng)建shared_ptr對象

同樣的,c++也提供了std::shared_ptr構(gòu)造函數(shù)和std::make_shared函數(shù)來創(chuàng)建std::shared_ptr對象。

#include  int main() {
	std::shared_ptr p1(new int(10));
	std::shared_ptr p2 = std::make_shared(20);
	return;
}

多個shared_ptr共享一個對象

可以通過賦值操作實(shí)現(xiàn)多個shared_ptr共享一個資源對象,例如

std::shared_ptrp3 = p2;

shared_ptr采用引用計數(shù)的方式管理資源對象的生命周期,通過分配一個額外內(nèi)存當(dāng)計數(shù)器。

當(dāng)一個新的shared_ptr被創(chuàng)建時,它對應(yīng)的計數(shù)器被初始化為1。每當(dāng)賦值給另外一個shared_ptr共享同一個對象時,計數(shù)器值會加1。當(dāng)某個shared_ptr被銷毀時,計數(shù)值會減1,當(dāng)計數(shù)值變?yōu)?時,說明沒有任何shared_ptr引用這個對象,會將對象進(jìn)行回收。

c++提供了use_count函數(shù)來獲取std::shared_ptr所管理對象的引用計數(shù),例如

std::cout << "p1 use count: " << p1.use_count() << std::endl;

釋放/重置所管理的對象

可以使用reset函數(shù)來釋放/重置shared_ptr所管理的對象。大概實(shí)現(xiàn)原理如下(不考慮并發(fā)場景)

void reset(t* ptr = nullptr) {
	if (ref_count != nullptr) { 
		(*ref_count)--;
		if (*ref_count == 0) { 
			delete data; 
			delete ref_count; 
		} 
	} 
	data = ptr; 
	ref_count = (data == nullptr) ? nullptr : new size_t(1); 
}

data指針來存儲管理的資源,指針ref_count來存儲計數(shù)器的值。

在 reset 方法中,需要減少計數(shù)器的值,如果計數(shù)器減少后為 0,則需要釋放管理的資源,如果減少后不為0,則不會釋放之前的資源對象。

如果reset指定了新的資源指針,則需要重新設(shè)置 data 和 ref_count,并將計數(shù)器初始化為 1。否則,將計數(shù)器指針置為nullptr

shared_ptr使用注意事項(xiàng)

避免循環(huán)引用

由于shared_ptr具有共享同一個資源對象的能力,因此容易出現(xiàn)循環(huán)引用的情況。例如:

struct node { 
	std::shared_ptr next; 
};
int main() {
	std::shared_ptr node1(new node);
	std::shared_ptr node2(new node); 
	node1->next = node2; 
	node2->next = node1;
}

在上述代碼中,node1和node2互相引用,在析構(gòu)時會發(fā)現(xiàn)計數(shù)器的值不為0,不會釋放所管理的對象,產(chǎn)生內(nèi)存泄漏。

為了避免循環(huán)引用,可以將其中一個指針改為weak_ptr類型。weak_ptr也是一種智能指針,通常配合shared_ptr一起使用。

weak_ptr是一種弱引用,不對所指向的對象進(jìn)行計數(shù)引用,也就是說,不增加所指對象的引用計數(shù)。當(dāng)所有的shared_ptr都析構(gòu)了,不再指向該資源時,該資源會被銷毀,同時對應(yīng)的所有weak_ptr都會變成nullptr,這時我們就可以利用expired()方法來判斷這個weak_ptr是否已經(jīng)失效。

我們可以通過weak_ptr的lock()方法來獲得一個指向共享對象的shared_ptr。如果weak_ptr已經(jīng)失效,lock()方法將返回一個空的shared_ptr。

下面是weak_ptr的基本使用示例:

#include  #include  int main() {
  std::shared_ptr sp = std::make_shared(42);
  // 創(chuàng)建shared_ptr對應(yīng)的weak_ptr指針
  std::weak_ptr wp(sp);
	// 通過lock創(chuàng)建一個對應(yīng)的shared_ptr
  if (auto p = wp.lock()) {
      std::cout << "shared_ptr value: " << *p << std::endl;
      std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
  } else {
      std::cout << "wp is expired" << std::endl;
  }
	// 釋放shared_ptr指向的資源,此時weak_ptr失效
  sp.reset();
  std::cout << "wp is expired: " <<  wp.expired() << std::endl;
  return 0;
}

代碼輸出如下

shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1

回到shared_ptr的循環(huán)引用問題,利用weak_ptr不會增加shared_ptr的引用計數(shù)的特點(diǎn),我們將node.next的類型改為weak_ptr, 避免node1和node2互相循環(huán)引用。修改后代碼如下

```cpp
struct node { 
	std::weak_ptr next; 
};
int main() {
	std::shared_ptr node1(new node);
	std::shared_ptr node2(new node); 
	node1->next = std::weak_ptr(node2); 
	node2->next = std::weak_ptr(node1); ;
}

避免裸指針與shared_ptr混用

先看看以下代碼

int* q = new int(9);
{
	std::shared_ptr p(new int(10));
	...
	q = p.get();
}
std::cout << *q << std::endl;

get函數(shù)返回std::shared_ptr所持有的指針,但是不會增加引用計數(shù)。所以在shared_ptr析構(gòu)時,將該指針指向的對象給釋放掉了,導(dǎo)致指針q變成一個懸空指針。

避免一個原始指針初始化多個shared_ptr

int* p = new int(10);
std::shared_ptr ptr1(p);
// error: 兩個shared_ptr指向同一個資源,會導(dǎo)致重復(fù)釋放
std::shared_ptr ptr2(p);

 

總結(jié)

避免手動管理內(nèi)存帶來的繁瑣和容易出錯的問題。我們今天介紹了三種智能指針:unique_ptr、shared_ptr和weak_ptr。
每種智能指針都有各自的使用場景。unique_ptr用于管理獨(dú)占式所有權(quán)的對象,它不能拷貝但可以移動,是最輕量級和最快的智能指針。shared_ptr用于管理多個對象共享所有權(quán)的情況,它可以拷貝和移動。weak_ptr則是用來解決shared_ptr循環(huán)引用的問題。

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