文章目录
- 智能指针
- 概念
- 为什么使用智能指针
- 智能指针使用
- 智能指针的常用函数
- get() 获取智能指针托管的指针地址.
- reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
- auto_ptr
- unique_ptr
- shared_ptr
- **shared_ptr的原理**
- 引用计数的使用
- 构造
- 初始化
- 主动释放对象
- 重置
- 交换
- shared_ptr使用陷阱
- 1. 互相引用的时候,就会出现循环引用的现象 造成无法释放资源
- 2.不要把一个原生指针给多个智能指针管理;
- 3.禁止delete 智能指针get 函数返回的指针;
- weak_ptr
- shared_from_this
- 使用原因
- shared_from_this 使用注意事项
- 不能再构造函数中调用shared_from_this()
- 继承问题
- 参考
智能指针
概念
在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
其实是RALL的思想。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。
为什么使用智能指针
一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏!
如下例子就是内存泄露的例子:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
// 动态分配内存,没有释放就return
void memoryLeak1() {
string *str = new string("动态分配内存!");
return;
}
// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
string *str = new string("内存泄露!");
// ...此处省略一万行代码
// 发生某些异常,需要结束函数
if (1) {
return -1;
}
/
// 另外,使用try、catch结束函数,也会造成内存泄漏!
/
delete str; // 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
return 1;
}
int main(void) {
memoryLeak1();
memoryLeak2();
return 0;
}
memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!
使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
所以我们要把分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存。
智能指针就是通过这个原理来解决指针自动释放的问题!
智能指针使用
智能指针的使用跟普通指针类似,可以使用运算符“ * " 和 ” -> "去获得指向的对象,
为什么智能指针可以像普通指针那样使用???
因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象。
C++ 提供了多种智能指针:
C++98 提供了 auto_ptr 模板的解决方案
C++11 增加unique_ptr、shared_ptr 和weak_ptr
智能指针的三个常用函数:
智能指针的常用函数
get() 获取智能指针托管的指针地址.
// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp = test.get(); // 获取指针返回
cout << "tmp->debug:" << tmp->getDebug() << endl;
但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
函数原型:
_NODISCARD _Ty * get() const noexcept
{ // return wrapped pointer
return (_Myptr);
}
reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
// 定义智能指针
auto_ptr<Test> test(new Test);
test.reset(); // 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之
reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
函数原型:
void reset(_Ty * _Ptr = nullptr)
{ // destroy designated object and store new pointer
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}
auto_ptr
auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
它采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
如果auto_ptr调用拷贝构造函数或者赋值重载函数后,如果再去使用原来的对象的话,那么整个程序就会崩溃掉(因为原来的对象被设置为nullptr),这对程序是有很大的伤害的.所以很多公司会禁用auto_ptr智能指针。
C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。
auto_ptr 被C++11抛弃的主要原因
- 复制或者赋值都会改变资源的所有权
- 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
- 不支持对象数组的内存管理
unique_ptr
C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。
unique_ptr特性
1、基于排他所有权模式:两个指针不能指向同一个资源
2、在 STL 容器中使用unique_ptr,不允许直接赋值
3、支持对象数组的内存管理
// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]); // 支持这样定义
shared_ptr
shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃掉。
shared_ptr的原理
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:
- shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
- 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
- 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
- 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
销毁过程:
举例:
class Person {
public:
Person(int v) {
this->no = v;
cout << "构造函数 \t no = " << this->no << endl;
}
~Person() {
cout << "析构函数 \t no = " << this->no << endl;
}
private:
int no;
};
// 仿函数,内存删除
class DestructPerson {
public:
void operator() (Person *pt) {
cout << "DestructPerson..." << endl;
delete pt;
}
};
引用计数的使用
调用use_count函数可以获得当前托管指针的引用计数。
shared_ptr<Person> sp1;
shared_ptr<Person> sp2(new Person(2));
// 获取智能指针管控的共享指针的数量 use_count():引用计数
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;
// 共享
sp1 = sp2;
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;
shared_ptr<Person> sp3(sp1);
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl;
cout << "sp2 use_count() = " << sp3.use_count() << endl << endl;
如上代码,sp1 = sp2; 和 shared_ptr< Person > sp3(sp1);就是在使用引用计数了。
sp1 = sp2; --> sp1和sp2共同托管同一个指针,所以他们的引用计数为2;
shared_ptr< Person > sp3(sp1); --> sp1和sp2和sp3共同托管同一个指针,所以他们的引用计数为3;
构造
-
shared_ptr< T > sp1; 空的shared_ptr,可以指向类型为T的对象
shared_ptr<Person> sp1; Person *person1 = new Person(1); sp1.reset(person1); // 托管person1
-
shared_ptr< T > sp2(new T()); 定义shared_ptr,同时指向类型为T的对象
shared_ptr<Person> sp2(new Person(2)); shared_ptr<Person> sp3(sp1);
-
shared_ptr<T[]> sp4; 空的shared_ptr,可以指向类型为T[]的数组对象 C++17后支持
shared_ptr<Person[]> sp4;
-
shared_ptr<T[]> sp5(new T[] { … }); 指向类型为T的数组对象 C++17后支持
shared_ptr<Person[]> sp5(new Person[5] { 3, 4, 5, 6, 7 });
-
shared_ptr< T > sp6(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D释放内存
shared_ptr<Person> sp6(NULL, DestructPerson());
-
shared_ptr< T > sp7(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存
shared_ptr<Person> sp7(new Person(8), DestructPerson());
初始化
-
构造函数
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1 shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
-
使用make_shared 初始化对象,分配内存效率更高(推荐使用)
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:make_shared<类型>(构造类型对象需要的参数列表);
shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个 shared_ptr<string> up4 = make_shared<string>("字符串"); shared_ptr<Person> up5 = make_shared<Person>(9);
赋值:
shared_ptrr<int> up1(new int(10)); // int(10) 的引用计数为1 shared_ptr<int> up2(new int(11)); // int(11) 的引用计数为1 up1 = up2; // int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
主动释放对象
shared_ptrr<int> up1(new int(10));
up1 = nullptr ; // int(10) 的引用计数减1,计数归零内存释放
// 或
up1 = NULL; // 作用同上
重置
p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
p1是一个指针!
交换
p1 和 p2 是智能指针
std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
shared_ptr使用陷阱
1. 互相引用的时候,就会出现循环引用的现象 造成无法释放资源
如下代码:
Boy类中有Girl的智能指针;
Girl类中有Boy的智能指针;
当他们交叉互相持有对方的管理对象时…
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Girl;
class Boy {
public:
Boy() {
cout << "Boy 构造函数" << endl;
}
~Boy() {
cout << "~Boy 析构函数" << endl;
}
void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;
}
private:
shared_ptr<Girl> girlFriend;
};
class Girl {
public:
Girl() {
cout << "Girl 构造函数" << endl;
}
~Girl() {
cout << "~Girl 析构函数" << endl;
}
void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}
private:
shared_ptr<Boy> boyFriend;
};
void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 陷阱用法
spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}
int main(void) {
useTrap();
system("pause");
return 0;
}
运行截图:
可以看出,程序结束了,但是并没有释放内存,这是为什么呢???
如下图:
当我们执行useTrap函数时,注意,是没有结束此函数,boy和girl指针其实是被两个智能指针托管的,所以他们的引用计数是2
seTrap函数结束后,函数中定义的智能指针被清掉,boy和girl指针的引用计数减1,还剩下1,对象中的智能指针还是托管他们的,所以函数结束后没有将boy和gilr指针释放的原因就是于此。
所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
当然,这也是有办法解决的,那就是使用weak_ptr弱指针。
针对上面的情况,还讲一下另一种情况。如果是单方获得管理对方的共享指针,那么这样着是可以正常释放掉的!
例如:
void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 单方获得管理
//spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
}
反过来也是一样的!
这是什么原理呢?
首先释放spBoy,但是因为girl对象里面的智能指针还托管着boy,boy的引用计数为2,所以释放spBoy时,引用计数减1,boy的引用计数为1;
在释放spGirl,girl的引用计数减1,为零,开始释放girl的内存,因为girl里面还包含有托管boy的智能指针对象,所以也会进行boyFriend的内存释放,boy的引用计数减1,为零,接着开始释放boy的内存。最终所有的内存都释放了。
2.不要把一个原生指针给多个智能指针管理;
int *x = new int(10);
unique_ptr< int > up1(x);
unique_ptr< int > up2(x);
// 警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);
3.禁止delete 智能指针get 函数返回的指针;
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
shared_ptr< int > sp1(new int(10));
// 一个典型的错误用法 shared_ptr< int > sp4(sp1.get());
weak_ptr
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和-> .但可以使用 lock 获得一个可用的 shared_ptr 对象。
shared_from_this
enable_shared_from_this是一个模板类,定义于头文件<memory>
,其原型为:
template< class T > class enable_shared_from_this;
std::enable_shared_from_this
能让一个对象(假设其名为 t
,且已被一个 std::shared_ptr
对象 pt
管理)安全地生成其他额外的 std::shared_ptr
实例(假设名为 pt1, pt2, ...
) ,它们与 pt
共享对象 t
的所有权。
若一个类 T 继承 std::enable_shared_from_this<T>
,则会为该类 T
提供成员函数:shared_from_this
。 当 T
类型对象 t
被一个为名为 pt
的 std::shared_ptr<T>
类对象管理时,调用 T::shared_from_this
成员函数,将会返回一个新的 std::shared_ptr<T>
对象,它与 pt
共享 t
的所有权。
使用原因
-
把当前类对象作为参数传给其他函数时,为什么要传递share_ptr呢?直接传递this指针不可以吗?一个裸指针传递给调用者,谁也不知道调用者会干什么?假如调用者delete了该对象,而share_tr此时还指向该对象。
-
这样传递share_ptr可以吗?
share_ptr <this>
这样会造成2个非共享的share_ptr指向一个对象,最后造成2次析构该对象。
举例:
当你想使用一个类的某个成员函数返回指向这个类的指针的时候,可以这样写
class A
{
public:
A(int y=0):x(y){ }
A* getthis()
{
return this;
}
int x;
};
int main (void)
{
A a;
cout<<a.x<<endl;
A*p=a.getthis();
p->x=1;
cout<<a.x<<endl;
}
通过在成员函数中返回this指针,我们得到了指向这个对象本身的指针,并且可以通过它来改变对象。
但是在很多情况中,我们更想用智能指针去控制对象的生存周期,比如这样。
int main (void)
{
shared_ptr<A>sp1(new A());
shared_ptr<A>sp2(sp1->getthis());
cout<<sp1.use_count()<<endl;
cout<<sp2.use_count()<<endl;
}
两个智能指针的引用计数都是1,可想而知在函数退出的时候会发生什么,同一块内存被释放了两次,程序崩溃是免不了的。原因是this是裸指针,我们这么操作和用同一个裸指针给两个智能指针赋值是一个意思。如果我们能返回一个智能指针,那么问题就解决了。这时我们可以使用shared_from_this这个模版类。
class A:public enable_shared_from_this<A>
{
public:
A(int y=0):x(y){ }
shared_ptr<A> getthis()
{
return shared_from_this();
}
int x;
};
这样上面那段程序的输出是两个2,两个智能指针都意识到自己指向的是同一片内存,引用计数正确的发挥作用,同一片内存只会被析构一次。
shared_from_this 使用注意事项
不能再构造函数中调用shared_from_this()
除此之外,shared_from_this还有一个需要注意的地方。你可以点开shared_from_this的源码看看,他返回了enable_shared_from_this中唯一的一个成员变量
mutable weak_ptr<_Ty> _Wptr;
而当我们再看这个类的构造函数,并没有对这个成员变量赋值。可想而知,如果这个变量始终没有被赋值,那么我们无法使用shared_from_this这个函数。
为了弄清楚这个变量什么时候被赋值,我一步一步的调试了下面这行代码,虽说只有一行,但是他真的做了很多事情。
shared_ptr <A> sp1(new A());
来看这条语句,它做了三件事情,第一件事情是构造enable_shared_from_this这个类,毕竟想要构造A就要先构造他的父类。
此时wptr没有被赋值。
第二件事情是构造A,此时wptr也没有被赋值。
第三件事情,构造sp1,令人想不到的是,构造sp1之后,属于类A的父类的wptr被赋值了
所以可想而知,如果没有这样一个sp1的出现,我们是无法使用shared_from_this的。所以才有了所说的“不能再构造函数中调用shared_from_this()”的说法,这个构造函数指的是A的构造函数,因为那个时候wptr还没有值,当然不能调用shared_from_this。
当然了,这样也是不行的。
int main (void)
{
A a;
shared_ptr<A>sp(a.shared_from_this());
}
继承问题
在同一个继承体系中,不能同时出现多个enable_shared_from_this类。父类继承了enable之后,子类只能对shared_from_this()的返回值进行转型。也就是说把shared_ptr转换成shared_ptr,想做到这一点,你只能这么写
return dynamic_pointer_cast<B>(shared_from_this());
如下代码验证:
namespace test_enable_shared_from_this{
class Derived : public std::enable_shared_from_this<Derived>
{
public:
void SetValue(int a)
{
_a = a;
}
int GetValue() const
{
return _a;
}
private:
int _a;
};
class Derived1 : public Derived
{
private:
int _b;
};
}
调用代码如下:
int main()
{
using namespace test_enable_shared_from_this;
auto d1 = std::make_shared<Derived1>();
auto obj = d1->shared_from_this();
return 0;
}
发现问题没有?虽然d1和obj对象地址一样,但是对象内存包含的成员属性不同。d1有自身的属性成员变量,但是obj没有,只含有基类的属性成员变量。是不是shared_from_this返回的对象中只包含那些直接继承
std::enable_shared_from_this呢?我们用多继承验证下:
namespace test_enable_shared_from_this{
class Derived : public std::enable_shared_from_this<Derived>
{
public:
void SetValue(int a)
{
_a = a;
}
int GetValue() const
{
return _a;
}
private:
int _a;
};
class Derived1 : public std::enable_shared_from_this<Derived1>
{
private:
int _b;
};
class Derived2 : public Derived, public Derived1
{
private:
int _c;
};
}
int main()
{
using namespace test_enable_shared_from_this;
auto d2 = std::make_shared<Derived2>();
auto obj = d2->shared_from_this();
return 0;
}
直接提示执行不明确,这个很好理解,它不知道你究竟返回哪个子类std::shared_ptr。所以,继承类中不能存在多次继承std::enable_shared_from_this。哪个类继承的这个类,就返回这个类实例化的对象。
参考
- C++ 智能指针 - 全部用法详解
- 智能指针详细解析
- C/C++编程:理解make_shared
- std::enable_shared_from_this使用
- C++ 笔记 shared_from_this()的原理与使用
- shared_from_this的使用