程序使用三种不同的内存

  1. 静态内存:static成员以及任何定义在函数之外的变量
  2. 栈内存:一般局部变量
  3. 堆内存(自由空间):动态分配的对象

静态内存和栈内存中的变量由编译器产生和销毁,动态分配的对象在我们不再使用它时要由程序员显式地销毁

一、介绍

动态分配内存

  1. new():为对象分配空间,并返回指向该对象的指针
  2. delete:销毁对象,并释放与之相关的内存

使用智能指针:定义在头文件memory

  1. shared_ptr:允许多个指针指向同一个对象
  2. unique_ptr:“独占”所使用的对象
  3. weak_ptr:伴随类,弱引用,指向shared_ptr所管理的对象

和容器一样,只能指针也是一种模板,需要给它传入一个参数来指定类型

二、shared_ptr类

声明shared_ptr:

  1. shared_ptr<string> p1; //shared_ptr,可以指向string
  2. shared_ptr<list<int>> p2; //shared_ptr,可以指向list<int>

使用方式与普通指针一致,解引用返回它所指向的对象,在条件表达式中检查是否为空

  1. //若p1不为空且指向一个空string
  2. if(p1 && p1->empty()){
  3. *p1 = "hi"; //对p1重新赋值
  4. }

make_shared函数

make_shared<typename>(arguments)

在动态内存中分配并初始化一个对象

返回指向此对象的shared_ptr指针

  1. //指向一个值为42的int的shared_ptr
  2. shared_ptr<int> p1 = make_shared<int>(42);
  3. //指向一个值为"999"的string的shared_ptr
  4. shared_ptr<string> p2 = make_shared<string>(3, '9');
  5. //指向一个值为0的int的shared_ptr
  6. shared_ptr<int> p3 = make_shared<int>();

没有传入参数时,进行值初始化

  1. auto p4 = make_shared<string>(); //p4指向空string

shared_ptr的拷贝和引用

每个share_ptr都有一个关联的计数器

  • 当拷贝shared_ptr时,计数器会递增
  • 当shared_ptr被赋予新值或者shared_ptr被销毁(如一个局部的shared_ptr离开其作用域),计数器会递减
  • 当一个shared_ptr的计数器==0时,内存会被释放
  1. auto r = make_shared<int>(42);
  2. r = q; //给r赋值,使它指向另一个地址
  3. //递增q指向的对象的引用计数
  4. //递减r指向的对象的引用计数
  5. //如果计数器为0,自动释放

shared_ptr自动销毁所管理的对象…

和其他类一样,shared_ptr类型也有析构函数

shared_ptr的析构函数会

  1. 递减指针所指向的对象的引用计数
  2. 当对象的引用计数为0时,销毁对象并释放内存

…shared_ptr还会自动释放相关联对象的内存

举例:

  1. //factory返回一个share_ptr,指向一个动态分配的对象
  2. shared_ptr<Foo> factory(T arg){
  3. //对arg的操作
  4. return make_shared<Foo>(arg);
  5. }
  6. void ues_factory(T arg){
  7. shared_ptr<Foo> p = factory(arg);
  8. //使用p
  9. }
  10. //p离开了作用域,由于引用计数由1减到0,对象被销毁,内存释放

如果有其他引用计数也指向该对象,则对象内存不会被释放掉

  1. //factory和上述一致
  2. //ues_factory返回shared_ptr的拷贝
  3. void use_factory(T arg){
  4. shared_ptr<Foo> p = factory(arg);
  5. //使用p
  6. return p; //返回p的拷贝,此时递增了计数器,引用数为2
  7. }//p离开作用域,对象计数器引用2-1=1,对象内存没有释放

return shared_ptr时,如果不是返回引用类型,则会进行拷贝,shared_ptr的计数器+1后-1,最终shared的计数器不变

由于在最后一个shared _ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。

share_ptr 在无用之后仍然保留的一种可能情况是,你将shared _ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。

如果你将shared ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

使用动态生存期的资源的类

程序使用动态内存的三种原因

  1. 程序不知道自己需要使用多少对象
  2. 不知道所需对象的准确类型
  3. 需要在多个对象间共享数据

