【c++ primer 笔记】第12章 动态内存

简介: 对象的生命周期: - 全局对象在程序启动时分配,结束时销毁。 - 局部对象在进入程序块时创建,离开块时销毁。 - 局部`static`对象在第一次使用前分配,在程序结束时销毁。 - 动态分配对象:只能显式地被释放。

动态内存

  • 对象的生命周期:

    • 全局对象在程序启动时分配,结束时销毁。
    • 局部对象在进入程序块时创建,离开块时销毁。
    • 局部static对象在第一次使用前分配,在程序结束时销毁。
    • 动态分配对象:只能显式地被释放。
  • 对象的内存位置:

    • 静态内存:用来保存局部static对象、类static对象、定义在任何函数之外的变量。
    • 栈内存:用来保存定义在函数内的非static对象。
    • 堆内存:又称自由空间,用来存储动态分配的对象。

12.0 补充知识

堆内存和栈内存的比较:

  1. 控制权:

    1. 栈由编译器自动分配和释放;
    2. 堆由程序员分配和释放
  2. 空间大小:

    1. 栈:windows下,栈是向低地址扩展的,是连续的内存区域,所以栈顶地址和栈的最大容量都是确定的,似乎一般是 2M 或 1M
    2. 堆:堆是向高地址扩展的,是不连续的内存区域。系统是用链表来存储空闲地址的。堆的大小由计算机的有效虚拟内存决定,因此空间大得多
  3. 分配效率:

    1. 栈:速度较快。
    2. 堆:速度较慢,但使用方便
  4. 系统响应:

    1. 栈:如果剩余空间不足,异常提示栈溢出
    2. 堆:在记录空闲地址的链表中寻找空间大于所申请空间的堆结点,然后将该结点从空闲节点链表中删除。一般会在首地址处记录本次分配空间的大小。
  5. 存储内容:

    1. 栈:存储函数的各个参数、局部变量、函数返回地址等。第一个进栈的就是函数返回地址
    2. 堆:内容由程序员决定。

12.1 动态内存与智能指针

c++ 使用 new 和 delete 管理动态内存

  • new:在堆中为对象分配空间并返回指向该对象的指针
  • delete:接受一个动态对象的指针,销毁该对象并释放内存。

动态内存使用出现的问题

  1. 忘记释放内存会引起内存泄漏
  2. 释放了后继续引用指针会引用非法内存。

智能指针

  • 定义在头文件 memory
  • shared_ptr:允许多个指针指向一个对象
  • unique_ptr:独占所指的对象
  • weak_ptr:一种弱引用,指向 shared_ptr 所管理的对象

智能指针作用

  • 管理动态对象,行为类似常规指针,负责自动释放所指向的对象,智能指针也是模板。

12.1.1 shared_ptr类

  • 智能指针实际上是一个类模板。但是它的操作与指针十分相似,在创建模板时,必须提供指针指向的类型。
shared_ptr<string> p1;       // 可以指向 string
shared_ptr<vector<int>> p2;  // 可以指向 int 的 vector

默认初始化的智能指针中保存着空指针。

定义shared_ptr方式

  • 可以使用另一个 shared_ptr 或一个 unique_ptr 或 new 的指针来初始化一个 shared_ptr。
shared_ptr<int> p;              // 默认初始化为空指针
shared_ptr<int> p(q);           // q 也是一个 shared_ptr,p 是 q 的拷贝,此操作会递增 q 中的计数器。
shared_ptr<int> p(qnew);        // qnew 是一个指向动态内存的内置指针(qnew = new int;))
shared_ptr<int> p(u);           // u 是一个 unique_ptr。p 从 u 接管了对象的所有权,u 被置为空
shared_ptr<int> p(q, deleter);  // q 是一个内置指针。p 将使用可调用对象 deleter 来代替 delete
shared_ptr<int> p(p2, deleter); // p2 是一个 shared_ptr,p 是 p2 的拷贝,唯一的区别是 p 将可调用对象 d 来代替 delete。

auto p = make_shared<int>(10);  //返回一个 shared_ptr,指向一个初始化为 10 的动态分配的 int 对象。注意不同于 make_pair

shared_ptr 操作

sp              // 智能指针作为 if 的判断条件时检查其是否为空,若 sp 指向一个对象,则为 true
sp->mem;        // 等价于 (*p).mem。用于当 sp 指向一个类时
sp.get();       // 返回 sp 中保存的指针。要小心使用!
swap(p, q);     // 交换 p 和 q 中的指针
p.swap(q);      // 同上

