1. 介绍

  一般一个程序在内存中可以大体划分为三部分——静态内存(局部的static对象、类static数据成员以及所有定义在函数或者类之外的变量)、栈内存(保存和定义在函数或者类内部的变量)和动态内存(实质上这块内存池就是堆,通常通过new/malloc操作申请的内存)。对于静态内存和栈内存来说,编译器可以根据它们的定义去自动创建和销毁的相应的内存空间。而对于动态内存,由于程序只有在运行时才知道需要分配多少内存空间,所以只能由程序员去动态的去创建和回收这块内存。

  而对于动态内存的回收是一个很复杂的问题,经常会因为一些难以观察的细节遗忘对一些对象的释放造成内存泄露,比如下面的代码:

#include <iostream>
#include <exception>
using namespace std;
class myException : public exception
{
public:
const char* what_happened() const throw(){
return "error: what you have down is error.";
}
}; void check(int x){
if(x == 0){
throw myException();
}
} int main(){
string* str = new string("testing....");
try {
check(0);
//do something I really want to do
// ....
} catch (myException &e) {
cout << e.what_happened() << endl;
return -1;
}
delete str;
return 0;
}

  一旦项目的代码量非常庞大时,此时像这样的内存泄露即便可以通过一些内存检测工具(比如valgrind),但去定位并改正这些错误还是很繁琐的。

  为了更方便且更安全的使用动态内存C++提供了四种智能指针来动态管理这些对象——auto_ptr(C++98,现在基本被淘汰),unique_ptr,shared_ptr,weak_ptr(后三种是C++11的新标准)。上面的程序改成如下形式,同时去掉delete str;就可以了。

std::auto_ptr<std::string> ps(new string("testing...."));

2.智能指针

  使用智能指针,需要引入头文件#include <memory>,接受参数的智能指针的构造函数是explict,如下

template<typename _Tp>
class auto_ptr
{
private:
_Tp* _M_ptr; public:
explicit
auto_ptr(element_type* __p = 0) throw() : _M_ptr(__p) { }
//....
}

  因此不能自动将指针转换为智能指针对象,而是采用直接初始化的方式来初始化一个指针,显示的创建对象。如下:

shared_ptr<std::string> ps(new string("testing...."));	  //正确
shared_ptr<std::string> ps = new string("testing...."); //错误

同时,应该避免把一个局部变量的指针传给智能指针:

//error —— double free or corruption (out): 0x00007fffffffd910 ***
string s("testing.....");
shared_ptr<string> pvac(&s); //correct
string* str = new string("testing....");
shared_ptr<string> pvac(str);

局部变量s是在栈上分配的内存,且其作用域范围仅限于当前函数,一旦执行完,该变量将被自动释放,而智能指针shared_ptr又会自动再次调用s的析构函数,导致一个变量double free。而new方式申请的内存在堆上,该部分的内存不会随着作用域范围的结束而被释放,只能等到智能指针调用析构函数再去释放。

题外话——隐式类型转换

  隐式类型转换可:能够用一个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。如下面程序:

#include <string>
#include <iostream>
using namespace std ;
class BOOK
{
private:
string _bookISBN ;
float _price ; public:
//这个函数用于比较两本书的ISBN号是否相同
bool isSameISBN(const BOOK& other){
return other._bookISBN==_bookISBN;
} //类的构造函数,即那个“能够用一个参数进行调用的构造函数”(虽然它有两个形参,但其中一个有默认实参,只用一个参数也能进行调用)
BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}
}; int main()
{
BOOK A("A-A-A");
BOOK B("B-B-B");
cout<<A.isSameISBN(B)<<endl; //正常地进行比较,无需发生转换
cout<<A.isSameISBN(string("A-A-A"))<<endl; //此处即发生一个隐式转换:string类型-->BOOK类型,借助BOOK的构造函数进行转换,以满足isSameISBN函数的参数期待。
cout<<A.isSameISBN(BOOK("A-A-A"))<<endl; //显式创建临时对象,也即是编译器干的事情。 return 0;
}

  此处发生了一个隐式类型转换,将一个string类型转化成了BOOK类,如果要阻止该类型的转换,可以将构造函数定义成如下形式:

explicit BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}

现在,我们只能显示的类型转换和显示的去创建BOOK对象。