容器类常出于第一种原因使用动态内存,在15章会看见出于第二种原因的例子,本节讨论第三种原因

先考虑这么一种情况:

我们要定义一个Blob类,当该类型的对象拷贝时,对象共享底层数据。

如b2 = b1时,b2,b1共享底层数据,对b2的操作也会印象到b1,且销毁b2时,b1的仍指向原数据

  1. Blob<string> b1; //空Blob
  2. {
  3. //新作用域
  4. Blob<string> b2 = {"a","an","the"};
  5. b1 = b2; //b1和b2共享数据
  6. }//b2离开作用域,被销毁了,但b2的数据不能被销毁
  7. //b1指向b2的原数据

应用举例:Blob类

定义Blob类

最终,我们希望将Blob定义为一个模板类,但现在我们先将其定义为StrBlob,即底层数据是vector<string>的Blob

  1. class StrBlob{
  2. public:
  3. //拷贝控制
  4. StrBlob();//默认构造函数
  5. StrBlob(initializer_list<string> il); //列表初始化
  6. StrBlob(const StrBlob& strb);
  7. //查询
  8. int size() const {return data->size();}
  9. bool empty() const {return data->empty();}
  10. //添加和删除元素
  11. void push_back(const string &t) {data->push_back(t);}
  12. void pop_back() {data->pop_back();}
  13. //访问元素
  14. string& front();
  15. string& back();
  16. private:
  17. shared_ptr<vector<string>> data;
  18. //如果data[i]不合法,抛出异常
  19. void check(int i, const string &msg) const;
  20. };

StrBlob的构造函数

  1. StrBlob::StrBlob() : data(make_shared<vector<string>>())
  2. {cout<<"in StrBlob dafault"<<endl;};
  3. StrBlob::StrBlob(initializer_list<string> il) :
  4. data(make_shared<vector<string>>(il))
  5. {cout<<"in StrBlob initializer_list"<<endl;}

元素访问成员函数

在访问时必须保证容器非空,定义check函数进行检查

  1. void StrBlob::check(int i, const string& msg) const{
  2. if(i >= data->size())
  3. throw out_of_range(msg);
  4. }

元素访问成员函数:

  1. string& StrBlob::front(){
  2. //如果vector为空,check会抛出一个异常
  3. check(0, "front on empty StrBlob");
  4. return data->front();
  5. }
  6. string& StrBlob::back(){
  7. check(0, "back on empty StrBlob");
  8. return data->back();
  9. }

StrBlob的拷贝、赋值和销毁

StrBlob使用默认的拷贝、赋值和析构函数对此类型的对象进行操作

当我们对StrBlob对象进行拷贝、赋值和销毁时,它的shared_ptr成员也会默认地进行拷贝、赋值和销毁

  1. //由于data是private的
  2. //在StrBlob中设置一个接口look_data
  3. //look_data返回data的引用
  4. class StrBlob{
  5. public:
  6. //...
  7. shared_ptr<vector<string>>& look_data()
  8. {return data;} //返回引用,避免对象拷贝
  9. private:
  10. //其余部分都不变
  11. };

测试程序:

  1. //测试程序
  2. int main(){
  3. StrBlob b1;
  4. {//新作用域
  5. StrBlob b2 = {"first element","second element"};
  6. cout<<"before assignment : "
  7. <<b2.look_data().use_count()<<endl;
  8. b1 = b2;
  9. cout<<"after assignment : "
  10. <<b2.look_data().use_count()<<endl;
  11. }//b2被销毁,计数器递减
  12. //b1仍指向b2的原数据
  13. cout<<b1.front()<<endl;
  14. //打印此时b1的计数器
  15. cout<<"b2 has been dstoryed : "
  16. <<b1.look_data().use_count()<<endl;
  17. return 0;
  18. }

输出结果:

如果look_data值返回,而不是引用返回,那么会存在拷贝【见6.2.2节笔记】,所有计数器的值会+1

三、直接管理内存

使用new分配内存

  • new分配动态内存
  • delete销毁动态内存

