智能指針是可以保存指向動態分配內存的對象指針的類,它可以確保在自身週期結束的時候自動的銷燬動態分配內存的對象,因此它可以有效的防止內存泄露。c++11中提供了三種智能指針,std::weak_ptr、std::shared_ptr和std::unique_ptr。
內存泄露
內存泄漏是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費。在項目開發中,內存泄露其實比較嚴重的問題,最終甚至會導致系統崩潰。
shared_ptr
從名字可以看出來,這是一種可以“共享的指針“的類。它本身包含一個引用計數,初始化時保存指針將引用計數置爲1,通過拷貝構造函數構造另外一個對象時引用計數+1;對一個對象進行賦值時,被賦值的對象的引用計數-1,賦值的對象的引用計數+1。每個對象析構時,引用計數-1,直到引用計數爲0時,會自動的保存的指針。
1.初始化
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> ptr1(new int(10));
std::cout << "use_cout :" << ptr1.use_count() << std::endl; // 1
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "use_cout :" << ptr1.use_count() << std::endl; // 2
std::cout << "use_cout :" << ptr2.use_count() << std::endl; // 2
std::shared_ptr<int> ptr3;
std::cout << "use_cout :" << ptr3.use_count() << std::endl; // 0
ptr3.reset(new int(10));
std::cout << "use_cout :" << ptr3.use_count() << std::endl; // 1
return 0;
}
使用**use_count()**可以輸出引用計數。
2.使用自定義的刪除器
當引用計數爲0時,智能指針會自動析構動態內存,默認的刪除方式是delete。但是有時候我們使用的指針比較複雜一些,例如是動態分配的數組等,這個時候可以採用自定義的deleter。
#include <memory>
#include <iostream>
struct Person {
char* name;
int name_len;
};
void Deleter(Person* person) {
if (nullptr != person) {
if (nullptr != person->name) {
delete[] person->name;
person->name = nullptr;
std::cout << "delete name" << std::endl;
}
delete person;
person = nullptr;
std::cout << "delete person" << std::endl;
}
}
int main() {
std::shared_ptr<Person> ptr1(new Person{nullptr, 0}, &Deleter);
ptr1->name_len = 10000000;
ptr1->name = new char[ptr1->name_len];
return 0;
}
3.循環引用問題
這是智能指針使用時最可能踩坑的地方。
#include <memory>
#include <iostream>
class Parent;
class Son;
class Parent {
public:
~Parent() {
std::cout << "delete parent" << std::endl;
}
std::shared_ptr<Son> ptr;
};
class Son {
public:
~Son() {
std::cout << "delete son" << std::endl;
}
std::shared_ptr<Parent> ptr;
};
int main() {
std::shared_ptr<Parent> parent(new Parent);
std::shared_ptr<Son> son(new Son);
parent->ptr = son;
son->ptr = parent;
return 0;
}
運行之後可以發,沒有任何打印,也就是說parent和son都沒有析構掉,存在內存泄露!那爲什麼沒有析構掉呢?莫非是引用計數最後不會減到0?我們添加輸出信息。
int main() {
std::shared_ptr<Parent> parent(new Parent);
std::shared_ptr<Son> son(new Son);
std::cout << parent.use_count() << std::endl; // 1
std::cout << son.use_count() << std::endl; // 1
parent->ptr = son;
std::cout << parent.use_count() << std::endl; // 1
std::cout << son.use_count() << std::endl; // 2
son->ptr = parent;
std::cout << parent.use_count() << std::endl; // 2
std::cout << son.use_count() << std::endl; // 2
return 0;
}
從輸出我們可以看到,由於循環引用,導致parent和son的引用計數都爲2,生命週期結束時,各自減1之後不爲0,因此不會銷燬而導致內存泄露。通過weak_ptr可解決這個問題。
weak_ptr
名字看起來就比較弱,沒有重載*和->操作符,不共享指針,不會使引用計數加1。它主要是爲了監視shared_ptr的生命週期。
1.初始化
int main() {
std::shared_ptr<Person> sh_ptr(new Person{nullptr, 10});
std::weak_ptr<Person> w_ptr(sh_ptr);
std::cout << sh_ptr.use_count() << std::endl;
std::cout << w_ptr.use_count() << std::endl;
return 0;
}
2.監視shared_ptr
int main() {
std::weak_ptr<Person> w_ptr;
{
std::shared_ptr<Person> sh_ptr(new Person{nullptr, 10});
w_ptr = sh_ptr;
std::cout << sh_ptr.use_count() << std::endl; // 1
std::cout << w_ptr.use_count() << std::endl; // 1
if (w_ptr.expired()) {
std::cout << "expired" << std::endl;
} else {
std::cout << w_ptr.lock()->name_len << std::endl; // 10
}
}
std::cout << w_ptr.use_count() << std::endl; // 0
if (w_ptr.expired()) {
std::cout << "expired" << std::endl; // expired
} else {
std::cout << w_ptr.lock()->name_len << std::endl;
}
return 0;
}
expired()用於判斷所監視shared_ptr的管理的內存是否釋放掉;lock()獲取被監視的shared_ptr。
3.解決循環引用問題
class Parent {
public:
~Parent() {
std::cout << "delete parent" << std::endl; // delete parent
}
std::shared_ptr<Son> ptr;
};
class Son {
public:
~Son() {
std::cout << "delete son" << std::endl; // delete son
}
std::weak_ptr<Parent> ptr;
};
int main() {
std::shared_ptr<Parent> parent(new Parent);
std::shared_ptr<Son> son(new Son);
std::cout << parent.use_count() << std::endl; // 1
std::cout << son.use_count() << std::endl; // 1
parent->ptr = son;
std::cout << parent.use_count() << std::endl; // 1
std::cout << son.use_count() << std::endl; // 2
son->ptr = parent;
std::cout << parent.use_count() << std::endl; // 1
std::cout << son.use_count() << std::endl; // 2
return 0;
}
將son中的成員ptr改成了weak_ptr,這樣在son->ptr = parent時,就不會增加引用計數,因此parent的引用仍然是1。這樣在parant生命週期結束時,parent的引用計數減1成爲0,這樣保存的Parent指針會被析構,首先打印出delete parent。接着由於Parent析構,內部元素son的引用計數減1成爲1,生命週期結束後,son的引用計數再減1變爲0,也會被析構掉。
unique_ptr
獨享指針,即不能將unique_ptr賦值給另外一個unique_ptr。例如下面的使用是錯誤的:
1.基本使用
std::unique_ptr<int> u_ptr1(new int);
std::unique_ptr<int> u_ptr2(u_ptr2);
編譯錯誤:
/usr/include/c++/4.8/bits/unique_ptr.h:273:7: error: declared here
unique_ptr(const unique_ptr&) = delete;
但是可以通過std::move來轉移
std::unique_ptr<int> u_ptr1(new int);
std::unique_ptr<int> u_ptr2 = std::move(u_ptr1);
2.使用自定義的刪除器
struct Deleter {
void operator()(Person* person) {
if (nullptr != person) {
if (nullptr != person->name) {
delete[] person->name;
person->name = nullptr;
std::cout << "delete name" << std::endl;
}
delete person;
person = nullptr;
std::cout << "delete person" << std::endl;
}
}
};
int main() {
std::unique_ptr<Person, Deleter> ptr1(new Person{nullptr, 0});
ptr1->name_len = 10000000;
ptr1->name = new char[ptr1->name_len];
return 0;
}
可以看到unique_ptr與shared_ptr添加deleter還是有區別的。unique_ptr出來像上面那樣添加deleter,還可以類似下面這樣通過lambda表達式添加deleter:
std::unique_ptr<Person, void(*)(Person *)> ptr2(new Person{nullptr, 0},
[](Person* person) {
if (nullptr != person) {
if (nullptr != person->name) {
delete[] person->name;
person->name = nullptr;
std::cout << "delete name" << std::endl;
}
delete person;
person = nullptr;
std::cout << "delete person" << std::endl;
}});
下面介紹如何簡單的實現shared_ptr和unique_ptr。