2.1 auto_ptr

  auto_ptr是旧版gcc的智能指针,现在新版本的已经将其摒弃,如下程序:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
int main(){
auto_ptr<string> day[7] = {
auto_ptr<string>(new string("Monday")),
auto_ptr<string>(new string("Tudsday")),
auto_ptr<string>(new string("Wednesday")),
auto_ptr<string>(new string("Thursday")),
auto_ptr<string>(new string("Friday")),
auto_ptr<string>(new string("Saturday")),
auto_ptr<string>(new string("Sunday"))
};
//将Saturday的值赋给today
auto_ptr<string> today = day[5];
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
cout << *day[i] << " ";
}
cout << endl;
return 0;
}

对于上面程序,会发现,编译的时候,没有什么问题,可以当运行的时候就会发生段错误。上面有两个变量day[5]和today都指向同一内存地址,当这两个变量的在这个作用域范围失效时,就会调用各自的析构函数,造成同一块内存被释放两次的情况。为了避免这种情况,在auto_ptr中有一种所有权的概念,一旦它指向一个对象后,这个对象的所有权都归这个指针控制,但是如果此时又有一个新的auto_ptr指针指向了这个对象,旧的auto_ptr指针就需要将所有权转让给新的auto_ptr指针,此时旧的auto_ptr指针就是一个空指针了,上面的程序通过调试可以看出这些变量值的变化过程。

程序可以编译通过,但运行时会出错,这种错误在项目中去查找是一件很痛苦的事情,C++新标准避免潜在的内存崩溃问题而摒弃了auto_ptr。

2.2 unique_ptr

  unique_ptr和auto_ptr类似,也是采用所有权模型,但是如果同样的程序,只是把指针的名字换了一下:

int main(){
unique_ptr<string> day[7] = {
unique_ptr<string>(new string("Monday")),
unique_ptr<string>(new string("Tudsday")),
unique_ptr<string>(new string("Wednesday")),
unique_ptr<string>(new string("Thursday")),
unique_ptr<string>(new string("Friday")),
unique_ptr<string>(new string("Saturday")),
unique_ptr<string>(new string("Sunday"))
};
//将Saturday的值赋给today
unique_ptr<string> today = day[5];
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
cout << *day[i] << " ";
}
cout << endl;
return 0;
}
/* 编译阶段就会报错
smart_ptr.cpp:17:37: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]’
unique_ptr<string> today = day[5];
*/

可以看出unique比auto_ptr更加安全,在编译阶段就可以提前告知错误,而且unique_ptr还有一个很智能的地方,就是虽然不允许两个unique_ptr的赋值操作,但是允许在函数返回值处去接受这个类型的指针,如下: 

unique_ptr<string> test(const char* c){
unique_ptr<string> temp(new string(c));
return temp;
}
int main(){
unique_ptr<string> ptr;
ptr = test("haha");
return 0;
}

  如果确实想让两个unique_ptr进行赋值操作,可以调用标准库函数std::move()函数,它可以实现对象资源的安全转移,如下:  

unique_ptr<string> today = std::move(day[5]);
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
cout << *day[i] << " ";
}
cout << endl;

  上面的代码虽然可以安全编译过,day[5]将资源所有权转移到today上,会造成像auto_ptr一样出现访问day[5]这个空指针异常的错误。

2.3 shared_ptr

  现在将上面的代码换成shared_ptr:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
shared_ptr<string> test(const char* c){
shared_ptr<string> temp(new string(c));
return temp;
}
int main(){
shared_ptr<string> day[7] = {
shared_ptr<string>(new string("Monday")),
shared_ptr<string>(new string("Tudsday")),
shared_ptr<string>(new string("Wednesday")),
shared_ptr<string>(new string("Thursday")),
shared_ptr<string>(new string("Friday")),
shared_ptr<string>(new string("Saturday")), //指向new string("Saturday")计数器为1
shared_ptr<string>(new string("Sunday"))
};
//将Saturday的值赋给today
shared_ptr<string> today = day[5]; //指向new string("Saturday")计数器为2
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
cout << *day[i] << " ";
}
cout << endl;
return 0;
}
/* output
today is Saturday
Monday Tudsday Wednesday Thursday Friday Saturday Sunday
*/