new和delete与智能指针不同,类对象的拷贝、赋值和销毁操作都不会默认地对动态分配的对象进行管理,无论是对象的创建还是销毁,都需要程序员显式地操作,在大型的应用场景中会十分复杂。

在熟悉C++拷贝控制之前,尽量只使用智能指针,而不是本节的方法管理动态内存

使用new动态分配和初始化对象

new type_name:返回一个指向该对象的指针

  1. //pi指向一个动态分配,默认初始化的无名对象
  2. int *pi = new int;
  3. //*pi的值是未定义的
  4. cout<<*pi<<endl;

对象是默认初始化这意味着:

  1. 指向的是:内置类型和组合类型对象。对象的值是未定义的

  2. 指向的是:类类型对象。调用默认构造函数

可以直接初始化动态分配的对象

  • 直接调用构造函数
  • 列表初始化
  1. //pi指向对象的值为42
  2. int *pi = new int(42);
  3. //"9999999999"
  4. string *ps = new string(10, '9');
  5. //vector有5个元素,依次为0,1,2,3,4
  6. vector<int> *pv = new vector<int>{0,1,2,3,4};

也可以值初始化

  1. string *ps1 = new string(); //值初始化为空string
  2. string *ps = new string; //默认初始化为空string
  3. int *pi1 = new int; //默认初始化,值未定义
  4. int *pi = new int(); //值初始化,*pi = 0;

所以,初始化动态分配的对象是一个好习惯

动态分配const对象

new可以分配const对象

和其他const对象一样,动态分配的const对象必须被初始化

  1. //分配并初始化const int
  2. const int *pi = new const int(1024);
  3. //分配并默认初始化const string
  4. const string *ps = new const string;

内存耗尽

如果new分配动态内存失败,返回一个空指针,并报出std::bad_alloc异常

  1. int *p1 = new int; //返回空指针,抛出异常
  2. int *p2 = new (nothrow) int; //如果分配失败,new返回空指针

我们第二种形式的new为定位new (placement new),其原因我们将在19.1.2节(第729页)中解释。

定位new表达式允许我们向new传递额外的参数

在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的 new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。

使用delete释放内存

基本介绍

delete():接受一个指针,指向我们想要销毁的对象

执行两个操作

  • 销毁对象
  • 释放对应的内存

注意点:

  1. 保证只传给delete动态分配的指针,将一般指针传给delete,其行为是未定义的
  2. 同一块内存不能释放两次
  3. 不要忘记delete内存
  4. 不要使用已经delete的对象
  1. int i, *pi = &i;
  2. int *pd = new int();
  3. delete pd; //正确:释放pd内存
  4. pd = nullptr; //好习惯:指出pd不再指向动态内存
  5. delete pi; //未定义:pi没有指向动态分配的内存
  6. delete pd; //未定义:pd内存已经被释放

保证以上两点是程序员的责任,编译器并不会检查以上错误

举例

在被显式地delete前,用new动态分配的内存一直存在

  1. Foo* factory(T arg){
  2. //处理arg
  3. return new Foo(arg);
  4. }//调用者负责释放
  5. void ues_factory(T arg){
  6. Foo *p = factory(arg);
  7. //使用p但不delete它
  8. }//p离开了作用域,但它所指向的内存没有被释放!!

use_factory返回时,局部变量p被销毁。但此变量是一个内置指针,而不是一个智能指针,所以p所指向的内存并没有被销毁

这样就产生了一块无名的内存块,存在又无法删除。

这也体现了智能指针与普通指针的区别:智能指针在离开自己的作用域,自己的变量名失效时,销毁指向的对象并释放关联内存;而new产生的指针不会。

修改use_factory:

  1. void use_factory(T arg){
  2. Foo *p = factory(arg);
  3. //使用p
  4. delete p; //记得释放p
  5. }

坚持使用智能指针,可以避免上述的绝大部分问题

四、shared_ptr和new结合使用

new直接初始化share_ptr

可以用new返回的指针初始化share_ptr

构造函数是explicit

所以,不存在new产生的指针向shared_ptr的隐式类型转换,必须采用直接初始化,而不是拷贝初始化或者赋值

  1. shared_ptr<int> p1(new int(42)); //正确:使用直接初始化
  2. shared_ptr<int> p2 = new int(30);//错误:new产生的指针

