MyTinySTL项目学习笔记02

MyTinySTL项目学习笔记02

构造和析构机制

分配和销毁内存空间

template <class T>
    T* allocator<T>::allocate(size_type n)
    {
        if (n == 0)
            return nullptr;
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

分配内存空间,使用new操作符,分配的空间大小为n*sizeof(T),返回一个指针;

template <class T>
    void allocator<T>::deallocate(T* ptr, size_type /*size*/)
    {
        if (ptr == nullptr)
            return;
        ::operator delete(ptr);
    }

销毁对应的内存空间,调用delete函数;

placement new概念

允许在已经分配好的内存区域中构造对象,而不会再次分配内存。通常情况下,new 操作符会同时分配内存和调用构造函数,而 placement new 则是直接在已经分配好的内存上调用构造函数。

new (ptr) Type;

其中ptr是一个指针,代表已经分配好的一个内存空间的起始地址,而Type是要构造的对象类型,这样对象Type就会在ptr所指向的内存中构造出来;

使用 placement new 主要有以下几个常见场景:

  • 在需要精确地控制对象内存分配和释放的场景,比如在实现自定义的内存池时。
  • 在需要在特定位置(比如内存映射区)创建对象的场景。
  • 在某些特殊的嵌入式系统或性能敏感的场景中,可以更灵活地管理对象的内存。

对于placement new构造的对象,需要手动调用析构函数进行释放资源,而不是使用delete操作符;

构造对象

调用对应的构造函数实现,construct仅仅将对象构造在指定内存空间;

template <class Ty1, class Ty2>
    void construct(Ty1* ptr, const Ty2& value)
{
    // value会传入Ty1的构造函数,然后初始化对象的成员变量
    ::new ((void*)ptr) Ty1(value); 
    // ::代表在全局的命名空间中寻找new运算符,避免名字冲突或者二义性
}

使用placement new构造对象,将value传入构造函数,初始化对象,然后对象会在ptr指向的内存空间(堆)中构造出来;

好处:使用 placement new 可以在预先分配的内存空间上构造对象,这样可以避免频繁的内存申请和释放,提高程序性能,也便于实现内存池等高效的内存管理机制。可以实现内存池(提前建立很多内存空间,保存内存空间的指针,然后将新建对象时,将对象构造在对应指针上)

析构对象

template <class Ty>
    void destroy_one(Ty*, std::true_type) {}

template <class Ty>
    void destroy_one(Ty* pointer, std::false_type)
{
    if (pointer != nullptr)
    {
        pointer->~Ty(); // 指针,用箭头运算符,调用析构函数
    }
}

设置多态,第一种为空函数处理方式,不进行析构;第二种才会真正进行析构;根据传递给 destroy_one 函数的 std::true_typestd::false_type 来选择调用空函数版本还是实际执行析构函数版本。这种技术在某些情况下非常有用,比如优化代码、减小代码量、更好地适应不同的场景等。空函数的实现方式通常是作为一种占位符,在某些条件下可能不希望执行特定操作,但又要保持接口一致。通过设置空函数,可以保持接口的完整性,同时根据需要选择是否执行具体操作,使代码更加灵活、可维护性更高。

关于箭头运算符和点运算符
#include <iostream>

class MyClass {
public:
    void myFunction() {
        std::cout << "Hello from myFunction!" << std::endl;
    }
};

int main() {
    MyClass obj;
    MyClass* ptr = &obj;

    // 使用点运算符来调用对象的成员函数
    obj.myFunction();

    // 使用箭头运算符来调用对象指针的成员函数
    ptr->myFunction();

    return 0;
}

对象是实例化用点运算符,对象是指针用箭头运算符;

回到vector中,如果pop_back()出一个元素,会发生什么?首先判断是否为空,不为空,则对最后一个元素调用destroy(end_-1),然后该函数会对最后一个元素对象调用对应的析构函数,之后会发生--end_--end_end_--更高效)

如果push_back(i)一个元素,则需要construct(mystl::address_of(*end_), value),而construct()函数实现new (void*)ptr Ty(value);,即调用构造函数初始化对象,并将对象绑定在一个堆上,之后++end_

