C++拷贝控制:动态内存管理类(简化的vector<string>实现)

动态内存管理类

​ 某些类需要在运行时动态分配可变大小的内存。这种类通常可以使用标准库容器来保存它们的数据。但是这一策略并不是对每个类都适用。某些类需要自己进行内存分配。这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。

​ 这里,我们将实现标准库 vector 的一个简化版本。我们所做的简化是不使用模版,我们的类只用于 string,所以它被命名为 StrVec。

StrVec 类的设计

​ 我们知道 vector 的工作方式 (这里不简述了)。我们的 StrVec 类采用类似的策略。我们将使用 allocator 来进行动态的分配内存。

​ 每个 StrVec 有三个指针成员指向其元素所使用的内存:

  • elements,指向分配的内存中的首元素
  • first_free,指向最后一个实际元素之后的位置
  • cap,指向分配的内存末尾之后的位置

除了这些指针外,StrVec 还有一个名为 alloc 的静态成员,其类型是 allocator<string>。alloc 成员会分配 StrVec 使用的内存。我们的类还有四个工具函数:

  • alloc_n_copy 会分配内存,并拷贝一个给定范围中的元素
  • free 会销毁构造的元素并释放内存
  • chk_n_alloc 保证 StrVec 至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc 会调用 reallocate 来分配更多内存。
  • reallocate 在内存用完时为 StrVec 分配新内存
StrVec 类定义
class StrVec {
public:
    StrVec(): elements(nullptr), first_free(nullptr), cap(nullptr) { }
    StrVec(const StrVec&);
    StrVec&operator= (const StrVec&);
    ~StrVec();
    void push_back(const std::string&);
    size_t size() const { return first_free - elements; }
    size_t capacity() const { return cap - elements; }
    std::string *begin() const { return elements; }
    std::string *end() const { return first_free; }

private:
    static std::allocator<std::string> alloc;
    void chk_n_alloc() {
        if(size() == capacity()) reallocate();
    }
    std::pair<std::string*, std::string*> alloc_n_copy
            (const std::string*, const std::string*);
    void free();                // 销毁元素并释放内存
    void reallocate();          // 获取更多内存,并拷贝已有元素
    std::string *elements;      // 指向分配的内存中的首元素
    std::string *first_free;    // 指向最后一个实际元素之后的位置
    std::string *cap;           // 指向分配的内存末尾之后的位置
};

类体定义了多个成员:

  • 默认构造函数隐式的初始化 alloc,并显式的初始化所有指针为 nullptr
  • size 返回实际元素数目,显然是 first_free - elements
  • capacity 返回当前可保存的元素数目,等于 cap - elements
  • 当前没有空间容纳新元素时,即 cap == first_free,chk_n_alloc 会为 StrVec 重新分配内存
  • begin 和 end 返回首元素和尾后位置的指针

有一点我们需要注意:alloc 成员是类静态成员。这里回顾一下,静态成员变量不属于任何对象,不能在类的构造函数中被构造和初始化,必须在类的外部定义和初始化,而且定义时不能重复 static 关键字。所以对于此代码,我们需要在所有函数及类之外定义 alloc,即我们必须添加此语句:std::allocator<std::string> StrVec::alloc; 才能正确编译代码。

使用 construct

​ 函数 push_back 调用 chk_n_alloc 确保有空间容纳新元素。如果需要,chk_n_alloc 会调用 reallocate。当 chk_n_alloc 返回时,push_back 知道必有空间容纳新元素。它要求其 allocator 成员来 construct 新的尾元素:

void StrVec::push_back(const std::string &s) {
    chk_n_alloc();  // 确保有空间容纳新元素
    // 添加进去,并更新尾后指针。在 first_free 指向的元素中构造 s 的副本
    alloc.construct(first_free++ ,s);
}

注意使用 construct,因为 alloc 分配的是原始未构造的内存。

alloc_n_copy 成员

​ 我们在拷贝或赋值 StrVec 时,可能会调用 alloc_n_copy 成员。类似 vector,我们的 StrVec 类有类值的行为。当我们拷贝或赋值 StrVec 时,必须分配独立的内存,并从原 StrVec 对象拷贝元素至新对象。

​ alloc_n_copy 成员会分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中。此函数返回一个指针 pair,两个指针分别指向新空间的开始位置和尾后的位置:

std::pair<std::string*, std::string*>
StrVec::alloc_n_copy(const std::string *beg, const std::string *end) {
    // 分配空间保存给定范围中的元素
    auto data = alloc.allocate(end - beg);
    return {data, uninitialized_copy(beg, end, data)};
}

uninitialized_copy 返回拷贝完成后的尾后指针。

free 成员

​ free 成员有两个责任:首先 destroy 元素,然后释放 StrVec 自己分配的内存空间。for 循环调用 allocator 的 destroy 成员,从构造的尾元素开始,到首元素位置,逆序销毁所有元素:

void StrVec::free() {
    if(elements) {      // 不能传递给 deallocate 一个空指针,如果 elements 为 0,函数什么也不用做
        while (first_free != elements)
            alloc.destroy(--first_free);
        alloc.deallocate(elements, cap - elements);
    }
}

注意:我们传递给 deallocate 的指针不能是空指针必须是之前某次 allocate 调用返回的指针。因此,我们需要在调用 deallocate 之前检查指针是否为空。

拷贝控制成员

​ 实现了 alloc_n_copy 和 free 成员后,我们为类实现拷贝控制成员就很简单了。拷贝构造函数调用 alloc_n_copy:

StrVec::StrVec(const StrVec &s) {
    auto newdata = alloc_n_copy(s.begin(),s.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}

​ 析构函数调用 free:

StrVec::~StrVec() {
    free();
}

​ 拷贝赋值运算符:

StrVec& StrVec::operator=(const StrVec &s) {
    // 考虑自赋值的情况,先拷贝后 free
    auto newdata = alloc_n_copy(s.begin(),s.end());
    free();
    elements = newdata.first;
    first_free = newdata.second;
    return *this;
}
在重新分配内存的过程中移动而不是拷贝元素

​ 在编写 reallocate 成员函数之前,我们考虑此函数可以做什么。它应该:

  • 为一个新的、更大的 string 数组分配内存
  • 在内存空间的前一部分构造对象,保存现有元素
  • 销毁原内存中的元素,并释放这块内存

我们知道 string 具有类值行为。当拷贝一个 string 时,副本和原对象相互独立。所以我们拷贝 string 会经历分配内存,然后拷贝;销毁时释放内存。

​ 拷贝一个 string 就必须真的拷贝数据。而我们希望的 reallocate 要求拷贝 StrVec 中的 string 避免分配和释放 string 的额外开销,这样 StrVec 的性能会好很多。

移动构造函数和 std::move

​ 通过使用新标准库引入的两种机制,我们就可以避免 string 的拷贝。有一些标准库类,包括 string,都定义了“移动构造函数”。

移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。 而且标准库保证“移后源” string 仍然保持一个有效的、可析构的状态。对 string,我们可以这样理解:想象每个 string 都有一个指向 char 数组的指针,而 string 的移动构造函数进行了指针的拷贝,而不是字符分配内存空间然后拷贝字符。

​ 我们使用的第二个机制使 move 标准库函数,它定义在 utility 头文件。对于move,我们目前需要了解两个关键点:当 reallocate 在新内存中构造 string 时,它必须调用 move 来表示希望使用 string 的移动构造函数。如果它漏掉了 move 的声明,将会使用 string 的拷贝构造函数。其次,我们通常不为 move 提供一个 using 声明。当我们使用 move 时,直接调用 std::move 而不是 move。

reallocate 成员

​ reallocate 函数分配新内存空间,每次重新分配的内存时都会将 StrVec 的容量加倍。如果 StrVec 为空,我们将分配一个容纳新元素的空间:

void StrVec::reallocate() {
    auto newcapacity = size() ? 2 * size() : 1;
    auto newdata = alloc.allocate(newcapacity);     // 分配新内存
    // 移动数据
    auto dest = newdata;
    auto elem = elements;   // 注意:这里需要拷贝,因为 free 中会使用到成员 elements
    for(size_t i = 0;i < size();++ i) {
        alloc.construct(dest ++,std::move(*elem ++));
    }
    free();     // 移动完成边释放旧内存
    // 更新我们的数据
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

我们 construct 中的第二个参数是 move 的返回值。调用 move 返回的结果会令 construct 使用 string 的移动构造函数,所以这些 string 管理的内存不会被拷贝。相反,我们构造的每个 string 都会从 elem 指向的 string 那里接管内存的所有权。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值