p = q;          // 此操作会递增 q 中的计数器,递减 p 原来的计数器,若其变为 0,则释放。
p.unique();     // 若 p.use_count() 为 1,返回 true,否则返回 false
p.use_count();  // 返回与 p 共享对象的智能指针数量。可能运行很慢,主要用于调试

p.reset();     // 将 p 置为空,如果 p 计数值为 1,释放对象。
p.reset(q);    // q 是一个内置指针,令 p 指向 q。
p.reset(q, d); // 调用可调用对象 d 而不是 delete 来释放 q

make_shared 函数

  • 尽量使用make_shared函数来分配内存
  • 通常用 auto 来定义一个对象保存 make_shared 的结果。
  • make_shared 是函数模板,要提供模板参数
shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2 = make_shared<int>();   //值初始化
auto p3 = make_shared<string>(10,'s');

shared_ptr的拷贝和赋值

  • 每个 shared_ptr 都有一个关联的计数器,如果拷贝一个 shared_ptr,计数器就会递增。
  • 当给share_ptr赋予新值或者shared_ptr被销毁(一个局部sgared_ptr离开作用域)时,计数器就会递减
  • 如果 shared_ptr 的计数器变为 0,就会自动释放管理的对象。
auto r = make_shared<int>(42);  // r 指向的 int 只有一个引用者
r = q; // 给 r 赋值,令它指向另一个地址。
    //这会递增 q 指向的对象的引用计数,
    //递减 r 原来指向的对象的引用计数。
    //因为 r 原来指向的对象没有已经没有引用者,所以会自动释放。

shared_ptr 自动销毁所管理的对象

  • shared_ptr 通过析构函数来完成销毁。
  • shared_ptr 的析构函数会递减对象的引用计数,如果计数变为 0,则销毁对象并释放内存。

shared_ptr 自动释放相关联的内存

1661157412620.png

  • 由于最后一个 shared_ptr 销毁前内存都不会释放,所以要保证 shared_ptr 无用之后就不要再保留了。
  • 如果忘记销毁不再需要的 shared_ptr,程序不会出错,但会浪费内存。
  • 一种常量的情况是将 shared_ptr 存放在一个容器中,后来其中有一部分元素不再用到了,这时要注意用 erase 删除不需要的元素。
  • `每个类都有析构函数。析构函数控制对象销毁时执行什么操作。

析构函数一般用来释放对象分配的资源。如 vector 的析构函数销毁它的元素并释放内存。`

使用动态内存的三种情况

  1. 程序不知道需要使用多少对象。例如容器类
  2. 程序不知道对象的准确类型。
  3. 程序需要在多个对象间共享内存。

使用动态内存在多个对象间共享内存

  • 定义一个类,类的数据成员为一个 shared_ptr。使用此 shared_ptr 来管理一个 vector,即可实现在多个类对象间共享同一个 vector。
  • 当所有类对象都被销毁时 vector 才会被销毁。注意一个类只会与它的拷贝共享一个 vector,单独定义的两个类是不共享的。

案例:定义StrBlob类

  • StrBlob 类是使用动态内存在多个对象间共享内存
  • 将vector保存在动态内存中
  • StrBlob 类中仅有一个 shared_ptr 成员,这个 shared_ptr 指向一个 string 的 vector。
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <exception>

using std::vector; using std::string;

class StrBlob {
public:
    using size_type = vector<string>::size_type;      // 灵活使用类型别名

    StrBlob():data(std::make_shared<vector<string>>()) { }
    StrBlob(std::initializer_list<string> il):data(std::make_shared<vector<string>>(il)) { }  //定义了一个接受初始化列表的转换构造函数(注意不是 explicit 的)

    size_type size() const { return data->size(); }   // size() 函数不改变数据成员,所以声明为 const 的
    bool empty() const { return data->empty(); }      // 声明为 const 的

    void push_back(const string &t) { data->push_back(t); }
    void pop_back() {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }

    std::string& front() {
        check(0, "front on empty StrBlob");
        return data->front();
    }

    std::string& back() {
        check(0, "back on empty StrBlob");
        return data->back();
    }

