C++ - 智能指针及其内存管理
智能指针
C已经有了指针了,为什么还要引入智能指针这种玩意?
旧式C的指针使用存在两大问题:
-
没有自动回收机制。旧式指针使用完毕后必须手动释放,程序员若忘记释放就会导致内存浪费,而且浪费的内存难以察觉,甚至直到出现内存泄漏的程序将整个内存占满程序员才会察觉。
-
存在指针悬挂问题。对旧式指针的操作本质上就是对指向的内存操作,而不是指针本身的地址操作,删除指针就相当于释放内存,此时若有其他指针指向该内存,这些指针就会变成野指针,造成指针悬挂问题:
//旧式指针 int buf1 = 10; int *p1,*p2; p1 = p2= &buf1; cout << *p1 << endl; //10 delete p1; //对旧式指针的删除本质上是释放内存而不是指针本身,多指针共享变量情况下极易造成悬挂问题 cout << *p2 << endl; //Error!
-
为此C++标准推出了智能指针来解决上述问题,并引入了引用计数(即统计某一块内存地址被引用的次数,引用变为0时即自动释放内存)和RAII的思想。
智能指针共有shared_ptr , unique_ptr 和 weak_ptr 三种,这三种智能指针的共同点是,其指向地址引用计数为0后自动释放内存。
### shared_ptr
该指针被设计用于解决上述的第二个问题,即多指针共享下出现的悬挂问题,对于该指针的释放本质上是释放指针地址而不是其所指对象地址,因此多指针场景下使用很安全:
//智能指针
int buf2 = 20;
shared_ptr<int> sp1,sp2;
sp1 = sp2 = make_shared<int>(buf2);
cout << *sp1 << endl; //20
sp1.reset();
cout << *sp2 << endl; //20
但该指针亦有其缺点,即会导致循环引用的问题,即如果对象间用共享指针相互应用,双方的引用计数将永远不为0(感觉有点像死锁),导致无法销毁。
weak_ptr
weak_ptr可用于解决上述的循环引用问题,该指针的特点是指向内存不会影响其引用计数,本质上该指针的使用是为shared_ptr服务的,若shared_ptr指向的内存被释放时weak_ptr指向该shared_ptr,则weak_ptr会成为空指针。
由于引用的对象随时可能不存在,weak_ptr必须用一个返回shared_ptr的lock()函数来引用对象。
int buf3 = 30;
//weak_ptr本身不增加内存使用计数,无法独立占有对象,必须和shared_ptr配合使用
weak_ptr<int> wp1,wp2;
wp1 = make_shared<int>(buf3); //尝试独立占有
auto sp = make_shared<int>(buf3); //让一个共享指针指向内存,再让弱指针指向共享指针
wp2 = sp;
cout << wp1.lock() << endl; //0,指针地址为空,解引用将异常
cout << wp2.lock() << " " << *wp2.lock() << endl;//显示指针地址及其引用值30
对于循环引用问题,可以结合图论去理解,被多少个shared_ptr引用就相当于实际入度,weak_ptr的引用则可以忽略,示例如下:
#include<iostream>
#include<memory>
using namespace std;
class Child;
class Parent {
private:
shared_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child){
this->ChildPtr = child;
}
void doSomething(){
}
~Parent(){
}
};
class Child{
private:
weak_ptr<Parent> ParentPtr;
public:
void setParent(shared_ptr<Parent> parent){
this->ParentPtr = parent;
}
void doSomething(){
}
~Child(){
}
};
int main(){
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent());
shared_ptr<Child> c(new Child());
p->setChild(c);
c->setParent(p);
wpp = p;
wpc = c;
cout << wpp.use_count() << endl; //1
cout << wpc.use_count() << endl; //2
cout << p.use_count() << endl; //1
cout << c.use_count() << endl; //2
}
cout << wpp.use_count() << endl; //0
cout << wpc.use_count() << endl; //0
}
unique_ptr
相比它的两个指针兄弟的特点是它所指向的内存不能被其他指针所共享,但是可以转交内存的使用权(可以将内存转交给shared_ptr使其变为共享,这一点容易被攻击者利用)。
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p)); //可以将一个快被销毁的unique_ptr赋值给另一个同类
}
int main(){
unique_ptr<vector<int>> up1(new vector<int>{1,2,3,4,5,6,7,8,9}); //unique_ptr所指向的对象不能被其他指针共享
unique_ptr<vector<int>> up2(up1); //Error
shared_ptr<vector<int>> sp1(up1); //Error
vector<int> *op1 = up1; //Error
unique_ptr<vector<int>> up3(up1.release()); // unique_ptr可以将对象的所有权转交给另一个指针
//1,2,3,4,5,6,7,8,9
for(auto it = up3->begin(); it != up3->end(); it++){
cout << *it << " ";
}
}
其是智能指针家族中唯一一个能通过下标访问的指针。(这样一看,unique_ptr其实就可以当做不能被共享的普通指针)
unique_ptr<int[]> up(pa);
for(int i = 0;i != 10; i++){
up[i] = i;
}
up.release();
for(int i = 0 ; i < 10; i++){
cout << up[i] << " ";
}
cout << endl;
shared_ptr<int> sp(new int[10],[](int *p){ delete[] p;});
for(int i = 0;i != 10; i++){
*(sp.get() + i) = i;
}
new和delete
和malloc和free有什么区别?
https://blog.csdn.net/u010510020/article/details/76266505
allocator
一般程序都是在分配对象的同时初始化对象,这就可能造成一个资源浪费的问题,试想下面的案例:
int n = 3;
string *const p = new string[n]; //事先就确定和分配了n个string对象
string *q = p;
string s;
while(cin >> s && q != p + n){
*q++ = s;
}
const size_t size = q - p;
for(int i = 0; i < n; i++){
cout << p[i] << endl;
}
delete[] p; //释放内存
代码中分配了几个string对象,但是我们实际可能不会需要这么多string,这样后面的string对象就没用了,而且由于分配的对象是已初始化过的,对它们赋值其实就是二次赋值。
allocator是C++提供的一种更微观的内存操作,它初始化的内存是相比传统方法是未构造的,未初始化的:
allocator<string> alloc;
string s;
auto const p = alloc.allocate(n); //分配n个string大的空间
auto q = p;
while( cin >> s && q != p + n){
alloc.construct(q++,s); //构造对象
}
for(int i = 0; i < n; i++){
cout << p[i] << endl;
}
while(q != p){
alloc.destroy(--q); //销毁时先销毁对象
}
alloc.deallocate(p,n); //再释放分配内存
可以想象成两个箱子,传统方法的箱子装了n张白纸(初始化是一片空白),输入赋值就相当于在上面写字,delete就是连纸带箱一起摧毁;
而allocator的箱子则是纯粹的空箱子(allocate),n张白纸需要手动放进去一张张写字(construct), 销毁时先将纸张一张张撕毁(destroy),再摧毁箱子(deallocate)
个人理解,使用allocator进行对象分配相比传统方法的优势就是省去了仅初始化时被赋值的无用对象占用的空间。
allocator Plus
后面学到拷贝控制时遇到了一个使用vector拷贝类的场合让我体会到使用allocator的重要性:
设有如下类:
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):ps(new std::string(s)),i(0) { printf("construct successfully\n");}
//合成构造
HasPtr(const HasPtr& HP):ps(new std::string("construct")),i(0) {}
//合成赋值
HasPtr& operator= (const HasPtr& HP)
{
std::string *p = new std::string(*HP.ps);//new返回的是指向分配好内存、创建了对象的指针
delete ps;//首先删除原内存
ps = new std::string("operator"); //赋值
i = HP.i;
return *this;//返回值
}
//合成析构
~HasPtr(){
printf("destruct successfully\n");
}
public:
std::string *ps;
int i;
};
如下代码最后不会被打印析构5次,因为这个vector本质上是先分配空间,然后初始化了5个未赋值的垃圾元素,这样我们的元素其实是插在垃圾元素的后面,而且初始化后的vector的size和capacity都相等,所以我们第一个元素插进去就会让vector扩容,原先vector的元素就被析构了,所以最后打印出的析构数实际上是vector每次扩容时移动的元素数(包括垃圾元素和我们的元素)加上最后循环结束时vector销毁的元素数(垃圾元素+我们的元素)再加上我们用于赋值的test1类的和。
HasPtr test1(std::string("test"));
printf("\nstart vector testing\n");
int times = 5;
std::vector<HasPtr> vec(times);
for(int i = 0; i < times; i++){
vec.push_back(test1);
}
construct successfully
start vector testing
construct successfully
construct successfully
construct successfully
construct successfully
construct successfully
before size: 5 cap:5
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
now size: 6 cap:10
before size: 6 cap:10
now size: 7 cap:10
before size: 7 cap:10
now size: 8 cap:10
before size: 8 cap:10
now size: 9 cap:10
before size: 9 cap:10
now size: 10 cap:10
loop 5 times,destroyed 5 times
vector testing done.
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
如果使用allocator将分配空间和对象初始化分离开就不会有这种问题:
//可以用allocator解决resize出的垃圾元素带来的浪费问题
HasPtr test1(std::string("test"));
int times = 5;
std::allocator<HasPtr> alloc;
auto const p = alloc.allocate(times);
auto q = p;
for(int i = 0; i < times; i++){
alloc.construct(q++,test1);
}
while(q != p){
alloc.destroy(--q);
}
alloc.deallocate(p,times);
construct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
destruct successfully
测试代码如下,大家随便摆弄,有助于了解vector的底层机制。
#include<iostream>
#include<memory>
#include<vector>
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):ps(new std::string(s)),i(0) { printf("construct successfully\n");}
//合成构造
HasPtr(const HasPtr& HP):ps(new std::string("construct")),i(0) {}
//合成赋值
HasPtr& operator= (const HasPtr& HP)
{
std::string *p = new std::string(*HP.ps);//new返回的是指向分配好内存、创建了对象的指针
delete ps;//首先删除原内存
ps = new std::string("operator"); //赋值
i = HP.i;
return *this;//返回值
}
//合成析构
~HasPtr(){
printf("destruct successfully\n");
decCnt++;
}
public:
std::string *ps;
int i;
};
int main(){
// HasPtr test1(std::string("test"));
// printf("\nstart vector testing\n");
// int times = 5;
// std::vector<HasPtr> vec(5);
// // /**注意,除了push_back本身对传入元素拷贝插入以外,vec本身超过一定数量会扩容,导致构析数异常增多
// // * 每次扩容都会销毁旧vector内的所有元素并从头拷贝进新vector
// // * /
// // /**
// // * 预先设定容量实际上是构造等于容量数的未初始化元素在vector内,这样后面push_back反而是在插在这些垃圾元素的后面而不是替换它们,导致扩容
// // * 最终构析数 = vec内含有元素数(设定导致的垃圾元素数加上后装入的元素数) + 1
// // * */
// for(int i = 0; i < times; i++){
// printf("\nbefore size: %d cap:%d\n",vec.size(),vec.capacity());
// vec.push_back(test1);
// printf("now size: %d cap:%d\n",vec.size(),vec.capacity());
// }
// printf("loop %d times,destroyed %d times\n",times,decCnt);
// printf("vector testing done.\n\n");
//可以用allocator解决resize出的垃圾元素带来的浪费问题
HasPtr test1(std::string("test"));
int times = 5;
std::allocator<HasPtr> alloc;
auto const p = alloc.allocate(times);
auto q = p;
for(int i = 0; i < times; i++){
alloc.construct(q++,test1);
}
while(q != p){
alloc.destroy(--q);
}
alloc.deallocate(p,times);
}