我们会惊讶的发现这个程序是可以正常的跑过的,而且day[5]也是可以正常打印出来的,原因在于share_ptr并不是采用所有权机制,当有多个share_ptr指向同一对象时,它就会向java的垃圾回收机制一样采用引用计数器,赋值的时候,计数器加1,而指针对象过期的时候,计数器减1,直到计数器的值为0的时候,才会调用析构函数将对象的内存清空。

shared_ptr内存也可以这样申请:

std::shared_ptr<ClassA> p1 = std::shared_ptr<ClassA>();
std::shared_ptr<ClassA> p2 = std::make_shared<ClassA>();

  

第一种方式会先申请A类对象所需的空间,然后再去申请针对对该空间控制的内存控制块。而第二种方式是数据块和控制块会一块申请,所以它的效率会更高一点。

2.4 wek_ptr

先来看一个例子,假设有两个对象,他们之间重存在这相互引用的关系:

#include <iostream>
#include <memory>
#include <vector>
using namespace std; class ClassB; class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
shared_ptr<ClassB> pb; // 在A中引用B
}; class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
shared_ptr<ClassA> pa; // 在B中引用A
}; int main02() {
//也可以通过make_shared来返回一个shared_ptr对象,它的效率会更高
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函数结束:spa和spb会释放资源么?
return 0;
} /** valgrind 一部分报告
==812== LEAK SUMMARY:
==812== definitely lost: 32 bytes in 1 blocks
==812== indirectly lost: 32 bytes in 1 blocks
==812== possibly lost: 0 bytes in 0 blocks
==812== still reachable: 72,704 bytes in 1 blocks
*/

  

使用valgrind可以看出确实造成了内存泄露,因为ClassA和ClassB相互循环的引用对方,造成各自的引用计数器都会加1,使得最终析构函数调用无法将其置为0。

这个时候可以用到wek_ptr,weak_ptr是一种“弱”共享对象的智能指针,它指向一个由share_ptr管理的对象,讲一个weak_ptr绑定到shared_ptr指向的对象去,并不会增加对象的引用计数器的大小,即使weak_ptr还指向某一个对象,也不会阻止该对象的析构函数的调用。这个时候需要判断一个对象是否存在,然后才可以去访问对象,如下代码:

class C
{
public:
C() : a(8) { cout << "C Constructor..." << endl; }
~C() { cout << "C Destructor..." << endl; }
int a;
};
int main() {
shared_ptr<C> sp(new C());
weak_ptr<C> wp(sp);
if (shared_ptr<C> pa = wp.lock())
{
cout << pa->a << endl;
}
else
{
cout << "wp指向对象为空" << endl;
}
sp.reset(); //reset--释放sp关联内存块的所有权,如果是最后一个指向该资源的(引用计数为0),就释放这块内存
//wp.lock()检查和shared_ptr绑定的对象是否还存在
if (shared_ptr<C> pa = wp.lock())
{
cout << pa->a << endl;
}
else
{
cout << "wp指向对象为空" << endl;
}
}
/* output
C Constructor...
8
C Destructor...
wp指向对象为空
*/

  然后将最开始的程序改成如下形式,则可以避免循环引用而造成的内存泄漏问题。

class ClassB;

class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
weak_ptr<ClassB> pb; // 在A中引用B
}; class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
weak_ptr<ClassA> pa; // 在B中引用A
}; int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函数结束,思考一下:spa和spb会释放资源么?
return 0;
} /* valgrind报告
==5401== LEAK SUMMARY:
==5401== definitely lost: 0 bytes in 0 blocks
==5401== indirectly lost: 0 bytes in 0 blocks
==5401== possibly lost: 0 bytes in 0 blocks
==5401== still reachable: 72,704 bytes in 1 blocks
==5401== suppressed: 0 bytes in 0 blocks
*/

参考资料

  1. C++ Primer(第五版)

  1. C++智能指针简单剖析

  2. C++ 隐式类类型转换

  3. 【C++11新特性】 C++11智能指针之weak_ptr

  