而分配空间是在调用构造函数时,销毁空间是在调用析构函数时;

{
	vector<int> vec(); // 调用vector的构造函数,申请空间,返回一个堆上的指针
    vec.push_back(1); // 调用int类型的构造函数,1传入构造函数初始化int对象,将该对象构造在end_指针指向的内存上
    vec.pop_back(); // 调用int类型的析构函数
} 
// 超出作用域,自动调用vector的析构函数

注意理清vector的构造和析构函数,以及int的构造和析构函数;


Set的实现

set底层是红黑树;

default和delete语法

在 C++11 中引入了一个特殊的语法 = default= delete,用来显式地定义或禁用特殊成员函数的默认生成行为。在这种语法中,default 就是一个关键字,表明使用编译器生成默认的特殊成员函数实现。

class MyClass {
public:
    MyClass() = default; // 使用编译器提供的默认构造函数实现
    MyClass(const MyClass&) = delete; // 明确删除拷贝构造函数
    ~MyClass() = default; // 使用编译器提供的默认析构函数实现
};

set() = default;

也就是说,如果我们平时自定义类时,自定义了有参构造函数而没有添加一个无参构造函数时,就会发生vector<T> vec()找不到构造函数,此时没必要辛辛苦苦补一个无参构造函数,将参数都列表初始化;只需要补上T() = default;即可;

构造函数

template <class InputIterator>
    set(InputIterator first, InputIterator last) 
    :tree_() 
    { tree_.insert_unique(first, last); } // 红黑树插入节点

这里使用了列表初始化,即set() :tree_() { };也可以将成员变量红黑树的初始化放在函数体里;红黑树的初始化实质就是调用了红黑树的构造函数;

相关操作

iterator               begin()         noexcept
{ return tree_.begin(); }
iterator               end()           noexcept
{ return tree_.end(); }
bool                   empty()    const noexcept 
{ return tree_.empty(); }
size_type              size()     const noexcept 
{ return tree_.size(); }
pair<iterator, bool> insert(const value_type& value)
{
    return tree_.insert_unique(value);
}
iterator       find(const key_type& key)              
{ return tree_.find(key); }

实质就是对红黑树的操作;

multiset实现

multiset是键值允许重复的集合;底层依旧是红黑树;

// 构造函数,和set一致,但是insert_unique变成了insert_multi,即键值不重复变成了键值可以重复
template <class InputIterator> 
    multiset(InputIterator first, InputIterator last) 
    :tree_() 
    { tree_.insert_multi(first, last); }

// 相关操作
iterator               begin()         noexcept
{ return tree_.begin(); }
iterator               end()           noexcept
{ return tree_.end(); }
bool                   empty()    const noexcept 
{ return tree_.empty(); }
size_type              size()     const noexcept 
{ return tree_.size(); }
iterator insert(const value_type& value)
{
    return tree_.insert_multi(value);
}
iterator       find(const key_type& key)              
{ return tree_.find(key); }

set实现基本一致,唯一区别就是插入时,调用的时insert_multi(),即键值对可以重复;


Stack栈的实现

stack时容器适配器而不是容器;即stack的底层可以由多种数据结构实现,可以是列表,也可以是队列,默认是双端队列deque

template <class T, class Container = mystl::deque<T>> // 底层容器,默认是双端队列
class stack {};

底层容器实例作为stack的一个私有成员变量;

private:
	container_type c_;  // 用底层容器表现 stack

构造函数

stack() = default;
explicit stack(size_type n) : c_(n) {}
stack(size_type n, const value_type& value) : c_(n, value) {}

构造函数只做了一件事:就是初始化底层容器;

相关的操作

reference top() { return c_.back(); }
bool empty() const noexcept { return c_.empty(); }
size_type size() const noexcept { return c_.size(); }
void push(const value_type& value) { c_.push_back(value); }
void pop()  { c_.pop_back(); }
void clear() 
{
    while (!empty())
        pop();
}