    const std::string& front() const {       //在普通的 front() 函数外又重载了一个 const 的版本
        check(0, "front on empty StrBlob");
        return data->front();
    }
    const std::string& back() const {       //在普通的 back() 函数外又重载了一个 const 的版本
        check(0, "back on empty StrBlob");
        return data->back();
    }

private:
    void check(size_type i, const string &msg) const {   //定义了一个 check 函数来检查索引是否超出边界
        if (i >= data->size()) throw std::out_of_range(msg);  //不检查 i 是否小于 0 是因为 i 的类型是 size_type,是无符号类型,如果 i<0 会被自动转换为大于 0 的数
    }

private:
    std::shared_ptr<vector<string>> data;
};

注意点

  1. 对于不改变类的成员的函数,要声明为 const 的。
  2. 对于 front(), back() 等返回成员的函数,既要定义返回普通引用的版本,也要定义返回常量引用的版本。返回常量引用的版本要声明为 const 的,这样才能成功地进行重载,不然只有返回值类型不同,编译器无法区分。
  3. check 函数不检查 i 是否小于 0 是因为 i 的类型是 size_type,是无符号类型,如果 i<0 会被自动转换为大于 0 的数
  4. 这里的接受 initializer_list 的转换构造函数没有定义为 explicit 的,这样的好处是使用方便,可以进行隐式的转换。缺点是不易调试。

12.1.2 直接管理内存

  • 可以使用 new 和 delete 来直接管理内存。相比于智能指针,它们非常容易出错。
  • 自己直接管理内存的类不能依赖默认合成的拷贝控制成员,通常都需要自己定义。而使用了智能指针的类则可以使用默认合成的版本。

注意

  • new

    动态分配和初始化对象。

    • new无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。
    • int *pi = new int(123);
    • 一旦内存耗尽,会抛出类型是bad_alloc的异常。
  • delete

    将动态内存归还给系统。

    • 接受一个指针,指向要释放的对象。
    • delete后的指针称为空悬指针(dangling pointer)。
  • 使用newdelete

    管理动态内存存在三个常见问题:

    • 1.忘记delete内存。
    • 2.使用已经释放掉的对象。
    • 3.同一块内存释放两次。

默认情况下,动态分配的对象会被默认初始化,也可以直接初始化列表初始化值初始化来初始化动态分配的对象

int* p = new int;   //默认初始化
string* sp = new string(10,'g');//直接初始化
vector<int>* vp = new vector<int>{0,1,2,3};//列表初始化

'对于类来说,值初始化与默认初始化没有什么区别,对于内置类型来说,值初始化对象会有一个良好的值,默认初始化对象值未定义。'
int* p1 = new int;   // 默认初始化,p1 所指对象的值是未定义的
int* p2 = new int(); // 值初始化,p2 所指对象的值初始化为 0

1661157442874.png

1661157467726.png

1661157483724.png

释放动态内存

delete执行两个动作:

  1. 销毁指针所指对象(但没有销毁指针本身)
  2. 释放对应内存
delete p; // p 必须指向一个动态分配的对象或是一个空指针
  • 释放一个不是动态分配的指针和相同的指针释放多次的行为都是未定义的。
  • 通常编译器不能分辨 delete 的对象是动态还是静态分配的对象,也不能分辨一个指针所指的内存是否已被释放。
  • 动态对象直到被显式释放前都是存在的。

两种特殊情况:

1.指针不在内存还在

  1. 当指针是一个局部变量,因超出作用域而被销毁时,其指向的动态内存不会自动释放。当没有指针指向这块内存时,就无法再释放了。这就是忘记 delete 产生的内存泄漏的问题。

2. 指针还在内存不在

  1. delete一个指针后,指针值已经无效,但是指针还是保存着地址,此时就变成了空悬指针。
  • 有两个解决方法

    1. delete 之后将指针置为空指针
    2. 在指针作用域的末尾 delete

1661157524717.png

1661157540091.png

导致内存泄漏

bool b() {
    int* p = new int; // p 是一个 int 型指针
    return p;     // 函数返回值是 bool 类型,将 int 型指针转换为 bool 类型会使内存无法释放,造成内存泄漏
}

12.1.3 shared_ptr和new结合使用

  • 可以使用new来初始化智能指针
  • 接受指针参数的智能指针构造参数是 explicit 的,因此不能将内置指针隐式地转换为智能指针。必须使用直接初始化,不能进行拷贝初始化(=,也就是赋值)。
