C++中的智能指针(1):unique_ptr
一、背景
普通指针是指向某块内存区域地址的变量。如果一个指针指向的是一块动态分配的内存区域,那么即使这个指针变量离开了所在的作用域,这块内存区域也不会被自动销毁。动态分配的内存不进行释放则会导致内存泄漏。
如果一个指针指向的是一块已经被释放的内存区域,那么这个指针就是悬空指针。使用悬空指针会造成不可预料的后果。
如果我们定义了一个指针但未初始化使其指向有效的内存区域时,这个指针就成了野指针。使用野指针访问内存一般会造成段错误,即segmentation fault.
注:Segmentation fault:当程序试图访问未被分配的内存或无权访问的内存区域时,操作系统强制终止程序产生的错误。常见原因有:1.解引用空指针(上面所提到的);2.访问已释放的内存(悬空指针);3.数组越界;4.栈溢出(无限递归或过大的局部变量)。
使用智能指针可以有效避免上述错误的发生。智能指针是一个对象,它封装了一个指向另一个对象的指针。当智能指针对象离开了作用域后,会被自动销毁。销毁过程中会调用析构函数删除所封装的对象
二、unique_ptr
unique_ptr与它所管理的动态对象是一对一的关系,换言之,不能有两个unique_ptr对象同时指向相同的一块地址。
创建一个unique_ptr对象有两个方法:
unique_ptr<T> ptr1(new T(参数))
unique_ptr<T> ptr1= make_unique<T>(参数)
这里的T是一个类名。方法一:在构造函数中传入new分配的类对象。方法二:使用make_unique函数,它的参数是类T的构造函数的参数。
我们来看一个例子:
class Person
{
private:string name;int age;public:Person(string m_name, int m_age) :name(m_name), age(m_age){}~Person(){cout << "对象"<<name<<"被释放" << endl;}void check(){if (age < 18){cout << name << "的年龄为" << age << ",小于18岁" << endl;}else{cout << name << "的年龄为" << age << ",是成年人" << endl;}}
};int main()
{unique_ptr<Person>ptr1(new Person("比企谷", 17));unique_ptr<Person>ptr2 = make_unique<Person>("伊蕾娜", 18);ptr1->check();ptr2->check();return 0;
}
在main函数中我们定义了两个unique_ptr指针ptr1和ptr2,分别封装了两个动态创建的Person对象“比企谷”和“伊蕾娜”。由于智能指针重载了间接成员运算符和解引用运算符,它们会返回智能指针所包含对象的指针或者引用,因此可以像使用普通指针那样使用智能指针。例如上面的ptr1->check(),直接调用了Person对象的成员check.智能指针离开作用域后自动销毁,并调用析构函数释放指向的Person对象。
需要注意,下面三种情况也会删除当前所管理的对象:
ptr1=nullptr;
ptr1=move(ptr2);
ptr1.reset(new Person("雪之下雪乃",17));
上文说过,unique_ptr对所管理的资源具有独占性,所以unique_ptr的一个重要特性就是不能被拷贝也不能被赋值。
下面这段代码会在编译时出错:
unique_ptr<Person>ptr3=ptr2;
unique_ptr的类定义中没有这个拷贝构造函数。这样就保证了unique_ptr对象封装的指针不能和其它unique_ptr共用。但是上文提到了,我们可以对unique_ptr所管理对象的所有权进行转移,即:使用move函数。
unique_ptr<Person>ptr3=move(ptr2)
这样ptr3就拥有了原来ptr2所封装的指针的控制权。此时ptr2只包含了一个空指针,可以使用ptr3来访问它所封装的对象的成员。由于ptr2只包含了一个空指针,如果还使用ptr2访问成员,会出现segmentation fault.
再来看一个例子:
class energy
{
public:energy(){cout << "能量已充满" << endl;}~energy(){cout << "能量值为0" << endl;}
};unique_ptr<energy> fill()
{return unique_ptr<energy>(new energy());
}void consume(unique_ptr<energy>Energy)
{cout << "能量被消耗了" << endl;
}int main()
{cout << "开始" << endl;auto eng = fill();consume(eng);//错误的cout << "结束" << endl;
}
为什么consume(eng)是错误的?因为没有相应的拷贝构造函数,所以不能直接传值。正确方法如下:
consume(move(eng));
使用move函数将所有权转移给新的unique_ptr对象。
结果如下:
可以看到,这个engery对象被自动释放。
再来看一个例子:
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{bool operator()(const Packet& a, const Packet& b){return a.m_id < b.m_id;}
};void sort_value_vector(int n)
{vector<Packet>vec;for (int i = 0; i < n; i++){vec.push_back(Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}
对于Packet这种较大的对象,排序意味着需要大量数据的移动复制。因为一个Packet对象大概是1008个字节,每交换两个Packet对象就需要复制1008字节的数据。我们可以将容器中的对象改成指针,这样排序的时候只涉及到指针值的复制。(交换两个指针只需复制4/8个字节)
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{bool operator()(const Packet* pa, const Packet* pb){return pa->m_id < pb->m_id;}
};void sort_value_vector(int n)
{vector<Packet*>vec;for (int i = 0; i < n; i++){vec.push_back(new Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}
但是对于指针,需要单独进行维护。删除替换时需要释放不再使用的指针对象。不妨把容器中的指针换成unique_ptr,则不仅获得了普通指针的性能,还实现了内存资源的自动释放。
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{template<template<typename> typename ptr>bool operator()(const ptr<Packet>&pa, const ptr<Packet>&pb){return pa->m_id < pb->m_id;}
};template<typename ptr>
void sort_value_vector(int n)
{vector<ptr>vec;for (int i = 0; i < n; i++){vec.push_back(new Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}