C++11——智能指针的更多相关文章

  1. c++11 智能指针 unique_ptr、shared_ptr与weak_ptr

    c++11 智能指针 unique_ptr.shared_ptr与weak_ptr C++11中有unique_ptr.shared_ptr与weak_ptr等智能指针(smart pointer), ...

  2. C++11智能指针之std::unique_ptr

    C++11智能指针之std::unique_ptr   uniqut_ptr是一种对资源具有排他性拥有权的智能指针,即一个对象资源只能同时被一个unique_ptr指向. 一.初始化方式 通过new云 ...

  3. 【C++11新特性】 C++11智能指针之weak_ptr

    如题,我们今天要讲的是C++11引入的三种智能指针中的最后一个:weak_ptr.在学习weak_ptr之前最好对shared_ptr有所了解.如果你还不知道shared_ptr是何物,可以看看我的另 ...

  4. 详解C++11智能指针

    前言 C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用. C++11智能指针介 ...

  5. C++11 智能指针

    C++ 11标准库引入了几种智能指针 unique_ptr shared_ptr weak_ptr C++内存管理机制是当一个变量或对象从作用域过期的时候就会从内存中将他干掉.但是如果变量只是一个指针 ...

  6. C++11智能指针

    今晚跟同学谈了一下智能指针,突然想要看一下C++11的智能指针的实现,因此下了这篇博文. 以下代码出自于VS2012 <memory> template<class _Ty> ...

  7. C++11智能指针的深度理解

    平时习惯使用cocos2d-x的Ref内存模式,回过头来在控制台项目中觉得c++的智能指针有点生疏,于是便重温一下.首先有请c++智能指针们登场: std::auto_ptr.std::unique_ ...

  8. C++11智能指针 share_ptr,unique_ptr,weak_ptr用法

    0x01  智能指针简介  所谓智能指针(smart pointer)就是智能/自动化的管理指针所指向的动态资源的释放.它是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动 ...

  9. C++11智能指针原理和实现

    一.智能指针起因 在C++中,动态内存的管理是由程序员自己申请和释放的,用一对运算符完成:new和delete. new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针: delete:指向 ...

随机推荐

  1. 博弈论基础之sg函数与nim

    在算法竞赛中,博弈论题目往往是以icg.通俗的说就是两人交替操作,每步都各自合法,合法性与选手无关,只与游戏有关.往往我们需要求解在某一个游戏或几个游戏中的某个状态下,先手或后手谁会胜利的问题.就比如 ...

  2. Python文件的两种用途

    目录 一.Python文件的两种用途 一.Python文件的两种用途 python文件总共有两种用途,一种是执行文件:另一种是被当做模块导入. 编写好的一个python文件可以有两种用途: 脚本,一个 ...

  3. WGAN的改进点和实操

    包含三部分:1.WGAN改进点  2.代码修改  3.训练心得 一.WGAN的改进部分: 判别器最后一层去掉sigmoid    (相当于最后一层做了一个y = x的激活) 生成器和判别器的loss不 ...

  4. 《css的总结》

    一.span标签:能让某几个文字或者某个词语凸显出来 <p> 今天是11月份的<span>第一天</span>,地铁卡不打折了 </p> 二.字体风格 ...

  5. thinkphp 插件

    1.切换到项目根目录,使用composer require 5ini99/think-addons:dev-master命令安装thinkphp插件 如果是root用户或是管理员执行的话会有提示 等一 ...

  6. case和decode的用法(行转列)

    创建了一张成绩表,如下图所示: 在oracle中,这两个函数我们都可以使用,代码及结果如下: decode用法: select Name,decode(Subject,'语文',1,'数学',2,'英 ...

  7. 几个linux下的命令

    sudo apt-get insall -f 修复依赖关系 sudo apt-get update    更新源 sudo apt-get upgrade 更新已经安装的包 sudo apt-get ...

  8. iDevice取证的一大突破

    近日手机取证领域传出令人震撼的消息,知名取证大厂Cellebrite宣称可破解任何版本,任何机型的iDevice,连最新的iPhone X也逃不过. 若真属实,代表着iOS的取证又重现光明.只是不确定 ...

  9. what is the CCA?

    Clear Channel Assessment (CCA) is one of two carrier sense mechanisms in WLAN (or WiFi). It is defin ...

  10. javascript数组去重 js数组去重

    数组去重的方法 一.利用ES6 Set去重(ES6中最常用) function unique (arr) { return Array.from(new Set(arr)) } var arr = [ ...