shared_ptr<double> p2 = new int(1024); // 错误:转换构造函数是 explicit 的,不能隐式转换
shared_ptr<double> p1(new int(1024));  // 正确:调用了转换构造函数
  • 默认情况下一个用来初始化智能指针的普通指针只能指向动态内存,因为智能指针默认使用 delete 释放对象。
  • 如果将智能指针绑定到一个指向其他类型资源的指针上,要定义自己的删除器(函数) 来代替 delete

不要混用智能指针和普通指针

  • shared_ptr 可以协调对象的析构,但仅限于自身的拷贝之间。这就是推荐使用 make_shared 而不是 new 的原因。

使用普通指针(即 new 返回的指针)来创建一个 shared_ptr 有两个易错之处:

  1. 使用普通指针创建 shared_ptr 后,又使用该普通指针访问动态对象。普通指针并不知道该对象何时被 shared_ptr 所释放,随时可能变成空悬指针。
  2. 使用同一个普通指针创建了多个 shared_ptr ,这就将同一块内存绑定到多个独立创建的 shared_ptr 上了。

当将一个 shared_ptr 绑定到一个普通指针后,就不要再用内置指针来访问所指内存了。

使用一个内置指针来访问一个智能指针所负责的对象是危险的,因为我们无法知道对象何时会被销毁

不要使用 get 初始化另一个智能指针或为智能指针赋值

  • 智能指针的get返回一个内置指针,指向智能指针所管理的对象,主要用于向不能使用智能指针的代码传递一个内置指针
shared_ptr<int> p(new int(42));
int* q = p.get();   // 这是正确的,但是使用get返回的指针代码不能delete指针

俩种错误情况

  1. 使用get返回的指针代码不能delete指针
  2. 不要使用 get 初始化另一个智能指针或为智能指针赋值。

shared_ptr 的关联计数只应用于自己的拷贝,如果使用某智能指针的 get 函数初始化另一个智能指针,两个指针的计数是不关联的,销毁一个就会直接释放内存使另一个成为空悬指针。

错误的例子

auto p = make_shared<int>();
auto q = p.get();
delete q;   //错误,这会造成 double free。

其他shared_ptr操作
1661157554915.png

12.1.4 智能指针和异常

  • 使用异常处理的程序能在异常发生后令程序流程继续它需要确保在异常发生后资源能被正确地释放,一种简单的方法是使用智能指针。
  • 使用智能指针时发生异常,智能指针管理的内存会被释放掉,而如果是直接管理内存时,在 new 和 delete 之间发生了异常,则内存不会被释放。
'shared_ptr<int>p'
void f()
{
    shared_ptr<int> sp(new int(42));
    //这段代码抛出异常,且在f中未被捕获
}   //在函数结束时shared_ptr自动释放内存

'new delete'
void f()
{
    int *ip = new int(42);
    //这段代码抛出异常,且在f中未被捕获
    delete ip;
}   //如果在New和delete之间发生异常,且未被捕获,则内存永远不会释放

智能指针和哑类

  • 所有标准库类都定义了析构函数,负责清理对象使用的资源。但是那些为 C 和C++ 两种语言设计的类,通常都没有良好定义的析构函数,必须显式释放资源。
  • 如果在资源分配和释放之间发生了异常,或程序员忘记释放资源,程序也会发生资源泄漏。
  • 例如网络连接中的在释放连接前发生了异常,那么连接就不会被释放了。
struct destination
struct connection
connection connect(destination*);
void disconnect(connection);
void f(destination &d)
{
    connection c  = connect(&d);
    //如果我们在f退出前忘记调用disconnect,就无法关闭c了
}

使用我们自己的释放操作

  • 默认情况下,shared_ptr 假定它们指向的是动态内存,在销毁时会使用 delete 操作。
  • 为了使用 shared_ptr 管理其他对象,如网络连接,这时就需要定义一个函数来代替delete,这个函数就称为删除器
share_ptr<T> p(&t, deleter);  //deleter 必须是一个接受一个 T* 类型参数的函数

使用 shared_ptr 管理网络连接

shared_ptr<connection> p(&c, end_connection);// end_connection 是 p 的删除器,它接受一个 connection* 参数
struct destination
struct connection
connection connect(destination*);
void disconnect(connection);
void end_connection(connection *p){disconnection(*p)};
void f(destination &d)
{
    connection c  = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    //如果我们在f退出(即使由于异常退出),connection会被正常关闭
}

