16.1 C++智能指针-new/delete探秘
16.2 C++智能指针-shared_ptr
16.3 C++智能指针-weak_ptr
16.4 C++智能指针-shared_ptr使用场景、陷阱、性能分析与使用建议
16.5 C++智能指针-unique_ptr
文章目录
4.shared_ptr使用场景、陷阱、性能分析与使用建议
4.1 std::shared_ptr使用场景
shared_ptr<int> createO(int value)
{
return make_shared<int>(value); //返回一个shared_ptr
}
void myfunc(int value)
{
shared_ptr<int> ptmp = createO(10);
return; //ptmp离开了作用域(ptmp是局部变量),因此它指向的内存会被自动释放;
}
如果不用一个变量接收该函数返回的结果,那么myfunc返回的shared_ptr会被销毁,它指向的对象也会被销毁。
或者,如果用一个变量接收该函数返回的结果,那么myfunc返回的shared_ptr就不会被销毁,当然,它所指向的对象也就不会被销毁。例如下面的代码:
{
myfunc(12); //如果这块不用变量接收返回结果,则调用myfunc返回的shared_ptr会被销毁,该shared_ptr指向的对象也会被销毁
}
{
auto p11 = myfunc(12); //这块用了变量接收myfunc返回结果,所以myfunc返回的shapred_ptr不会被销毁,指向的对象也不会被销毁,此时p11是1个强引用
}
4.2 std::shared_ptr使用陷阱分析
智能指针虽然智能,但使用是有陷阱和禁忌的,不要以为用了智能指针就高枕无忧了,一旦用错了,也是致命的。
(1)慎用裸指针
void myfunc(int value)
{
shared_ptr<int> ptmp = createO(10);
return; //ptmp离开了作用域(ptmp是局部变量),因此它指向的内存会被自动释放;
}
void proc(shared_ptr<int> ptr)
{
return;
}
{
int* p = new int(100); //裸指针
//proc(p); //语法错 int *p不能转换成shared_ptr<int>
proc(shared_ptr<int>(p)); //参数是个临时shared_ptr,用一个裸指针显式构造的
*p = 45; //不可以预料到的结果;因为p指向的内存已经被释放了
}
所以,把一个普通裸指针绑到了一个shared_ptr上,那内存管理的责任就交给了这个shared_ptr,这时就不应该再使用裸指针(内置指针)访问shared_ptr指向的内存了。
只如下修改即可:
{
shared_ptr<int> myp(new int(100));
proc(myp);
*myp = 45; //myp可是shared_ptr<...>类型,*代表解引用
}
另外请注意,裸指针虽然可以初始化shared_ptr,不要用裸指针初始化多个shared_ptr。看如下代码
为了避免这个问题,即便用裸指针,直接传递new运算符而不是传递一个裸指针变量。
{
int* pi = new int;
shared_ptr<int> p1(pi);
shared_ptr<int> p2(pi); //p1一个引用,p2一个引用,会导致p1,p2两个指针之间无关联关系(每个的强引用计数都是1),所以释放时pi所指向的内存释放2次,这显然会出问题
shared_ptr<int> p1(new int); //这种写法至少大大降低了用pi来创建p2的可能性
}
(2)慎用get返回的指针
get返回的指针不能delete,否则会产生异常
{
shared_ptr<int> myp(new int(100));
int* p = myp.get(); //返回myp中保存的指针
delete p; //不可以这样,会导致异常
}
不能将其他智能指针绑到get返回的指针上
{
shared_ptr<int> myp(new int(100));
int* p = myp.get(); //这个指针千万不能随便释放,否则myp就没有办法正常管理该指针了
{
//shared_ptr<int> myp2(p); //这行代码万万不可。现在myp和myp2引用计数都为1,但是一旦跳出这个程序块,往下看。其实测试中发现,这句代码本身就会在程序执行结束时产生异常
shared_ptr<int> myp2(myp); //执行后myp和myp2引用计数都为2,跳出程序块后,myp2失效,myp引用计数恢复为1,myp可以正常使用
}
//离开上面这个myp2的有效范围,导致myp指向的内存也被释放了
//*myp = 100; //该内存已经释放,赋值会导致不可预料的后果
}
因为myp和myp2彼此是独立创建的,每一个都有自己的独立引用计数(都是1),myp2跑出作用域后导致其所指向的对象(内存)被销毁,而这块内存恰好也是myp所指向的内存。
所以结论就是:永远不要用get得到的指针来初始化另外一个智能指针或者给另外一个智能指针赋值
(3)用enable_shared_from_this返回this
class CT
{
public:
shared_ptr<CT> getself()
{
return shared_ptr<CT>(this);
}
};
{
shared_ptr<CT> pct1(new CT);
shared_ptr<CT> pct2 = pct1; //这没问题,2个强引用
}
{
shared_ptr<CT> pct1(new CT);
shared_ptr<CT> pct2 = pct1->getself(); //问题出现
}
上面这两行代码是用同一个指针构造了两个智能指针pct1和pct2,这两个智能指针之间没有任何关系。这类似于上面讲过的:用裸指针初始化多个shared_ptr的感觉。也就是在释放时一个对象内存会释放两次。
怎样能够让pct1和pct2产生关联关系(换句话说,也就是安全地通过this指针创建一个shared_ptr)呢?这里就得用到一个C++标准库里的类模板enable_shared_from_this。直接看代码——修改CT类:
class CT :public std::enable_shared_from_this<CT> //这是C++标准库里提供的一个类模板
{
public:
shared_ptr<CT> getself()
{
//return shared_ptr<CT>(this);
return shared_from_this();//这个是enable_shared_from_this类中方法,要通过此方法返回智能指针
}
};
现在在类外创建CT对象的智能指针以及通过CT对象返回的this智能指针都是安全的了。这个问题其实也就是如何让多个shared_ptr安全地指向同一个类对象的问题。
这里解释一下shared_from_this的工作原理:
enable_shared_from_this是一个类模板,它的类型模板参数就是继承它的子类的类名。该类模板中有一个弱指针weak_ptr,这个弱指针能够观测this,调用shared_from_this方法的时候,这个方法内部实际是调用了这个weak_ptr的lock方法,lock方法会让shared_ptr指针计数+1,同时返回这个shared_ptr。
(4)避免循环引用
循环引用会导致内存泄漏。
class CA;
class CB;
class CA
{
public:
shared_ptr<CB>m_pbs;
~CA()
{
cout << "~A()执行了" << endl;
}
};
class CB
{
public:
shared_ptr<CA>m_pas;
~CB()
{
cout << "~B()执行了" << endl;
}
};
{
shared_ptr<CA> pca(new CA);
shared_ptr<CB> pcb(new CB);
pca->m_pbs = pcb; //现在等价于指向CB对象的有两个强引用
pcb->m_pas = pca; //现在等价于指向CA对象的有两个强引用
}
这是一段很诡异的代码,能写出这样一段代码不容易,执行的结果就是CA和CB的析构函数都没执行,也就是pca和pcb这两个智能指针所指向的对象都没删除,那显然程序退出的时候,new出来的对象没删除,就等于内存泄漏。
导致这两个对象的引用计数都变成2了,离开作用域时,计数-1,-1变成1了,没有变成0,所以导致pca和pcb这两个对象都没被释放,产生内存泄漏。
那该如何解决呢?
可以把CA类或者CB类里面的任何一个成员变量修改为weak_ptr,这里就把CB里面的成员变量m_pas修改为weak_ptr吧
运行起来正常,不泄漏内存,两个智能指针都能正确释放,正确调用析构函数:
class CA
{
public:
shared_ptr<CB>m_pbs;
weak_ptr<CB>m_pbs;
~CA()
{
cout << "~A()执行了" << endl;
}
};
上面代码的执行结果是先执行CA类的析构函数,再执行CB类的析构函数。
如果把弱指针的修改放到CB类中,那么显然执行的结果应该是先执行CB类的析构函数,再执行CA类的构造函数。
4.3 性能说明
(1)尺寸问题
shared_ptr的尺寸是裸指针的2倍。
make_shared:分配并初始化一个对象,返回指向此对象的shared_ptr。所以make_shared总是创建一个控制块。
使用裸指针来创建一个shared_ptr对象时。
(2)移动语义
{
shared_ptr<int> p1(new int(100)); //p1指向该对象(内存)
shared_ptr<int> p2(std::move(p1)); //移动语义,移动构造p2,p1不再指向该对象而变成空,p2指向了该对象,引用计数保持为1
shared_ptr<int> p3;
p3 = std::move(p2); //移动赋值,p2指向空,p3指向该对象,整个对象引用计数依旧为1
}
可以看到,复制会使shared_ptr的强引用计数递增,而移动并不会使shared_ptr的强引用计数递增。
4.4 补充说明和使用建议
已经掌握了绝大部分shared_ptr使用方法。当然,讲解不会面面俱到,有些很晦涩也不常用的内容,并没有讲出来。例如不但可以给shared_ptr提供删除器,还可以给它提供分配器来解决内存分配问题,内存分配器的信息其实也是保存在控制块中。
优先使用make_shared构造智能指针,编译器内部会有一些针对内存分配的特殊处理,所以会使make_shared效率更高,如消除重复代码、改进安全性等。看如下代码:
{
shared_ptr<string> ps1(new string("I Love China!"));
}
上面这行代码会至少分配两次内存,第一次是为string类型的实例分配内存(从而保存字符串"I love China!"),第二次是在shared_ptr构造函数中给该shared_ptr控制块分配内存。
而如下代码,针对make_shared,编译器只会分配一次内存,这个内存分配的足够大,既能保存字符串(“I love China!”)又能够同时保存控制块:
{
auto ps2 = make_shared<string>("I Love China!");
}