C++ 学习笔记之(12) - 动态内存、智能指针和动态数组

C++ 学习笔记之(12) - 动态内存、智能指针和动态数组

程序中所使用的对象都有严格定义的生存期。

  • 全局对象:在程序启动时分配,程序结束时销毁
  • 局部自动对象:程序进入其定义所在块时创建,离开块时销毁
  • 局部static对象:第一次使用前分配,程序结束时销毁
  • 动态分配对象:显示创建,显示释放

内存存放区间

  • 静态内存:局部static对象、类static数据成员以及定义在任何函数之外的而变量
  • 栈内存:定义在函数内的非static对象
  • 自由空间(堆):动态分配对象,动态分配对象的生存期由程序控制

动态内存与智能指针

动态内存的管理是通过一对运算符完成的

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,用来对对象进行初始化
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

动态内存的使用容器出问题

  • 内存泄漏:忘记释放内存
  • 非法内存指针:在尚有指针引用内存的情况下就释放了呢村

新标准库提供了两种智能指针类型管理动态对象,智能指针与常规指针的区别在于它负责自动释放所指向的对象

  • shared_ptr:允许多个指针指向同一个对象
  • unique_ptr:独占所指向的对象
  • weak_ptr:一种弱引用,指向shared_ptr所管理的对象

shared_ptr

shared_ptr_and_unique_ptr_specific_operations

创建智能指针

shared_ptr<string> p1;  // shared_ptr, 可以指向 string
shared_ptr<list<int>> p2;  // shared_ptr, 可以指向 int 的 list

make_shared 函数

make_shared在动态内存中分配一个队形并初始化它,返回指向此对象的shared_ptr, 用其参数来构造给定类型的对象

// 指向一个值为“9999999999” 的 string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// 也可使用 auto, p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();

引用计数

  • 拷贝时计数器递增
    • 一个shared_ptr初始化另一个shared_ptr
    • shared_ptr作为参数传递给函数
    • shared_ptr作为函数的返回值
  • 计数器递减
    • shared_ptr赋予新值时
    • shared_ptr销毁时(比如局部shared_ptr对象离开其作用域)
auto r = make_shared<int>(42);  //  r 指向的 int 只有一个引用者
r = q;  // 给 r 赋值,令它指向另一个地址
        // 递增 q 指向的对象的引用计数
        // 递减 r 原来指向的对象的引用计数
        // r 原来指向的对象已没有引用者,会自动释放

shared_ptr销毁和释放内存

shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用技术变为0shared_ptr的析构函数就会销毁对象,并释放其占用的内存

void function(T arg)
{
    shared_ptr<int> p = make_shared<int>(125);
          // 情况1:不返回, p 为局部变量,函数结束时被销毁,p被销毁时,引用计数递减为0,故内存被释放
    return p;  // 情况2:返回 p 的拷贝, 引用计数进行递增操作,故即使 p 被销毁,但内存还有其他使用者,不会被释放
}

使用动态内存的原因

  • 程序不知道自己需要使用多少对象, 比如容器类
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据

直接管理内存

C++语言定义了两个运算符来分配和释放动态内存,相对于智能指针,直接管理容器出错

  • new:分配内存,并返回一个指向该对象的指针。

    // 默认情况下,动态分配对象是默认初始化的,即内置类型或组合类型的对象的值将未定义,类使用默认构造函数
    int *pi = new int;  // pi 指向一个动态分配的、未初始化的无名对象,即默认初始化,`*pi值未定义
    // 也可对动态分配对象进行值初始化,只需在类型名之后跟一对控括号
    int *pi2 = new int();  // 值初始化为0, *pi2 为0
    ///若括号中仅有单一初始化器,可使用 auto 推断想要分配的对象类型
    auto p1 = new auto(obj);  // p1类型是指针,指向从 obj 自动推断出的类型
    auto p2  new auto{a, b, c};  // 错误:括号中只能有单个初始化器
    //用 new 分配 const 对象是合法的, 下为分配并初始化一个 const int
    const int *pci = new const int(1024);
  • delete:销毁给定的指针指向的对象,释放对应的内存。必须是指向动态分配的指针,或空指针。释放一块并非new分配的内存,或者将相同的指针释放多次,其行为是未定义的

  • 空悬置镇:指向一块曾经保存数据对象但现在已经无效的内存的指针