智能指针陷阱
1. 不使用相同的内置指针值初始化或 reset 多个智能指针
2. 不 delete get() 返回的指针
3. 不使用 get() 初始化或 reset 另一个智能指针
4. 如果使用 get() 返回的指针,当最后一个对应的智能指针被销毁后,指针就变为无效的了
5. 如果智能指针管理的不是 new 分配的内存,记住传递给它一个删除器

12.1.5 unique_ptr

  • 某一个时刻只能有一个unique_ptr指向一个给定的对象。(与shared_ptr不同)
  • 不支持拷贝或者赋值操作
  • 当 unique_ptr 被销毁时,指向对象也被销毁。
  • 类似 shared_ptr,初始化 unique_ptr 必须采用直接初始化。(这里指使用 new 初始化)
  • 定义 unique_ptr 时,需要绑定到一个 new 返回的指针上

unique_ptr定义和初始化

unique_ptr<int> u1;             // 定义一个空 unique_ptr
unique_ptr<int> u1(new int());  // 正确
unique_ptr<int,deleter> u;      // 定义一个空 unqiue,用可调用对象 deleter 代替 delete
unique_ptr<int,deleter> u(d);   // 空 unique,用类型为 deleter 的对象 d 代替delete
unique_ptr<int,decltype(d)*> u(new int(42),d);

unique_ptr<int> u2(u1);         // 错误:不支持拷贝
unique_ptr<int> u3;
u3 = u2;                        // 错误,不支持赋值      

注意

  • 我们不能通过拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique
  • unique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中

unique_ptr操作

u.get();
u1.swap(u2);swap(u1,u2);

unique_ptr<T> u1       //空unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T,D> u2    //u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T,D> u(d)  //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete

u = nullptr;             // 释放 u 指向的对象并将 u 置为空
auto u2 = u.release();   // u 放弃对指针的控制权,返回 u 中保存的内置指针,并将 u 置为空,注意 u2 的类型是内置指针,而不是 unique_ptr

'u.reset'
u.reset();               // 释放 u 指向的对象
u.reset(nullptr);        // 释放 u 指向的对象,并将 u 置为空,等价于 u = nullptr;
u.reset(q);              // 令 u 指向内置指针 q 指向的对象

unique_ptr<int> u2(u1.release());  // 所有权转移给 u2,u1 置为空
u3.reset(u1.release());            // 释放 u3 原来指向的内存,u3 接管 u1 指向的对象。

release使用

  • release 返回的指针通常用来初始化其他智能指针或给其他智能指针赋值。
  • release 返回的指针不能空闲,必须有其他指针接管对象。如果是一个内置指针接管了 release 返回的指针,那么程序就要负责资源的释放。
p2.release();            //错误,p2不会释放内存,而且我们失去了指针
auto  p = p2.release();  //正确,但是要必须记得delete(p)
shared_ptr p = p2.release();  //不需要delete(p)

传递unique_ptr参数和返回unique_ptr

  • 不能拷贝 unique_ptr 参数的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr。如从函数返回一个 unique_ptr,最常见的例子是从函数返回一个unique_ptr;
unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    return ret;   //返回一个局部对象的拷贝
}
return unique_ptr<int> (new int(p));
//返回一个临时对象,与局部对象是一样的

auto_ptr

  • auto_ptr 是标准库的较早版本包含的一个类,它具有 unique_ptr 的部分特性。相比于 unique_ptr,不能在容器中保存 auto_ptr,也不能从函数返回 auto_ptr。
  • 尽量使用unique_ptr

向 unique_ptr 传递删除器

  • 类似 shared_ptr,unique_ptr 默认情况下使用 delete 释放它指向的对象。可以重载 unique_ptr 中默认的删除器。

但 unique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中
因为对于 unique_ptr 来说,删除器的类型是构成 unique_ptr 类型的一部分。

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

'具体例子'
void end_connection(connection *p){disconnection(*p)};
void f(destination &d)
{
    connection c  = connect(&d);
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
    //如果我们在f退出(即使由于异常退出),connection会被正常关闭
}

错误案例

int ix = 1024, *pi = &ix, *pi2 = new int(2048);
unique_ptr<int> p0(ix);      // 错误:从 int 到 unique_ptr<int> 的无效的转换
unique_ptr<int> p1(pi);      // 运行时错误:当 p1 被销毁时会对 pi 调用 delete,这是一个对非动态分配返回的指针调用 delete 的错误。
unique_ptr<int> p2(pi2);     // 不会报错,但当 p2 被销毁后会使 pi2 成为一个悬空指针
unique_ptr<int> p3(new int(2048));   // 正确,推荐的用法