实质就是对底层容器的操作;取栈顶元素实质就是取容器的末尾元素,压栈就是在容器末尾加上一个元素,出栈就是在容器末尾弹出一个元素;(后进先出)

由于容器本身作为stack类的一个私有成员变量,所以只能通过stack提供的public方法在类外进行访问,所以没办法直接操作容器,只能通过有限的接口来操作容器;即实现了受限访问的容器;而栈,可以理解为一个受限访问的队列(当底层容器是队列时);


queue队列

包括queuepriority_queue

queue队列

和栈一样,是容器适配器而不是容器;

template <class T, class Container = mystl::deque<T>> // 默认底层容器也是双端队列
class queue {
public:
private:
	container_type c_;  // 用底层容器表现 queue
};

用私有成员变量存储底层容器,保证了受限访问(只能通过public中的函数接口操作底层容器,实现了队列的受限访问)

queue() = default;
explicit queue(size_type n) : c_(n) {}
queue(size_type n, const value_type& value) : c_(n, value) {}

stack一样,queue的构造函数就是初始化底层容器(可以使用列表初始化)

reference front() { return c_.front(); }
void push(const value_type& value) { c_.push_back(value); }
void push(value_type&& value) { c_.emplace_back(mystl::move(value)); }
void pop() { c_.pop_front(); }

访问元素的操作也和栈一样,就是对底层容器的操作;

priority_queue优先级队列

// 参数一代表数据类型,参数二代表容器类型,缺省使用 mystl::vector 作为底层容器
// 参数三代表比较权值的方式,缺省使用 mystl::less 作为比较方式
template <class T, class Container = mystl::vector<T>,
class Compare = mystl::less<typename Container::value_type>>
class priority_queue
{
public:
private:
    container_type c_;     // 用底层容器来表现 priority_queue
    value_compare  comp_;  // 权值比较的标准(一个函数对象,用于比较)
}

优先级队列也是容器适配器而不是容器,默认底层容器为vector<T>

// 函数对象:小于
template <class T>
struct less :public binary_function<T, T, bool>
{
    bool operator()(const T& x, const T& y) const { return x < y; }
};

这段代码是定义了一个函数对象(functor)less,它是用来比较两个元素是否小于的。函数对象是一个类或结构体对象,它实现了函数调用操作符 operator(),使得它可以像函数一样被调用。在这里,less 结构体定义了函数调用操作符 operator() ,用来比较两个参数是否满足小于关系,并返回对应的布尔值。

为什么选择使用结构体(struct)来定义函数对象?这是因为在 C++ 中,结构体和类的本质是相同的,唯一的区别在于默认的访问权限。结构体默认的访问权限是 public,而类默认的访问权限是 private。对于函数对象来说,通常我们使用结构体来定义,是因为我们希望函数调用操作符 operator() 是公共的,任何地方都可以调用,所以使用结构体更为直观。

另外,如果在类内部没有其他成员变量或方法需要管理,仅仅是作为函数对象使用的话,选择结构体会让代码更加简洁明了,省去了一些类定义的繁琐性,因此在定义函数对象时常常使用结构体来实现。

struct less :public binary_function<T, T, bool>表示:struct less 继承自 binary_function<T, T, bool>。在这里,binary_function是一个 template类,它定义了一个接受两个参数并返回一个布尔值的函数对象的抽象接口。

binary_function 是 C++98 中用于函数对象的一个较早时期的实现。在 C++11 之后,binary_function 已经被标记为废弃。在现代 C++ 中,通常直接定义函数对象而无需继承自 binary_function

C++11中,可以直接:

template <typename T>
struct Less {
    bool operator()(const T& a, const T& b) const {
        return a < b;
    }
};

无需继承public binary_function<T, T, bool>

构造函数

除了初始化底层容器,还需要初始化用于比较的函数对象;

