文章目录
- 智能指针概念
- 内存泄漏的危害
- RAII与智能指针
- 智能指针的赋值
- auto_ptr 管理权转移
- auto_ptr 的对象悬空问题
- unique_ptr 防拷贝
- unique_ptr 简单实现
- shared_ptr 引用计数
- shared_ptr 简单实现
- shared_ptr 的循环引用问题与 weak_ptr
- 智能指针的自定义删除器
智能指针概念
智能指针是C++标准库中用于管理动态分配内存的一种对象;
通过封装原生指针为用户自动管理内存的生命周期从而减少内存泄漏和悬空指针的问题;
在C++中动态分配内存是通过new
运算符在堆空间中进行开辟的,使用后需要手动调用delete
来释放内存;
如果程序因为错误或是用户忽略而导致释放内存就会造成内存泄漏,这种未delete
的原因可能是异常导致的,也可能是用户忽略造成的;
class Data {
public:
Data() { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
};
double Div(double num1, double num2) {
if (num2 == 0) throw "Division by zero error";
return num1 / num2;
}
void func() {
Data *d1 = new Data();
Data *d2 = new Data();
cout << Div(10, 0) << endl;
delete d1;
delete d2;
}
int main() {
try {
func();
} catch (const char *abnor) {
cout << "main get a abnormal : " << abnor << endl;
}
return 0;
}
在这个例子中定义了一个Data
类,其中该类存在构造和析构函数用来判断构造和析构是否被调用;
定义了一个除法函数Div
,当num2
为0
时则抛出一个异常并在main
函数中处理;
func
函数调用Div
且在调用该函数前用new
在堆上开了两个Data
类型的对象,main
函数调用func
函数,运行结果如下:
$ ./test
Data()
Data()
main get a abnormal : Division by zero error
在这个例子中Data
类的析构函数并未被调用,意味着出现了内存泄漏的问题;
同时如果用户多次释放同一块内存则可能导致未定义的问题(悬空指针,当一块空间被delete
后所指向的空间将是一个无效的空间,如果对一个悬空指针进行释放则导致未定义问题);
int main() {
int *a = new int;
int *b = a;
delete a;
delete b;
return 0;
}
/*
运行结果为:
$ ./test
*** Error in `./test': double free or corruption (fasttop): 0x00000000012dec20 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7fe3332b2329]
...
...
Aborted
*/
在这个例子中new
了一个int
类型的对象并用指针a
进行接收,并声明了一个指针将a
所指向的空间赋值给b
;
当对a
进行delete
时a
与b
所指向的空间就已经释放,这时指针a
与b
都为悬空指针,当再次对已经释放的空间进行delete
后则出现未定义问题;
而智能指针可解决这一系列问题以防止内存泄漏;
class Data {
public:
Data() { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
};
double Div(double num1, double num2) {
if (num2 == 0) throw "Division by zero error";
return num1 / num2;
}
void func() {
auto_ptr<Data> d1(new Data); // 智能指针
auto_ptr<Data> d2(new Data);
cout << Div(10, 0) << endl;
}
int main() {
try {
func();
} catch (const char *abnor) {
cout << "main get a abnormal : " << abnor << endl;
}
return 0;
}
/*
运行结果为:
$ ./test
Data()
Data()
~Data()
~Data()
main get a abnormal : Division by zero error
*/
在这个例子中使用了auto_ptr
智能指针来封装原生指针,当智能指针的生命周期结束时将自动调用析构函数来释放资源以解决上述的内存泄漏问题,Data
类的析构函数被调用;
内存泄漏的危害
内存泄漏是指程序在动态分配内存后 没有释放或无法释放已分配的内存块 这会导致内存永远无法被再次使用 内存泄漏的危害主要包括以下几点;
-
资源耗尽
内存泄漏会逐渐减少系统中的可用内存,最终可能导致系统无法再分配足够的内存来满足新的请求,这会导致程序或系统崩溃;
-
性能下降
内存泄漏会使内存分配效率降低;
系统在分配内存时需要花费更多时间来搜索可用的内存块,从而增加分配和释放内存的时间开销从而降低整体性能;
-
程序不稳定
随着内存泄漏的积累,程序会消耗越来越多的内存;
这会导致程序运行变得不稳定,甚至最终崩溃;
-
数据丢失或损坏
如果内存泄漏涉及到重要的数据结构或对象;
这些数据可能因为内存泄漏而被破坏或丢失;
这会导致程序运行时出现意外的行为或结果;
-
难以调试
内存泄漏通常不会立即显现问题而是逐渐积累;
因此当问题出现时,定位和修复内存泄漏可能非常困难,需要使用专门的内存分析工具来检测和排查;
-
安全风险
在某些情况下,内存泄漏可能导致安全漏洞;
如果未释放的内存中包含敏感信息,如密码或加密密钥,这些信息可能会泄露给未经授权的第三方;
RAII与智能指针
RAII
(Resource Acquisition Is Initialization)旨在通过对象的生命周期来管理资源,确保在对象被创建时获取资源并在对象被销毁时释放资源;
RAII
使用构造函数和析构函数的特性来自动管理资源以避免资源泄露问题;
-
资源获取即初始化
对象在构造时获得资源(文件,文件句柄,网络连接等),并在析构时释放资源,这表示对象的构造和析构会自动管理他所拥有的资源;
-
对象的生命周期管理
大部分大于旋风在离开作用域时会自动清理(调用析构函数),使通过
RAII
管理的资源也会在对象消亡时会被自动释放; -
异常安全
RAII
能够保障异常情况下资源的正确释放;即使在构造或操作过程中发生异常,析构函数也会被调用从而正确释放资源;
智能指针是RAII
的一种实现,主要用于动态内存管理;
template <class T>
class smartPoint {
public:
// 完成RAII
smartPoint(T *ptr) : ptr_(ptr) {}
~smartPoint() {
delete ptr_;
ptr_ = nullptr;
std::cout << " ~smartPoint()" << std::endl;
}
T &operator*() { return *ptr_; }
T *operator->() { return ptr_; }
private:
T *ptr_;
};
这是一个简单的智能指针的实现,其中构造函数和析构函数完成了RAII
,即对象创建时被初始化,对象销毁时清理资源;
其中operator*()
与operator->()
对*
与->
的重载使其能够像指针一样进行解引用;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
{ // 可使用 {} 来控制对象的生命周期
smartPoint<Data> d1(new Data(10));
cout << "对象创建即初始化" << endl;
cout << "d1 -> data_ :" << (d1->data_ += 10)
<< endl; // 像指针一样进行解引用
}
cout << "对象清理结束" << endl;
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
对象创建即初始化
d1 -> data_ :20
~Data()
~smartPoint()
对象清理结束
*/
智能指针的赋值
智能指针必须像指针一样进行赋值;
上文中的智能指针的实现的赋值采用的是浅拷贝,即将一个智能指针所指向的空间地址赋值给另一个智能指针对象,即两个智能指针都指向同一块空间;
当同一块空间被连续delete
则会出现未定义行为;
template <class T>
class smartPoint {
public:
smartPoint(T *ptr) : ptr_(ptr) {}
~smartPoint() {
delete ptr_;
ptr_ = nullptr;
std::cout << " ~smartPoint()" << std::endl;
}
T &operator*() { return *ptr_; }
T *operator->() { return ptr_; }
private:
T *ptr_;
};
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() { smartPoint<Data> d1(new Data(10));
smartPoint<Data> d2 = d1;
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
~Data()
~smartPoint()
~Data()
*** Error in `./test': double free or corruption (fasttop): 0x0000000000abdc20 ***
======= Backtrace: =========
...
...
Aborted
*/
在这个例子中智能指针进行了赋值,导致对同一块空间连续delete
导致崩溃;
解决智能指针赋值问题有几种方法:
-
管理权转移
管理权转移是指指针的所有权在指针之间转移,以确保某一时刻只有一个智能指针拥有资源;
-
防拷贝
防拷贝机制禁止智能指针的拷贝构造和拷贝赋值以确保资源的唯一性和不可复制性;
可直接使用
deleted
关键字禁用拷贝构造和赋值重载; -
引用计数
引用计数是一种共享所有权的机制,允许多个指针共享同一个资源(对象);
当每创建一个新的智能指针指向统一资源时引用计数增加;
当指针被销毁时引用计数减少,当引用计数减到
0
时资源才会实际被释放;
auto_ptr 管理权转移
auto_ptr
是C++98
标准库引入的一种智能指针,主要目的用于自动管理动态分配的内存,以避免内存泄漏;
auto_ptr
所采用的智能指针赋值的解决方案及采用管理权转移的方式;
template <class T>
class myauto_ptr {
public:
myauto_ptr(T *ptr) : ptr_(ptr) {}
~myauto_ptr() {
// 确保在指针为非空状态时进行资源释放
if (ptr_) {
delete ptr_;
ptr_ = nullptr;
std::cout << " ~myauto_ptr()" << std::endl;
}
}
myauto_ptr(myauto_ptr<T> &ptr) : ptr_(ptr.ptr_) {
// 构造函数进行管理权转移
ptr.ptr_ = nullptr;
}
myauto_ptr<T> &operator=(myauto_ptr<T> &ptr) {
// 赋值重载进行管理权转移
ptr_ = ptr.ptr_;
ptr.ptr_ = nullptr;
return *this;
}
T &operator*() { return *ptr_; }
T *operator->() { return ptr_; }
private:
T *ptr_;
};
在这个例子中模拟了一个auto_ptr
指针的实现;
其中当进行拷贝或者赋值时进行管理权的转移,即将原本的智能指针所指向的空间的管理权交给新的智能指针对象,而原本的智能指针对象的指针设为空;
析构函数为当智能指针所指向的空间不为空nullptr
时则对资源进行清理;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
myauto_ptr<Data> d1(new Data(10));
myauto_ptr<Data> d2 = d1; // 进行赋值
}
int main() {
func();
return 0;
}
/*
运行结果:
$ ./test
Data()
~Data()
~myauto_ptr()
*/
这个例子使用了智能指针d1
封装原生指针并且再创建了一个智能指针对象d2
将d1
赋值给d2
,其中空间的管理权会转移给d2
;
当智能指针的生命周期结束时将调用析构函数,析构函数为当指向空间不为空时对空间进行delete
并将其指针置为nullptr
;
auto_ptr 的对象悬空问题
auto_ptr
智能指针的对象悬空问题指当一个auto_ptr
智能指针对象赋值给另一个auto_ptr
智能指针时进行指针的管理权转移后当前的auto_ptr
智能指针对象将处于一个对象悬空的状态,此时再次对该智能指针对象进行修改等操作将会导致对空指针的非法解引用导致出现未定义行为;
template <class T>
class myauto_ptr {
public:
myauto_ptr(T *ptr) : ptr_(ptr) {}
~myauto_ptr() {
// 确保在指针为非空状态时进行资源释放
if (ptr_) {
delete ptr_;
ptr_ = nullptr;
std::cout << " ~myauto_ptr()" << std::endl;
}
}
myauto_ptr(myauto_ptr<T> &ptr) : ptr_(ptr.ptr_) {
// 构造函数进行管理权转移
ptr.ptr_ = nullptr;
}
myauto_ptr<T> &operator=(myauto_ptr<T> &ptr) {
// 赋值重载进行管理权转移
ptr_ = ptr.ptr_;
ptr.ptr_ = nullptr;
return *this;
}
T &operator*() { return *ptr_; }
T *operator->() { return ptr_; }
private:
T *ptr_;
};
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
myauto_ptr<Data> d1(new Data(10));
myauto_ptr<Data> d2 = d1; // 进行赋值
++d2->data_;
++d1->data_; // 对悬空对象进行修改操作
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
Segmentation fault
*/
在这个例子中使用了自定义的myauto_ptr
封装了自定义的Data
类指针为a1
与a2
,将a1
赋值给a2
并对a1
进行修改操作,最终运行崩溃;
在使用库中的auto_ptr
也存在相同的问题;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
auto_ptr<Data> d1(new Data(10));
auto_ptr<Data> d2 = d1; // 进行赋值
++d2->data_;
++d1->data_; // 对悬空对象进行修改操作
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
Segmentation fault
*/
auto_ptr
是C++98
引入的一种智能指针,旨在简化动态内存管理;
而由于其设计上的一些缺陷特别是拷贝语义的不直观和不适用于STL
容器等问题在C++11
后被弃用;
unique_ptr 防拷贝
unique_ptr
是C++11
标准引入的一种智能指针,旨在提供独占资源所有权,无法被复制只能通过移动;
这一特性确保了在任何时刻某个动态分配的资源只由一个unique_ptr
管理从而避免了重复删除和潜在的资源泄露问题;
-
防拷贝
class Data { public: Data(int data = 0) : data_(data) { cout << "Data()" << endl; } ~Data() { cout << "~Data()" << endl; } public: int data_; }; void func() { unique_ptr<Data> d1(new Data(10)); unique_ptr<Data> d2 = d1; // 进行赋值 } int main() { func(); return 0; } /* 编译结果为: $ make g++ -o test Main.cpp -g -Wall -std=c++11 Main.cpp: In function ‘void func()’: Main.cpp:19:25: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Data; _Dp = std::default_delete<Data>]’ unique_ptr<Data> d2 = d1; // 进行赋值 ^ In file included from /usr/include/c++/4.8.2/memory:81:0, from Main.cpp:2: /usr/include/c++/4.8.2/bits/unique_ptr.h:273:7: error: declared here unique_ptr(const unique_ptr&) = delete; ^ make: *** [test] Error 1 */
在这个例子中使用标准库中的
unique_ptr
对象d1
封装自定义类型Data
的指针,并试图使用拷贝构造来将其赋给d2
,最终编译结果为编译报错; -
移动拷贝
class Data { public: Data(int data = 0) : data_(data) { cout << "Data()" << endl; } ~Data() { cout << "~Data()" << endl; } public: int data_; }; void func() { unique_ptr<Data> d1(new Data(10)); unique_ptr<Data> d2 = move(d1); // 移动构造 cout << ++d2->data_ << endl; } int main() { func(); return 0; } /* 运行结果为: $ ./test Data() 11 ~Data() */
在这个例子中使用了标准库中的
unique_ptr
对象d1
封装了自定义类型Data
对象指针,并使用了move()
移动语句将其移动拷贝至d2
中;由于移动语义语义明确即使用移动的方式进行管理权转移,本身使用
move()
进行移动任意自定义类型时对应的资源也会被转移,即对转移后的资源进行修改本质上就是一种错误的行为;
unique_ptr 简单实现
// 类模板 myunique_ptr 的定义
template <class T>
class myunique_ptr {
public:
// 默认构造函数,初始化 ptr_ 为 nullptr
myunique_ptr() : ptr_(nullptr) {}
// 带指针的构造函数,用给定指针初始化 ptr_
myunique_ptr(T *ptr) : ptr_(ptr) {}
// 析构函数,负责释放 ptr_ 指向的资源
~myunique_ptr() {
if (ptr_) {
delete ptr_;
ptr_ = nullptr;
std::cout << "~myunique_ptr()" << std::endl;
}
}
// 删除拷贝构造函数,防止拷贝 myunique_ptr 对象
myunique_ptr(const myunique_ptr<T> &ap) = delete;
// 删除拷贝赋值运算符,防止拷贝赋值 myunique_ptr 对象
myunique_ptr<T> &operator=(const myunique_ptr<T> &) = delete;
// 移动构造函数,将资源从另一个 myunique_ptr 对象转移过来
myunique_ptr(myunique_ptr &&ap) noexcept : ptr_(ap.ptr_) {
ap.ptr_ = nullptr;
}
// 移动赋值运算符,将资源从另一个 myunique_ptr 对象转移过来
myunique_ptr<T> &operator=(myunique_ptr &&ap) noexcept {
if (this != &ap) {
if (ptr_) {
delete ptr_;
}
ptr_ = ap.ptr_;
ap.ptr_ = nullptr;
}
return *this;
}
// 重置指针,释放当前持有的资源并指向新的资源
void reset(T *ptr = nullptr) {
if (ptr_) {
delete ptr_;
}
ptr_ = ptr;
}
// 释放控制权,返回原始指针,并将内部指针置空
T *release() {
T *tmp = ptr_;
ptr_ = nullptr;
return tmp;
}
// 解引用运算符,返回指向的对象
T &operator*() { return *ptr_; }
// 成员访问运算符,返回原始指针
T *operator->() { return ptr_; }
private:
T *ptr_; // 内部指针,指向管理的资源
};
这是一个unique_ptr
的简单实现;
-
构造函数
默认构造函数,初始化指针为
nullptr
,支持构造一个指向空的unique_ptr
智能指针对象;带参构造函数为使用传入的指针初始化智能指针内部的指针;
-
析构函数
在对象销毁时检查内部指针是否为空,如果不为空则释放资源并输出析构信息;
-
防拷贝机制
通过
delete
关键字修饰拷贝构造和拷贝赋值运算符防止对象的拷贝从而保证该智能指针具有唯一权; -
移动语义
-
移动构造函数
移动构造函数将源对象的指针转移到新的对象,并将源智能指针对象的指针置空;
-
移动赋值
通过检查自赋值后释放当前对象的资源并转移源智能指针对象的指针,将源智能指针对象指针置空;
-
-
成员函数
-
reset()
重置内部指针,释放当前持有的资源并指向新的资源(默认为
nullptr
); -
release()
释放控制权,返回原始指针并将内部指针置空;
-
解引用和成员访问运算符重载
提供与原始指针相同的访问接口;
-
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
// 创建一个 myunique_ptr<Data> 对象 d1,管理一个新的 Data 对象,初始值为 10
myunique_ptr<Data> d1(new Data(10));
// 使用移动构造函数将 d1 的所有权转移到 d2,d1 现在为空
myunique_ptr<Data> d2 = move(d1); // 移动构造
// 创建一个新的 Data 对象,初始值为 50,并将其指针赋给 d3
Data* d3 = new Data(50);
// 释放 d2 管理的资源,并返回原始指针,赋值给 d4
Data* d4 = d2.release();
// 输出 d4 指向的 Data 对象的值
cout << "d4: " << d4->data_ << endl;
// 手动删除 d4 指向的 Data 对象,释放资源
delete d4;
// 使用 reset() 方法将 d1 重置为管理新的 Data 对象 d3
d1.reset(d3);
// 输出 d1 指向的 Data 对象的值
cout << "d1: " << d1->data_ << endl;
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
Data()
d4: 10
~Data()
d1: 50
~Data()
~myunique_ptr()
*/
shared_ptr 引用计数
shared_ptr
是C++11
提供的一种智能指针类型,用于管理共享资源的生命周期;
通过引用计数机制确保资源在最后一个引用它的shared_ptr
对象被销毁时才会释放;
-
引用计数
shared_ptr
内部维护一个引用计数用于记录有多少个shared_ptr
实例共享同一个资源;这个引用计数是单独开的空间(
new
)确保对应的指向同一个资源的shared_ptr
实例能够访问这个引用计数;每次拷贝构造或是赋值时引用计数将会增加,销毁一个
shared_ptr
智能指针实例或将其reset
时引用计数减少,当引用计数减为零时所管理的资源被释放; -
线程安全
引用计数的增加和减少是线程安全的,但对同一个
shared_ptr
对象的读写操作则需要额外增加同步机制; -
类型安全
shared_ptr
是类型安全的,能够确保指针类型正确以避免常见的指针类型转换错误;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
shared_ptr<Data> d1(new Data(10));
shared_ptr<Data> d2 = d1; // 拷贝构造 此时d1 与 d2都指向同一个资源
cout << "d1: " << d1->data_ << endl;
cout << "d2: " << d2->data_ << endl;
}
int main() {
func();
return 0;
}
/*
运行结果:
$ ./test
Data()
d1: 10
d2: 10
~Data()
*/
在这个例子中使用Data
类型指针实例化一个标准库中的shared_ptr
智能指针对象,而后利用拷贝构造使得两个智能指针对象d1
与d2
指向同一个空间,并对其进行打印;
最终结果为d1
与d2
中的Data
中data_
成员相同,且两个智能指针对象只有最后的智能指针对象在调用析构函数后对资源进行释放;
shared_ptr 简单实现
template <class T>
class myshared_ptr {
public:
// 构造函数,用于初始化智能指针。接受一个原始指针 ptr。
myshared_ptr(T *ptr) : ptr_(ptr) {}
// 析构函数。在对象被销毁时调用。
~myshared_ptr() {
// 检查 pcount_ 是否非空,减少引用计数。
// 如果引用计数为 0,则释放资源,包括底层指针和引用计数器。
if (pcount_ && --(*pcount_) == 0) {
delete pcount_;
delete ptr_;
pcount_ = nullptr; // 防止悬挂指针
ptr_ = nullptr;
}
}
// 拷贝构造函数。创建一个新的 myshared_ptr 对象,并共享传入对象的资源。
myshared_ptr(myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) {
++(*pcount_); // 增加引用计数,因为增加了一个新的共享对象。
}
// 拷贝赋值运算符。用于用另一个智能指针赋值给当前智能指针。
myshared_ptr<T> operator=(const myshared_ptr<T> &sp) {
if (&sp != this) { // 检查自我赋值
// 减少当前对象的引用计数,如果为零则释放资源。
if (--(*pcount_) == 0) {
delete ptr_;
delete pcount_;
}
// 将右侧对象的数据复制给当前对象,并增加引用计数。
ptr_ = sp.ptr_;
pcount_ = sp.pcount_;
++(*pcount_);
}
return *this; // 返回当前对象以支持链式赋值
}
// 移动构造函数。转移资源拥有权而不是拷贝,实现高效的资源管理。
myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) {
ap.pcount_ = nullptr; // 清除原对象的指针和引用计数,避免悬挂指针
ap.ptr_ = nullptr;
}
// 解引用运算符,返回所指向的对象。
T &operator*() { return *ptr_; }
// 成员访问运算符,返回内部存储的原始指针以便访问成员。
T *operator->() { return ptr_; }
private:
T *ptr_; // 存储管理的对象的原始指针
int *pcount_ = new int(1); // 用于跟踪有多少 myshared_ptr 指向相同的资源
这是一个简单的shared_ptr
智能指针实现;
-
智能指针成员
private: T *ptr_; // 管理的对象的原始指针。 int *pcount_ = new int(1); // 引用计数器,跟踪多少个 myshared_ptr 共享相同的对象。
其中
ptr_
是类型为T*
的指针,指向由智能指针管理的对象;pcount_
是一个动态分配的整数指针,用于共享此对象的智能指针实例数的计数器,初始化为1
,因为为带参构造,创建时存在一个所有者; -
构造函数
public: myshared_ptr(T *ptr) : ptr_(ptr) {}
构造函数接受一个原始指针将其存储在
ptr_
中,引用计数pcount_
在声明时初始化为1
;这意味着新创建的
myshared_ptr
独占管理权; -
析构函数
~myshared_ptr() { if (pcount_ && --(*pcount_) == 0) { delete pcount_; delete ptr_; pcount_ = nullptr; // 防止悬挂指针 ptr_ = nullptr; } }
每当
shared_ptr
类型对象被销毁时调用;首先检查引用计数是否存在并递减引用计数,如果递减后为
0
则表示没有其他shared_ptr
实例共享这个资源从而安全地删除资源和计数器本身; -
拷贝构造函数
myshared_ptr(myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) { ++(*pcount_); }
当创建新的
myshared_ptr
实例并将其初始化为现有实例时调用;拷贝构造函数将会复制成员变量并增加引用计数表明当前有一个新的智能指针共享对象;
-
拷贝赋值运算符重载
myshared_ptr<T> operator=(const myshared_ptr<T> &sp) { if (&sp != this) { if (--(*pcount_) == 0) { delete ptr_; delete pcount_; } ptr_ = sp.ptr_; pcount_ = sp.pcount_; ++(*pcount_); } return *this; }
用于将现有的
myshared_ptr
内容赋给另外一个现有的myshared_ptr
对象并防止自我赋值,如果当前对象是唯一拥有者会减少引用计数并可能删除起拥有的资源和计数器;
随后从右侧操作数复制数据并增加新的引用计数,将当前对象成为新的资源共享者;
-
移动构造函数
myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) { ap.pcount_ = nullptr; ap.ptr_ = nullptr; }
将右值引用(临时对象或将亡值)的所有权转移到新的智能指针对象中而不增加引用计数,在进行该操作后重置移动源(右值)以确保它在生命周期内不会无意中释放资源;
-
操作符重载
T &operator*() { return *ptr_; } T *operator->() { return ptr_; }
提供了解引用运算符和成员访问操作费提供了普通指针访问对应资源的接口;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
void func() {
myshared_ptr<Data> d1(new Data(10));
myshared_ptr<Data> d2 = d1; // 拷贝构造 此时d1 与 d2都指向同一个资源
cout << "d1: " << d1->data_ << endl;
cout << "d2: " << d2->data_ << endl;
}
int main() {
func();
return 0;
}
/*
运行结果为:
$ ./test
Data()
d1: 10
d2: 10
~Data()
*/
在该例子中使用了自定义的Data
类对象指针实例化一个myshared_ptr
智能指针d1
而后利用拷贝构造实例化了一个d2
;
其中d1
与d2
共享同一块资源;
使用引用计数,两个智能指针的析构函数只有一个智能指针将会释放资源故~Data()
只调用了一次;
shared_ptr 的循环引用问题与 weak_ptr
循环引用发生在两个或多个对象互相持有对方的shared_ptr
的情况从而形成一个环;
当这种情况出现时即使这些对象不再被其他任何对象引用他们仍然不能被析构,因为各自的引用计数永远不会降到0
;
struct ListNode {
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
ListNode() { cout << "ListNode()" << endl; }
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->next = n2;
n2->prev = n1;
return 0;
}
/*
运行结果为:
$ ./test
ListNode()
ListNode()
*/
在这个例子中创建了一个链表节点的ListNode
类,其中该链表节点为双向链表的节点,当调用构造函数与析构函数时将会打印对应的构造与析构信息;
运行结果表明在这个例子中并没有调用析构函数,意味着节点没有成功被释放发生了内存泄漏的问题;
这张图片对应上面的例子;
其中n1
与n2
中的prev
智能指针实例管理同一块空间,n2
与n1
中的next
智能指针实例管理同一块空间;
其对应的引用计数也都为2
;
n1
与n2
在生命周期结束时将会调用析构函数,空间的释放需要根据两个智能指针所指向的引用计数决定,当两个智能指针实例调用析构函数时对应的引用计数都将减到1
;
当n1
与n2
都调用析构函数后左边节点的管理权交由右边节点的prev
智能指针实例进行管理,右边节点的管理权交由左边节点的next
智能指针实例进行管理;
而只有左边节点被释放了对应的next
智能指针实例才能被释放,右边节点被释放了对应的prev
智能指针实例才能被释放从而形成一个环;
-
weak_ptr
weak_ptr
是C++11
所引入的一种智能指针,主要是用于解决shared_ptr
的循环引用问题;其提供了一种弱引用机制,可以指向一个由
shared_ptr
管理的对象,但是不会参与引用计数的管理,因此使用weak_ptr
可以有效打破循环引用问题而不会影响对象的生命周期管理;struct ListNode { weak_ptr<ListNode> next; // 使用weak_ptr代替shared_ptr weak_ptr<ListNode> prev; ListNode() { cout << "ListNode()" << endl; } ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> n1(new ListNode); shared_ptr<ListNode> n2(new ListNode); n1->next = n2; n2->prev = n1; return 0; } /* 运行结果为: $ ./test ListNode() ListNode() ~ListNode() ~ListNode() */
在这个例子中将原先的
shared_ptr
换成了weak_ptr
,运行结果表明析构函数被调用;也可用
shared_ptr
中的use_count
成员函数观察对应的引用计数数值;
weak_ptr
不是传统的智能指针,其不支持RAII
;
template <class T>
class myshared_ptr {
public:
// ...
// ...
const T *get() const { return ptr_; } // 增加get函数用于myweak_ptr使用myshared_ptr进行构造
private:
T *ptr_;
int *pcount_ = new int(1);
};
// -----------------
template <class T>
class myweak_ptr {
public:
myweak_ptr() : ptr_(nullptr) {}
myweak_ptr(const myshared_ptr<T> &sp) : ptr_(sp.get()) {}
myweak_ptr<T> &operator=(const myshared_ptr<T> &sp) {
ptr_ = sp.get();
return *this;
}
// 解引用运算符,返回指向的对象
T &operator*() { return *ptr_; }
// 成员访问运算符,返回原始指针
T *operator->() { return ptr_; }
private:
const T *ptr_;
};
这段代码是weak_ptr
智能指针的一个简单实现;
其主要功能为通过weak_ptr
指向shared_ptr
所管理的空间但不参与其引用计数;
struct ListNode {
myweak_ptr<ListNode> next;
myweak_ptr<ListNode> prev;
ListNode() { cout << "ListNode()" << endl; }
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
myshared_ptr<ListNode> n1(new ListNode);
myshared_ptr<ListNode> n2(new ListNode);
n1->next = n2;
n2->prev = n1;
return 0;
}
/*
运行结果为:
$ ./test
ListNode()
ListNode()
~ListNode()
~ListNode()
*/
在这个例子中使用了myshared_ptr
与myweak_ptr
来替代标准库里的智能指针,最终结果为节点的析构函数被调用,不存在内存泄漏问题;
智能指针的自定义删除器
现代智能指针提供了一个自定义的删除器使智能指针通过包含不同的对象来对其进行特殊的删除;
template <class U, class D> shared_ptr (U* p, D del);
以shared_ptr
为例,存在一个包含特定的类内模板函数的构造函数,支持传入一个自定义删除器对特殊的资源完成特殊的资源清理工作;
其传入的自定义删除器可以为任何可调用对象,lambda
表达式,function
包装器,bind
绑定对象,仿函数等可调用对象;
class Data {
public:
Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
~Data() { cout << "~Data()" << endl; }
public:
int data_;
};
int main() {
shared_ptr<FILE> file(fopen("log.txt", "w"), [](FILE* fp) { fclose(fp); });
// 使用定制删除器(lambda表达式)关闭文件
shared_ptr<Data> dt(new Data[3], [](Data* d) { delete[] d; });
// 使用定制删除器(lambda表达式)清理自定义类型数组
return 0;
}
/*
运行结果为:
$ ./test
Data()
Data()
Data()
~Data()
~Data()
~Data()
*/
其自定义删除器可使用function
包装器实现;
#include <functional> // 需要包含这个头文件以使用 std::function
template <class T>
class myshared_ptr {
public:
myshared_ptr(T *ptr) : ptr_(ptr) {}
myshared_ptr() : ptr_(nullptr), pcount_(nullptr) {}
// 带自定义删除器的构造函数:初始化指针和删除器
template <class D>
myshared_ptr(T *ptr, D del) : ptr_(ptr), del_(del) {}
~myshared_ptr() {
if (pcount_ && --(*pcount_) == 0) {
delete pcount_;
del_(ptr_); // 调用删除器
pcount_ = nullptr;
ptr_ = nullptr;
}
}
myshared_ptr(const myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) {
++(*pcount_);
}
myshared_ptr<T> operator=(const myshared_ptr<T> &sp) {
if (&sp != this) {
if (pcount_ && --(*pcount_) == 0) {
del_(ptr_); // 调用删除器
delete pcount_;
}
ptr_ = sp.ptr_;
pcount_ = sp.pcount_;
++(*pcount_);
}
return *this;
}
myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) {
ap.pcount_ = nullptr;
ap.ptr_ = nullptr;
}
T &operator*() { return *ptr_; }
T *operator->() { return ptr_; }
const T *get() const { return ptr_; }
private:
T *ptr_;
int *pcount_ = new int(1);
std::function<void(T *)> del_ = [](T *ptr) { delete ptr; }; // 默认删除器为 delete
};
这个实现中使用了function
包装器与lambda
合作实现;
当用户传入一个删除器时默认删除器将会被替换;