shared_ptrnew结合使用

  • 接受指针参数的智能指针构造函数是explicit的,故不能讲一个内置指针隐式转换为智能指针,必须使用直接初始化方式

    shared_ptr<int> p1 = new int(1024);  // 错误:必须使用直接初始化方式
    shared_ptr<int> p2(new int(1024));  // 正确:使用了直接初始化方式
    

    shared_ptr_define_and_modify_other_operations

  • 不要混合使用普通指针和智能指针,因为内置指针访问智能指针所负责的对象是很危险的,因为无法知道对象何时会被销毁

  • 不要使用get初始化另一个智能指针或为智能指针赋值。get定义在智能指针类型中,迎来返回一个内置指针,指向智能指针管理的对象。

    shared_ptr<int> p(new int(42));  // 引用计数为 1 
    int *q = p.get();  // 正确:但使用 q 时要注意,不要让他管理的指针被释放
    {// 新程序块
        // 未定义:两个独立的`shared_ptr`指向相同的内存
      shared_ptr<int>(q)
    }// 程序块结束,q 被销毁,它指向的内存被释放
    int foot = *p;  // 未定义:p 指向的内存已经被释放了

智能指针和异常

  • 在发生异常时,智能指针仍能正确释放,但直接管理的内存不会自动释放

    void f()
    {
      shared_ptr<int> sp(new int(42));  // 分配一个新对象
        int *ip = new int(42);  // 动态分配一个新对象
        // 这段代码抛出一个异常,且在 f 中未被捕获
        delete ip;  // 在推出之前释放内存,发生异常时,没有执行,被跳过
    } // 在函数结束时 shared_ptr 自动释放内存
  • 删除器:释放 shared_ptr中保存的指针,用来取代shared_ptr被销毁时默认进行的delete操作,在创建shared_ptr时即可指定

  • 正确使用智能指针的接本规范

    • 不使用相同的内置指针值初始化(或reset)多个智能指针
    • delete``get()返回的指针
    • 不使用get()初始化或reset另一个智能指针
    • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,它会失效
    • 如果智能指针管理的资源表示new分配的内存,记住传递给他一个删除器

unique_ptr

unique_ptr拥有它所指向的对象,即某时刻只能有一个unique_ptr指向某对象,当其被销毁时,所指向的对象也被销毁

  • unique_ptr无类似make_shared函数。定义时,需要将其绑定到new返回的指针上

  • 类似shared_ptr,初始化unique_ptr需采用直接初始化形式

  • unique_ptr独占其指向的对象,故不支持普通的拷贝或赋值操作

    unique_ptr<string> p1(new string("hao"));  // p2 指向一个值为 hao 的 string
    unique_ptr<string> p2(p1);  // 错误:unique_ptr 不支持拷贝
    unique_ptr<string> p3;
    p3 = p2;  // 错误:unique_ptr 不支持赋值
  • unique_ptr虽然不能赋值或拷贝,但可以调用releasereset将所有权从非constunique_ptr转移到另一个unique_ptr

    unique_ptr_operations

    // 接上
    unique_ptr<string> p2(p1.release());  // release 将 p1 置为空,并将所有权转移给 p2
  • unique_ptr在将被销毁时可以拷贝或赋值, 比如从函数返回unique_ptr

    unique_ptr<int> clone(int p)
    {
      return unique_ptr<int>(new int(p));
    }
  • 类似shared_ptr, unique_ptr也可重载其默认的删除器

    // p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT对象
    // 它会调用一个名为 fcn 的 delT 类型对象
    unique_ptr<objT, delT> p (new objT, fcn);

weak_ptr

一种不控制所指向对象生存期的智能指针,指向由一个shared_ptr管理的对象。且绑定后不会改变shared_ptr的引用计数

