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. windows 下搭建安装 sass

    众所周知,sass 解析需要有 ruby 的支撑,所以, 第一步:点我下载 ruby: 第二步:安装 ruby: 在安装 ruby 过程中需要注意的一点:把 ruby 执行文件添加到 path,勾选这 ...

  2. 解决eclipse oxygen+java 10+Tomcat的Could not create the Java virtual machine问题

        本文首发于cartoon的博客     转载请注明出处:https://cartoonyu.github.io/cartoon-blog     这个坑我遇到了两次了,所以就写下来以防自己再遇 ...

  3. python3键盘输入

    1.脚本 # -*- coding: utf-8 -*- print("今年是哪一年?"),year = input("年份:")print ("ji ...

  4. 【Android】error: Error retrieving parent for item: No resource found that matches the given name 'Theme.Sherlock.Light.NoActionBar'.

    问题: res 文件夹下的 values 下的 styles.xml <style name="Sherlock.Light.NoActionBar" parent=&quo ...

  5. ASP.NET Core - 实现自定义WebApi模型验证

    Framework时代 在Framework时代,我们一般进行参数验证的时候,以下代码是非常常见的 [HttpPost] public async Task<JsonResult> Sav ...

  6. Something wrong with EnCase v8 index search results

    My friend told me that she installed EnCase v8.05 on her workstation which OS version is Win 10. She ...

  7. java 8中新的日期和时间API

    java 8中新的日期和时间API 使用LocalDate和LocalTime LocalDate的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息.另外,它也不附带任何与时区相关的信 ...

  8. 按需制作最小的本地yum源

    [需求背景] 有时候客户的环境里面只能离线安装文件,此时可以使用CentOS的ISO光盘作为本地源进行安装,或者是制作一个包含了YUM源服务的虚拟机. 无论上面的哪一种方式都不够轻量,我们自己的组件可 ...

  9. Myeclipse8.5上基于JAX-WS开发WebService

    1.JAX-WS介绍 JAX-WS规范是一组XML web services的JAVA API. 2.开发步骤 基于JAX-WS的WebService开发步骤如下: 2.1 新建一个Web Servi ...

  10. 为什么双重检查锁模式需要 volatile ?

    双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量.这个模式还可以用来创建单例.下面来看一个 Spring 中双重检查锁定的例子. 这个例子 ...