priority_queue(const Compare& c) : c_(), comp_(c) {}
explicit priority_queue(size_type n) : c_(n)
{
	mystl::make_heap(c_.begin(), c_.end(), comp_); // 根据comp_创建堆
}
相关操作
const_reference top() const { return c_.front(); }
bool empty() const noexcept { return c_.empty(); }
size_type size() const noexcept { return c_.size(); }
template <class... Args>
void emplace(Args&& ...args)
{
    c_.emplace_back(mystl::forward<Args>(args)...);
    mystl::push_heap(c_.begin(), c_.end(), comp_);
}
void push(const value_type& value)
{
    c_.push_back(value);
    mystl::push_heap(c_.begin(), c_.end(), comp_);
}
void pop()
{
    mystl::pop_heap(c_.begin(), c_.end(), comp_);
    c_.pop_back();
}
void clear()
{
    while (!empty())
        pop();
}

优先级队列实质上就是一个大顶堆或者小顶堆(虽然底层容器是数组,但是数据结构却是堆);所以对优先级队列的操作实质就是对堆的操作;

在构造函数中,根据底层容器vector中的元素,以及comp_函数对应的比较规则,通过make_heap()创建了堆;然后之后的操作都是对该堆的操作;用vector c_来维护一个堆,堆顶元素在数组的第一个元素;

修改堆以及创建堆的函数传入的都是vector c_的迭代器,意味着堆的修改直接修改在了vector c_上;对于一个大顶堆或者小顶堆,我们只关心堆顶元素,所以我们只会在c_[0]读取元素,即使用vector可以实现堆;


heap堆的相关操作

在上面的优先级队列的实现过程中,使用了push_heap, pop_heap, sort_heap, make_heap四个堆的算法;

push_heap操作

template <class RandomIter>
void push_heap(RandomIter first, RandomIter last)
{ // 新元素应该已置于底部容器的最尾端
    mystl::push_heap_d(first, last, distance_type(first));
}

template <class RandomIter, class Distance>
void push_heap_d(RandomIter first, RandomIter last, Distance*)
{
    mystl::push_heap_aux(first, (last - first) - 1, static_cast<Distance>(0), *(last - 1));
}

template <class RandomIter, class Distance, class T>
void push_heap_aux(RandomIter first, Distance holeIndex, Distance topIndex, T value) // T value代表最末端元素
{
    auto parent = (holeIndex - 1) / 2;
    while (holeIndex > topIndex && *(first + parent) < value)
    {
        // 使用 operator<,所以 heap 为 max-heap
        *(first + holeIndex) = *(first + parent);
        holeIndex = parent;
        parent = (holeIndex - 1) / 2;
    }
    *(first + holeIndex) = value;
}
  1. 计算新插入元素的父节点位置 parent,公式为 (holeIndex - 1) / 2
  2. 若当前位置不是堆顶位置且父节点的值小于新插入元素的值 value,进入循环。
  3. 将父节点的值向下移动到当前节点,并更新当前节点的位置为父节点的位置。
  4. 重复步骤 2 直到当前位置到达堆顶或者父节点的值大于等于新插入元素的值。
  5. 将新插入元素放置到最终确定的位置。

堆实际上是一种特殊的完全二叉树。在堆的实现中,通常使用数组来表示二叉树的结构,而不是显式地使用指针连接节点。这种数组表示法使得堆数据结构在存储和操作上更加高效。

堆并不像二叉搜索树一般需要右子树所有节点不小于根节点,根节点不小于左子树所有节点,只需要保证根节点不小于左子树和右子树的节点(大顶堆),至于左子树和右子树之间没有严格的大小关系;

在堆中插入节点,就是在数组末尾插入元素,相当于在二叉树插入一个叶子节点;插入之后需要上溯(即和父节点比较,直到到达根节点或者停止上溯为止),由于用数组存储的完全二叉树,所以父节点位置就是(i - 1) / 2i为当前节点索引,索引从0开始)

pop_head操作

template <class RandomIter>
void pop_heap(RandomIter first, RandomIter last)
{
    mystl::pop_heap_aux(first, last - 1, last - 1, *(last - 1), distance_type(first));
}