weak_ptr

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp 弱共享 p; p 的引用计数未改变
// 由于对象可能不存在,故不能使用 weak_ptr 直接访问队形,而必须调用 lock
if(shared_ptr<int> np = wp.lock())  // 如果 np 不为空则条件成立
{
    // 在 if 中, np 与 p 共享对象
}

动态数组

C++语言和标准库提供了两种一次分配一个对象数组的方法。

  • C++ 语言定义了另一种new表达式语法,可以分配并初始化一个对象数组
  • 标准库提供了allocator类,允许分配和初始化分离,性能更好且更灵活

new和数组

  • new分配数组

    int *pia = new int[5];  // pia 指向第一个 int
    typedef int arrT[42];  // arrT 表示 42 个 int 的数组类型
    int *p = new arrT;  // 分配一个 42 个int 的数组; p 指向第一个 int
  • 虽然通常称new T[]分配的内存为动态数组,但并未得到数组类型对象,而是得到一个数组元素类型的指针,即动态数组并不是数组类型,不能使用某些函数比如beginend或范围for语句

  • 默认,new分配的对象,都执行默认初始化。也可以执行值初始化,只要在后面加()即可

    int *pia = new int[10];  // 10 个未初始化的 int
    int *pia2 = new int[10]();  // 10 个值初始化为 0 的 int
    int *pia3 = new int[10]{0, 1, 2, 3, 4, ,5};  // 列表初始化,剩余元素执行值初始化
  • 不能在括号中给出初始化器,即不能用auto分配数组

  • 释放动态数组时要在指针前加一个空方括号,即使使用类型别名定义数组,元素逆序销毁

    typedef int arrT[42];  // arrT 表示 42 个 int 的数组类型
    int *p = new arrT;  // 分配一个 42 个int 的数组; p 指向第一个 int
    delete [] p;  // 方括号必需,因为分配的是一个数组
  • 标准库提供了可管理new分配的数组的unique_ptr版本

    unique_ptr<int[]> up(new int[10]);  // up 指向一个包含10个未初始化 int 的数组
    up.release();  // 自动用 delete[] 销毁其指针

    unique_ptr_point_to_array

  • 若希望使用shared_ptr管理动态数组,必须提供自定义的删除器

    shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});  // 提供自定义删除器
    sp.reset();  // 使用自定义的 lambda 释放数组,它使用 delete[]

allocator

allocator类将内存分配和对象构造分离,其分配的内存是原始的、未构造的。

allocator_class_and_its_algorithms

allocator<string> alloc;  // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n);  // 分配 n 个未初始化的 string

auto q = p;  // q 指向最后构造的元素之后的位置
alloc.construct(q++);  // *q 为空字符串
alloc.construct(q++, 10, 'c');  // *q 为 cccccccccc
alloc.construct(q++, "hi");  // *q 为 hi

while(q != p)
    alloc.destroy(--q);  // 释放我们真正构造的 string

alloc.deallocate(p, n);  // 释放内存
  • 为了使用allocator返回的内存,必须用construct构造对象,使用位构造的内存,行为未定义

  • 使用完,必须对每个构造的元素调用destroy来销毁,destroy接受一个指针,对指向的对象执行析构函数

  • 销毁后,可重新使用这部分内存保存其他 string, 也可以释放内存还给系统

  • 拷贝和填充未初始化内存的算法

    allocator_algorithms

    vector<int> vi{1, 2, 3};
    allocator<int> alloc;
    auto p = alloc.allocate(vi.size() * 2);  // 分配比 vi 中元素所占空间大一倍的动态内存
    auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷贝vi中元素构造从p开始的元素
    uninitialized_fill_n(q, vi.size(), 42);  // 将剩余元素初始化为42

结语

在C++中,内存通过new 表达式分配,通过delete表达式释放。标准库还定义了一个``allocator类来分配动态内存块

现在C++程序应尽可能使用智能指针,因为直接管理很容易出错

猜你喜欢

转载自blog.csdn.net/u011221820/article/details/80157524