同理,返回shared_ptr的函数不能返回new产生的指针

  1. shared_ptr<int> clone(int p){
  2. //错误:构造函数为explicit,无法转换
  3. //return new int(p);
  4. //正确:显式地用int*构造shared_ptr<int>
  5. return shared_ptr<int>(new int(p));
  6. }

如对隐式类型转换有疑问查看 7-5笔记第三点”隐式类类型转换”

初始化时传入可调用对象代替delete

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete。我们将在12.1.4节介绍如何定义自己的释放操作。

五、unique_ptr

和shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象

基本操作

必须采用直接初始化

  1. unique_ptr<double> p1; //可以指向double的一个unique_ptr
  2. unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int

unique_ptr不支持拷贝与赋值

  1. unique_ptr<string> p1(new string("hello"));
  2. unique_ptr<string> p2(p1); //错误:不支持拷贝
  3. unique_ptr<string> p3;
  4. p3 = p1; //错误:不支持赋值

unique_ptr支持的操作

可以使用release和reset将指针的所有权从一个(非const)unique_ptr转移到另一个unique_ptr

  1. //将所有权从p1,转移到p2
  2. unique_ptr<string> p1(new string("hello"));
  3. unique_ptr<string> p2(p1.release()); //release将p1置空
  4. cout<<*p2<<endl; //输出 hello
  5. unique_ptr<string> p3(new string("world"));
  6. //p2绑定的对象被释放,p3置空,p2指向p3原来指向的对象
  7. p2.reset(p3.release());
  8. cout<<*p2<<endl; //输出: world

传递和返回unique_ptr

不能拷贝unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

  1. unique_ptr<int> clone(int p){
  2. //正确:从int*创建一个unique_ptr<int>
  3. return unique_ptr<int>(new int(p));
  4. }

还可以返回一个局部变量的拷贝

  1. unique_ptr<int> clone(int p){
  2. unique_ptr<int> ret(new int(p));
  3. return ret;
  4. }

对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,我们将在13.6.2节(移动构造函数和移动运算符)中介绍它。

向unique_ptr传递删除器

  1. //p指向一个类型为objT的对象
  2. //并使用一个类型为delT的可调用对象释放objT
  3. //p会使用一个名为fcnd的delT对象来删除objT
  4. unique_ptr<objT, delT> p(new objT, fcn);

作为一个更具体的例子,我们将写一个连接程序,用unique_ptr来代替shared_ptr,如下所示:

  1. void f(destination &d /*其他需要的参数*/)
  2. {
  3. connection c = connect(&d);//打开链接
  4. unique_ptr<connection, decltype(end_connection)*>
  5. p(&c, end_connection);
  6. //使用链接
  7. //当f退出时(即使是由于异常而退出)
  8. //connection会调用end_connection正常退出
  9. }

注意decltype(end_connection)返回一个函数类型,而函数类型不能作为参数,函数指针可以

所以要加上*表示函数指针

p(&c, end_connection)中,类似于数组名表示指针一样,函数名实际上就表示函数指针

所以也可写作p(&c, &end_connection),但没必要。【前一个&表示引用传递,后一个&表示取址得到指针】