template <class RandomIter, class T, class Distance>
void pop_heap_aux(RandomIter first, RandomIter last, RandomIter result, T value, Distance*)
{
    // 先将首值调至尾节点,然后调整[first, last - 1)使之重新成为一个 max-heap
    *result = *first;
    mystl::adjust_heap(first, static_cast<Distance>(0), last - first, value);
}

template <class RandomIter, class T, class Distance>
void adjust_heap(RandomIter first, Distance holeIndex, Distance len, T value)
{
    // 先进行下溯(percolate down)过程
    auto topIndex = holeIndex;
    auto rchild = 2 * holeIndex + 2;
    while (rchild < len)
    {
        if (*(first + rchild) < *(first + rchild - 1))
            --rchild;
        *(first + holeIndex) = *(first + rchild);
        holeIndex = rchild;
        rchild = 2 * (rchild + 1);
    }
    if (rchild == len)
    {  // 如果没有右子节点
        *(first + holeIndex) = *(first + (rchild - 1));
        holeIndex = rchild - 1;
    }
    // 再执行一次上溯(percolate up)过程
    mystl::push_heap_aux(first, holeIndex, topIndex, value);
}

在堆数据结构中,下溯和上溯是用于调整堆结构的两种重要操作。

当执行弹出堆顶元素(pop_heap)操作时,会将堆顶元素移动到最末端(即将数组开头元素和末尾元素交换),在此时,堆顶元素处于不满足堆性质的状态。下溯的过程则是为了找到合适的位置将最末端元素上浮,以恢复堆的性质。

下溯过程找到了最合适的位置将新元素放置后,虽然从该位置到叶子节点的序列满足堆的性质,但是从新插入的元素位置向根结点的路径却不一定满足堆的性质。这时就需要进行上溯来保证整个堆继续满足堆的性质。

上溯的目的是根据新元素的值和父节点的值,向上调整新元素的位置,直到找到该元素适合的位置,使整个堆重新满足堆的性质。

下溯和上溯是为了确保弹出堆顶元素后,调整剩余元素的位置,使整个堆依然保持对应的堆性质。

sort_heap操作

// 该函数接受两个迭代器,表示 heap 容器的首尾,不断执行 pop_heap 操作,直到首尾最多相差1
template <class RandomIter>
void sort_heap(RandomIter first, RandomIter last)
{
    // 每执行一次 pop_heap,最大的元素都被放到尾部,直到容器最多只有一个元素,完成排序
    while (last - first > 1)
    {
        mystl::pop_heap(first, last--); // first和last都是迭代器
    }
}

通过不断地pop_head操作将堆顶元素放在末尾,然后通过last--操作将堆的规模缩小,即堆最后一个元素剔出堆;

make_heap操作

template <class RandomIter>
void make_heap(RandomIter first, RandomIter last)
{
    mystl::make_heap_aux(first, last, distance_type(first));;
}

template <class RandomIter, class Distance>
void make_heap_aux(RandomIter first, RandomIter last, Distance*)
{
    if (last - first < 2)
        return;
    auto len = last - first;
    auto holeIndex = (len - 2) / 2;
    while (true)
    {
        // 重排以 holeIndex 为首的子树
        mystl::adjust_heap(first, holeIndex, len, *(first + holeIndex));
        if (holeIndex == 0)
            return;
        holeIndex--;
    }
}
  1. make_heap函数是对外暴露的接口函数,其中调用了make_heap_aux函数。make_heap函数负责调用 make_heap_aux 函数,并传入相应的参数。
  2. make_heap_aux 函数中,首先判断如果范围内的元素数量小于 2,直接返回,因为单个或者空元素序列已经是一个堆。
  3. 如果范围内的元素数量大于等于 2,计算堆的总长度,然后设置初始的 holeIndex 为最后一个非叶子节点,即 (len - 2) / 2
  4. 然后开始循环调用 adjust_heap 函数来对当前节点进行堆调整,使得以当前节点为根的子树重新满足堆的性质。(对于子树,也要满足堆的性质,所以循环建立堆)
  5. 循环中,每次调整完成后,将 holeIndex 往前移动一个节点,继续调整上一个节点的子树,直至最后调整到第一个节点为根的子树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OutlierLi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值