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
5.unique_ptr简介与常用操作
5.1 unique_ptr简介
讲解了不少shared_ptr的知识,但是谈到使用智能指针,一般来说,最先想到和优先考虑选择使用的还是unique_ptr智能指针。
unique_ptr智能指针是一种独占式智能指针,或者理解成专属所有权这种概念也可以,也就是说,同一时刻,只能有一个unique_ptr指针指向这个对象(这块内存)。当这个unique_ptr被销毁的时候,它所指向的对象也会被销毁。
(1)常规初始化(unique_ptr和new配合)
{
unique_ptr<int> pi; //可以指向int对象的一个空指针
if (pi == nullptr)//条件成立
{
cout << "pi目前还是空指针" << endl;
}
unique_ptr<int> pi2(new int(105)); //定义该智能指针时,直接把它绑定到一个new返回的指针上,此时pi2就指向一个值为105的int对象了
}
(2)make_unique函数
C++11中没有make_unique函数,但是C++14里提供了这个函数。
与常规初始化比,也是要优先选择使用make_unique函数,这代表着更高的性能。当然,后续会讲解“删除器”概念,如果想使用删除器,那么就不能使用make_unique函数,因为make_unique不支持指定删除器的语法。
{
unique_ptr<int> p1 = std::make_unique<int>(100);
auto p2 = std::make_unique<int>(200); //可以用auto简写
shared_ptr<int> p3(new int(100)); //int重复两次。而且不能使用auto来简写,不然p3就变成普通指针(裸指针)了而不是智能指针
}
5.2 unique_ptr常用操作
(1)unique_ptr不支持的操作
{
unique_ptr<string> ps1(new string("I Love China!"));
//unique_ptr<string> ps2(ps1); //不可以,该智能指针不支持拷贝动作
//unique_ptr<string> ps3 = ps1; //不可以,该智能指针不支持拷贝动作
//unique_ptr<string> ps4;
//ps4 = ps1; //不可以,该智能指针不支持赋值动作
}
总结:unique_ptr不允许复制、赋值等动作,是一个只能移动不能复制的类型。
(2)移动语义
虽然刚刚讲述了unique_ptr不支持的操作,如它不支持复制动作,但是它支持移动。看一看这种移动语义的写法。
可以通过std::move来将一个unique_ptr转移到其他的unique_ptr:
{
unique_ptr<string> ps1(new string("I Love China!"));
unique_ptr<string> ps3 = std::move(ps1); //转移后ps1为空了,ps3指向原来ps1所指
}
(3)release成员函数
放弃对指针的控制权(切断了智能指针和其所指向的对象之间的联系),返回指针(裸指针),将智能指针置空。返回的这个裸指针可以手工delete释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值。
{
//将所有权从ps1转移(移动)给ps2:
unique_ptr<string> ps1(new string("I Love China!"));
unique_ptr<string> ps2(ps1.release());
if (ps1 == nullptr)//条件成立
{
cout << "ps1被置空" << endl;
}
//ps2.release(); //这会导致内存泄漏
string* tempp = ps2.release(); //或者写成auto tempp = ps.release();
delete tempp;
}
(4)reset成员函数
当reset不带参数时,释放智能指针指向的对象,并将智能指针置空。当reset带参数时,释放智能指针原来所指向的内存,让该智能指针指向新内存。
{
unique_ptr<string> prs(new string("I Love China!"));
prs.reset(); //当reset()不带参数时,释放prs指向的对象,并将prs置空
if (prs == nullptr)//条件成立
{
cout << "prs被置空" << endl;
}
}
{
unique_ptr<string> prsdc(new string("I Love China 1!"));
unique_ptr<string> prsdc2(new string("I Love China 2!"));
//当prsdc2.reset(......)中带参数时,释放prsdc2原来所指向的内存,让prsdc2指向新内存
prsdc2.reset(prsdc.release()); //reset释放原来prsdc2指向的对象内存,让prsdc2指向prsdc所指向的内存,同时prsdc被置空
prsdc2.reset(new string("I Love China!")); //reset参数可以是个裸指针,reset释放原来prsdc2指向的对象内存,让prsdc2指向新new出来的string
}
(5)=nullptr;
释放智能指针所指向的对象,并将智能指针置空。
{
unique_ptr<string> ps1(new string("I Love China!"));
ps1 = nullptr; //释放ps1指向的对象,并将ps1置空
}
(6)指向一个数组
{
std::unique_ptr<int[]> ptrarray(new int[10]); //前面带上空括号[]表示是数组,下面行才可以用[下标]来引用数组元素
ptrarray[0] = 12; //数组提供索引运算符[]
ptrarray[1] = 24;
ptrarray[9] = 124; //能访问的下标是0-9,不要超过这个范围,否则可能导致程序异常
}
class A
{
public:
A()
{
}
~A() //有自己的析构函数
{
}
};
{
//std::unique_ptr<A> ptrarray(new A[10]); //一个类A的数组,而且类A有析构函数,但前面的<>中没有使用A[],就报异常。原因在16.2.1的5中已经解释了
std::unique_ptr<A[]> ptrarray(new A[10]); //这个写法没有问题,也不会泄露内存,注意前面的<>中正常的书写为A[]
}
(7)get成员函数
返回智能指针中保存的对象(裸指针)。小心使用,若智能指针释放了所指向的对象,则返回的对象也就变得无效了
{
unique_ptr<string> ps1(new string("I Love China!"));
string* ps = ps1.get();
const char* p1 = ps->c_str();
*ps = "This is a test very good!";
const char* p2 = ps->c_str(); //调试观察不难发现p1和p2是不同的内存地址,这是string内部工作机制决定的
}
为什么要有这样一个函数呢?主要是考虑到有些函数的参数需要的是一个内置指针(裸指针),所以需要通过get取得这个内置指针并传递给这样的函数。但要注意,不要delete这个get到的指针,否则会产生不可预料的后果。
(8)*解引用
*p:解引用的感觉,获得p指向的对象。
{
unique_ptr<string> ps1(new string("I Love China!"));
const char* p1 = ps1->c_str();
*ps1 = "This is a test very good!";
const char* p2 = ps1->c_str();//调试观察不难发现p1和p2是不同的内存地址,这是string内部工作机制决定的
std::unique_ptr<int[]> ptrarray(new int[10]); //对于定义的内容是数组,是没有*解引用运算符的
//*ptrarray; //错误
}
(9)swap成员函数
用于交换两个智能指针所指向的对象
{
unique_ptr<string> ps1(new string("I Love China1!"));
unique_ptr<string> ps2(new string("I Love China2!"));
std::swap(ps1, ps2); //用全局函数也可以
ps1.swap(ps2); //也可以
}
(10)智能指针名字作为判断条件
{
unique_ptr<string> ps1(new string("I Love China1!"));
//若ps1指向一个对象,则为true
if (ps1) //条件成立
{
//执行
cout << "ps1指向了一个对象" << endl;
}
}
(11)转换成shared_ptr类型
如果unique_ptr为右值,就可以将其赋给shared_ptr。模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr,shared_ptr将接管原来归unique_ptr所拥有的对象。
auto myfunc()
{
return unique_ptr<string>(new string("I Love China!")); //这就是一个右值(短暂的临时对象,都是右值,14.12.4中详细说过)
}
{
shared_ptr<string> pss1 = myfunc(); //可以成功,引用计数为1
}
另外前面讲过,一个shared_ptr创建的时候,它的内部指针会指向一个控制块,当时讲解过这个控制块创建时机,那么,把unique_ptr转换成shared_ptr的时候,系统也会为这个shared_ptr创建控制块。因为unique_ptr并不使用控制块,只有shared_ptr才使用控制块。
{
unique_ptr<std::string> ps(new std::string("I Love China!"));
shared_ptr<string> ps2 = std::move(ps); //执行后ps为空,ps2是shared_ptr且引用计数为1
}
5.3 返回unique_ptr、删除器与尺寸问题
(1)返回unique_ptr
虽然上面说过,unique_ptr智能指针不能复制。但有一个例外,如果这个unique_ptr将要被销毁,则还是可以复制的,最常见的是从函数返回一个unique_ptr。
unique_ptr<string> tuniqp()
{
unique_ptr<string> pr(new string("I Love China!"));
return pr; //从函数返回一个局部unique_ptr对象是可以的 返回局部对象pr会导致系统生成临时unique_ptr对象,并调用unique_ptr的移动构造函数
}
unique_ptr<string> tuniqp()
{
return unique_ptr<string>(new string("I Love China!"));
}
{
unique_ptr<string> ps;
ps = tuniqp(); //可以用ps接收tuniqp返回结果,则临时对象直接构造在ps里,如果不接收,则临时对象会释放,同时释放掉所指向的对象的内存
}
(2)删除器
默认情况下,当析构一个unique_ptr时,如果这个智能指针非空(指向一个对象),则在unique_ptr内部,会用delete来删除unique_ptr所指向的对象(裸指针)。所以这里的delete可以看成是unique_ptr智能指针的默认删除器。程序员可以重载这个默认的删除器,换句话说,就是提供一个自己的删除器,提供的位置就在unique_ptr的尖括号“<>”里面,并在所指向的对象类型之后。
那么,这个删除器究竟是什么呢?其实就是一个可调用对象,可调用对象在15.3.3节中有所提及,在后面的章节也会更详细地讲解。简单来说,例如:函数是可调用对象的一种,另外,如果一个类中重载了“()”运算符,就可以像调用函数一样来调用这个类的对象,这也叫可调用对象。
前面已经学习过shared_ptr的删除器,shared_ptr的删除器指定比较简单,在参数中书写一个具体删除器名(如函数名、lambda表达式等)就可以了。
而unique_ptr的删除器相对复杂一点,多了一步——先要在类型模板参数中传递进去类型名,然后在参数中再给具体的删除器名。看一看删除器的写法和使用方法。
1>范例。
void mydeleter(string* pdel)
{
delete pdel;
pdel = nullptr;
}
{
typedef void(*fp)(string*); //定义一个函数指针类型,类型名为fp
unique_ptr<string, fp> ps1(new string("I Love China!"), mydeleter);
}
2>做个修改。
{
using fp2 = void(*)(string*); //用using定义一个函数指针类型,类型名为fp2
unique_ptr<string, fp2> ps2(new string("I Love China!"), mydeleter);
}
3>继续在main主函数中做修改。
{
typedef decltype(mydeleter)* fp3; //注意这里多了个*,因为decltype是返回函数类型,加*表示函数指针类型,现在fp3应该是void *(string *),decltype后面会讲
unique_ptr<string, fp3> ps3(new string("I Love China!"), mydeleter);
}
4>继续在main主函数中做修改。
{
std::unique_ptr<string, decltype(mydeleter)*> ps4(new string("I Love China!"), mydeleter);
}
5>改用lambda表达式的写法再看看
{
auto mydella = [](string* pdel) {
delete pdel;
pdel = nullptr;
};
std::unique_ptr<string, decltype(mydella)> ps5(new string("I Love China!"), mydella);
int ilen = sizeof(ps5);
}
指定删除器额外说明
还记得学习shared_ptr的时候曾说过:就算是两个shared_ptr指定的删除器不相同,只要它们所指向的对象相同,那么这两个shared_ptr也属于同一个类型。
但是unique_ptr不同,指定unique_ptr中的删除器会影响unique_ptr的类型。因为在unique_ptr中,删除器类型是智能指针类型的一部分(在“<>”里),所以从这一点来讲,shared_ptr的设计更灵活。
在讲解shared_ptr的时候,删除器不同,但指向类型(所指向对象的类型)相同的shared_ptr,可以放到同一个容器中。但到了unique_ptr这里,如果删除器不同,则就等于整个unique_ptr类型不同,那么,这种类型不同的unique_ptr智能指针没有办法放到同一个容器中去的。
(3)尺寸问题
通常情况下,unique_ptr的尺寸与裸指针一样
{
string* p;
int ilenp = sizeof(p); //4(字节)
unique_ptr<string> ps1(new string("I Love China!"));
int ilen = sizeof(ps1); //4(字节)
}
可以看到,unique_ptr也基本和裸指针一样:足够小,操作速度也够快。但是,如果增加了删除器,那unique_ptr的尺寸可能不变化,也可能有所变化。
1.如果删除器是lambda表达式这种匿名对象,unique_ptr的尺寸就没变化。
2.如果删除器是一个函数,unique_ptr的尺寸就会发生变化。
unique_ptr尺寸变大肯定对效率有一定影响,所以把一个函数当作删除器,还是要慎用。这一点与shared_ptr不同,shared_ptr是不管指定什么删除器,其大小都是裸指针的2倍。
智能指针总结
智能指针背后的设计思想
智能指针主要的目的就是帮助程序员释放内存,以防止忘记释放内存时造成内存泄漏。实际上,作为一个严谨的程序员,写出内存泄漏的代码当然是不应该的,但作为新手程序员,内存泄漏这样的事情确实是时有发生。
void myfunc()
{
string* ps = new std::string("I Love China!");
//....
//....
if (true) //当某个条件为真,就return,忘记释放内存导致泄漏
{
return;
}
delete ps; //释放内存
return;
}
这种本不应该出现的内存泄漏在已然发生的情况下,智能指针的好处就体现出来了。通过修改一下代码解决内存泄漏问题。这里笔者用C++98中的auto_ptr演示,读者可以先理解成auto_ptr和已经讲解的unique_ptr一样。
{
std::auto_ptr<std::string> ps(new std::string("I Love China!"));
if (true) //当某个条件为真,就return,忘记释放内存导致泄漏
{
return;
}
//delete ps; //释放内存
return;
}
代码经过上面的修改,就不需要担心忘记delete造成内存泄漏的问题了。所以,这里面的自动释放内存就是智能指针背后设计的思想。
auto_ptr为什么被废弃
auto_ptr是C++98时代的智能指针,具有unique_ptr的部分特性。实际上,在C++11新标准之前,也只有auto_ptr这么一个智能指针。而unique_ptr、shared_ptr、weak_ptr都是C++11新标准出现后才出现的。
auto_ptr有些使用上的限制(缺陷),如不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr。所以,在C++11新标准中,auto_ptr已经被unique_ptr取代(在支持C++11新标准的编译器上,读者也不要再使用auto_ptr了)。
{
//std::auto_ptr<std::string> ps(new std::string("I Love China!"));
//std::auto_ptr<std::string> ps2 = ps; //ps2指向字符串,ps变为空,这可以防止ps和ps2析构一个string两次,所以这个代码没问题
//std::shared_ptr<std::string> ps(new std::string("I Love China!"));
//std::shared_ptr<std::string> ps2 = ps; //ps2和ps都有效,引用计数为2
//std::unique_ptr<std::string> ps(new std::string("I Love China!"));
//std::unique_ptr<std::string> ps2 = ps;
std::unique_ptr<std::string> ps(new std::string("I Love China!"));
std::unique_ptr<std::string> ps2 = std::move(ps); //要用移动语义了
}
虽然auto_ptr和unique_ptr都是独占式智能指针,但unique_ptr这种编译的时候就会报错,而不会默默地就把ps的所有权转移到ps2上去的方式,避免了后续误用ps导致程序崩溃的问题。
不难看出,auto_ptr被废弃的主要原因就是设计的不太好,容易被误用引起潜在的程序崩溃等问题,所以C++11中使用unique_ptr来取代auto_ptr,因为unique_ptr比auto_ptr使用起来更安全。
智能指针的选择
如果程序中要使用多个指向同一个对象的指针,应选择shared_ptr。
如果程序中不需要多个指向同一个对象的指针,则可使用unique_ptr。
总之,在选择的时候,优先考虑使用unique_ptr,如果unique_ptr不能满足需求,再考虑使用shared_ptr。