c++动态内存管理与智能指针的更多相关文章

  1. 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱

    必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱 十三.小心使用智能指针.         在前面几节已经很详细了介绍了智能指针适用方式.看起来,似乎智能指针很强大,能够很方便很安全的管理 ...

  2. 字符串输出输入函数,const修饰符,内存分区,动态内存管理,指针和函数,结构体

    1.字符串输出输入函数 读入字符串的方法: 1) scanf 特点:不能接收空格 2) gets 特点:可以接受含有空格的字符串 ,不安全 3) fgets(); 特点:可以帮我们自动根据数组的长度截 ...

  3. C++ Primer : 第十二章 : 动态内存之动态内存管理(new和delete)

    C++语言定义了两个运算符来分配和释放动态内存:运算符new分配内存,运算符delete释放new分配的内存. 运算符new和delete 使用new动态分配和初始化对象 在自由空间分配的内存是无名的 ...

  4. C++动态内存管理之shared_ptr、unique_ptr

    C++中的动态内存管理是通过new和delete两个操作符来完成的.new操作符,为对象分配内存并调用对象所属类的构造函数,返回一个指向该对象的指针.delete调用时,销毁对象,并释放对象所在的内存 ...

  5. Keil C动态内存管理机制分析及改进(转)

    源:Keil C动态内存管理机制分析及改进 Keil C是常用的嵌入式系统编程工具,它通过init_mempool.mallloe.free等函数,提供了动态存储管理等功能.本文通过对init_mem ...

  6. FreeRTOS 动态内存管理

    以下转载自安富莱电子: http://forum.armfly.com/forum.php 本章节为大家讲解 FreeRTOS 动态内存管理,动态内存管理是 FreeRTOS 非常重要的一项功能,前面 ...

  7. C++程序设计入门 引用和动态内存管理学习

    引用: 引用就是另一个变量的别名,通过引用所做的读写操作实际上是作用于原变量上. 由于引用是绑定在一个对象上的,所以定义引用的时候必须初始化. 函数参数:引用传递 1.引用可做函数参数,但调用时只需 ...

  8. 动态内存管理详解:malloc/free/new/delete/brk/mmap

    c++ 内存获取和释放 new/delete,new[]/delete[] c 内存获取和释放 malloc/free, calloc/realloc 上述8个函数/操作符是c/c++语言里常用来做动 ...

  9. 动态内存管理---new&amp;delete

    动态内存管理 动态对象(堆对象)是程序在执行过程中在动态内存中用new运算符创建的对象. 因为是用户自己用new运算符创建的.因此也要求用户自己用delete运算符释放,即用户必须自己管理动态内存. ...

随机推荐

  1. python2.7发送邮件失败之——邮箱安全问题

    使用python2.7发送邮件,通过脚本调试,脚本运行通过成功发出了邮件,但是目标邮箱qq没有收到. 刚开始怀疑脚本问题,上网查找资料后,发现邮箱发送成功后目标邮件没有收到有可能有以下几种原因: 1. ...

  2. 在KALI以外的Linux上安装KALI上的工具(ubuntu,debian)

    添加KALI源 vim /etc/apt/sources.list 在sources.list中加入 deb http://http.kali.org/kali kali-rolling main c ...

  3. protobuf详解

    protobuf的基本类型和默认值,python中的小坑 标量数值类型 标量消息字段可以具有以下类型之一--该表显示了.原型文件,以及自动生成类中的对应类型: 默认值 python操作的坑 目录结构 ...

  4. golang中的RPC开发-2

    RPC简介 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程 如果 ...

  5. golang中time包日期时间常用用法

    package main import ( "fmt" "reflect" "time" ) var week time.Duration ...

  6. JVM之Java内存区域

    JVM之Java内存区域 世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程. 一.JAVA内存区域 谈及JAVA虚拟机运行时数据区域就不得不祭出这张经典的图了: ...

  7. 【Azure 应用服务】Azure Mobile App (NodeJS) 的服务端部署在App Service for Windows中出现404 Not Found -- The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

    问题描述 使用NodeJS的后端应用,开发一个Mobile App的服务端,手机端通过REST API来访问获取后端数据.在本地编译好后,通过npm start启动项目,访问效果如下: 但是,当把项目 ...

  8. json模块 os模块 文件加密

    目录 一:random随机模块 二:os模块 三:文件处理选择任意视频 四:sys模块 五:实现文件执行加密操作 六:json 序列化模块 七:json序列化 反序列化 八:json 文件写读方式 九 ...

  9. redis分析系列之set命令

    前言 最近研究下redis源码,现在从最基本的命令行操作来分析,redis是如何处理命令操作的 1. redis的set命令操作 我们在redis-cli执行下面的命令 set c c debug 发 ...

  10. Spring系列7:`autowire`自动装配怎么玩

    回顾 前几篇我们介绍各种依赖依赖注入,都是显式指定的,配置明确但同时也有些繁杂和重复."很多发明的出发点,都是为了偷懒,懒人是推动社会进步的原动力".Spring 提供了自动注入依 ...