12.1.6 weak_ptr

  • weak_ptr是一种不控制所指向对象生存期的智能指针。
  • 指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数。
  • 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。

weak_ptr初始化

  • 创建 weak_ptr 时,要用 shared_ptr 来初始化它。
weak_ptr<T> w;         // 默认初始化,定义一个空 weak_ptr w,w 可以指向类型为 T 的对象
w = p;                 // p 可以是一个 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
weak_ptr<T> w(sp);     // 定义一个与 shared_ptr sp 指向相同对象的 weak_ptr。T 必须能转换成 sp 指向的类型(不必相同)

weak_ptr 操作

  • 由于 weak_ptr 的对象可能被释放的,因此不能直接访问对象,必须调用 lock()。lock()
  • 检查 weak_ptr 所指的对象是否仍存在,如果存在,返回一个指向共享对象的 shared_ptr。
w = p;            // p 可以是一个 shared_ptr 或 weak_ptr。赋值后 w 与 p 共享对象。
w.reset();        // 将 w 置为空
w.use_count();    // 返回与 w 共享对象的 shared_ptr 的数量
w.expired();      // 若 w.use_count() 为 0,返回 true,否则返回 false。expired 是 “过期的” 意思
w.lock();         // 如果 w.expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr
if(shared_ptr<int> np = wp.lock())  // 如果 np 不为空则条件成立

一个实例:StrBlobPtr类

class StrBlobPtr
{
public:
    StrBlobPtr() : curr(0) {}
    StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {} 

    string &deref() const;
    StrBlobPtr &incr();

    bool operator!=(const StrBlobPtr &rhs) const { return this->curr != rhs.curr; }

private:
    shared_ptr<vector<string>> check(std::size_t, const string &msg) const; //不能在 const 成员函数内调用本类的非 const 成员函数,调用的必须也是 const 成员函数

private:
    weak_ptr<vector<string>> wptr;
    size_t curr;
};

shared_ptr<vector<string>> StrBlobPtr::check(std::size_t sz, const string &msg) const{
    auto ret = wptr.lock();
    if (!ret) throw std::runtime_error("unbound StrBlobPtr");    //检查 wptr 是否绑定了一个 StrBlob
    if (sz >= ret->size()) throw std::out_of_range("msg");
    return ret;
}

string &StrBlobPtr::deref() const {     //const 成员函数在定义时也要加上 const
    auto p = check(curr, "dereference past end");
    return (*p)[curr];
}

StrBlobPtr &StrBlobPtr::incr(){
    check(curr, "increment past end of StrBlobPtr");
    ++curr;
    return *this;
}

解释

  1. StrBlobPtr 类起到一个充当 StrBlob 迭代器的作用,指向 StrBlob 管理的容器中的某个元素。
  2. StrBlobPtr 构造函数接受的是 StrBlob 的非常量引用,因此无法使用 const StrBlob,如果想要使用,那需要再定一个 Const StrBlobPtr 类。
  3. check返回的是一个shared_ptr
  4. return (*p) [curr] 解引用获得vector,然后通过下标访问vector当中的元素

12.2 动态数组

c++中提供俩种一次分配一个对象数组的方法:
1. 使用如 new int[10] 来分配一个对象数组
2. 使用 allocator 类。allocator 类可以实现内存分配与对象构造的分离,更灵活地管理内存。

  • 一般不需要使用动态分配的数组,而是使用如 vector 之类的 STL 容器。使用容器的类可以使用默认版本的拷贝、赋值、析构等操作,而分配动态数组的类必须定义自己版本的函数在相关操作时管理内存。

12.2.1 new和数组

  • new一个动态数组:

    • 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
    • 返回指向第一个对象的指针
    • int *p = new int[size];
  • delete一个动态数组:

    • delete [] p;
  • unique_ptr和数组:

    • 指向数组的unique_ptr不支持成员访问运算符(点和箭头)。
  • new和delete运算符一次分配一个/释放一个对象

1661157577324.png

分配一个数组得到一个元素类型的指针

  • 虽然常把 new T[] 分配的内存叫做动态数组,但是实际上它并不是一个数组,而只是返回第一个元素的指针。

理解:数组类型是包含其维度的,而 new 分配动态数组时提供的大小不必是常量,这正是因为它并非分配了一个“数组类型”。

  • 因为动态数组不是数组类型所以不能对它调用 begin() 或 end() 函数(这两个函数根据数组维度返回指向首元素和尾后元素的指针),也不能使用范围 for 语句来处理动态数组。

初始化动态分配对象的数组

  • 可以用空括号对数组中的元素进行值初始化,但不能在括号中给出初始化器,因此也不能使用 auto 分配数组。

因为值初始化时不能提供参数,所以没有默认构造函数的类是无法动态分配数组的。
1661157610145.png

动态分配一个空数组是合法的

  • 虽然不能创建一个大小为 0 的数组对象,但当 n=0 时,调用 new int[n] 是合法的,它返回一个合法的非空指针。此指针保证与 new 返回的其他任何指针都不相同。
  • 对零长度的数组来说,此指针就像尾后指针一样,不能解引用,但是可以用在循环的终止条件判断中。

1661157768779.png

释放动态数组

  • 使用 delete [] 会将动态数组中的元素按逆序销毁并释放内存。

1661157781324.png

智能指针和动态数组-unique_ptr

  • 指向数组的 unique_ptr 不支持成员访问运算符(点和箭头),但支持通过下标访问数组中的元素。

1661157792667.png

1661157803444.png

智能指针和动态数组-shared_ptr

  • 如果不提供删除器,shared_ptr 将会使用 delete 来销毁动态数组,这种行为是未定义的。

1661157815193.png

12.2.2 allocator类

  • new 有一个局限性是它将内存分配和对象构造结合在了一起,对应的 delete 将对象析构和内存释放结合在了一起。
  • 标准库 allocator 类定义在头文件 memory 中,可以实现内存分配与对象构造的分离。
  • allocator 是一个类模板。定义时需指出这个 allocator 可以分配的对象类型,它会根据对象类型来分配恰当的内存。

1661157828778.png

allocator的定义与操作

allocator<string> alloc;            // 定义一个可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n);   // 分配 n 个未初始化的 string,返回一个 string* 指针
alloc.construct(p, args);           // p 是一个 string* 指针,指向原始内存。arg 被传递给 string 的构造函数,用来在 p 指向的内存中构造对象。
alloc.destory(p);                   // p 是一个 string* 指针,此算法对 p 指向的对象执行析构函数 
alloc.deallocate(p, n);             // 释放从 p 开始的长度为 n 的内存。p 是一个 allocate() 返回的指针,n 是 p 创建时要求的大小。
                                    // 在 deallocate 之前必须先 destory 掉这块内存中创建的每个对象。   

解释:内存分配、对象构造、对象销毁、内存释放四种操作,且这四种操作是分开的,分别对应一个函数。

allocator分配未构造的内存

  • allocator 分配的内存是未构造的,需要使用 construct 成员函数按需在内存中构造对象。
  • construct 成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素,额外参数用来初始化构造的对象。

1661157857319.png

destory 销毁对象

  • 使用完对象后,必须对每个构造的元素都调用 destory 来摧毁它们。
  • destory 接受一个指针,对指向的对象执行析构函数。注意只能对构造了的元素执行 destory 操作。
  • 元素被销毁后可以重新在这块内存构造对象也可以释放掉内存。
  • construct 和 destory 一次都只能构造或销毁一个对象,要想完成对所有元素的操作,需要通过指针来遍历对每个元素进行操作。

deallocate 释放内存

  • 传递给 deallocate 的 p 必须指向由 allocate 分配的内存,大小参数 n 必须与 allocate 分配内存时提供的大小参数一样。

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

  • 除了使用 construct 构造对象外,标准库还提供了两个伴随算法,定义在头文件 memory 中,他们在给定的位置创建元素。

1661157871236.png

  • uninitialized_copy 函数返回指向构造的最后一个元素之后位置的指针。

将一个 vector 拷贝到动态内存中,并对后一半空间用给定值填充。

目录
相关文章
|
19天前
|
存储 前端开发 Java
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
24 2
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
27天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
65 4
|
2月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
135 21
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
44 0
【C++打怪之路Lv6】-- 内存管理
|
2月前
|
存储 C语言 C++
【C/C++内存管理】——我与C++的不解之缘(六)
【C/C++内存管理】——我与C++的不解之缘(六)