Loading

数据结构与算法 课程随记

因为有时候需要在不同设备编辑同一份文档,本地不太方便了,先在放着博客园比较省事吧。

但是博客园是不是快要四了啊,没事再整一个个人博客吧。

内容非常杂,因为不想去上课所以还是有点东西不会,就记录一下查不会东西的时候学会的东西。没什么参考价值。

Class

https://www.runoob.com/cplusplus/cpp-classes-objects.html
image

大纲

定义成员函数,(无论public/private都可以在内部/外部定义)

范围 public,private,protected

pub,pri,pro三种类的继承

构造函数,析构函数,拷贝函数

1.构造函数可以用列表方法/常规方法
常规
image
列表
image

2.拷贝函数使用时机:
1.使用一个已经创建完毕的对象来初始化一个新对象(就是直接用类赋值给另一个类);
2.值传递的方式给函数参数传值;
3.以值方式返回局部对象(比如一个函数返回的类型是这个类)。
总之就是需要用到赋值的时候,都必须有拷贝函数

函数参数为 对象的引用时,不会调用拷贝构造函数:

image
image

又悟了一些,原来是要配合析构函数才用拷贝函数。

this指针:只是更明确的说明是访问类的成员,避免重名。

深浅拷贝

默认是浅拷贝,写了拷贝函数可以深度拷贝。

image

浅拷贝是创建一系列新的对象使得值和原对象完全相同,包括指针的值,这就使得如果类内有指针成员,拷贝前后的指针虽然不同(指针的地址不同),但是指针指向的地址相同。

深拷贝对于指针类成员来说,拷贝前后的指针不同(指针的地址不同),但是指针指向的地址也不同。

其实浅拷贝是保证指针的值相同,深拷贝只保证了指针指向的数据的值相同。

引用类型变量&

https://blog.csdn.net/Jiangtagong/article/details/109227756

image
image
image
image

const 进阶

https://blog.csdn.net/xingjiarong/article/details/47282255

https://blog.csdn.net/as480133937/article/details/120804503

https://www.runoob.com/w3cnote/cpp-const-keyword.html

static 进阶

template

image

重载运算符

image
image
image

然后悟到了返回一个引用的话,就是 &operator 这类返回代表做完这个运算之后得到的是一个左值,但是如果返回的是 operator 的话就是一个值,只能作为右值。

image

比如 (++i)=10是正确的,但是(i++)=10就不行,因为++i是一个变量,i++只是一个值。

然后 operator++(int) 就是一个标识符,代表处理后自增(i++)

丢一份注释很全的代码

突然发现自己竟然连折叠代码的指令都忘了,翻了半天博客才找到

code
#ifndef __LIST_MARK__
#define __LIST_MARK__

#include <utility>
#include <initializer_list>

/**
 * @brief 课本上的 List 实现.
 *
 * @tparam Object List 中的元素类型.
 */
template <typename Object>
class List
{
private:
    /**
     * @brief 节点的定义. 因为定义的是私有类,所以不需要考虑命名冲突.
     * 外部不会访问到这个类. 因为 struct 默认是 public 的. 所以在
     * List 类内部,Node 类的成员变量和成员函数都是可以直接访问的.
     */
    struct Node
    {
        Object data; /**<! 节点内存放的数据. */
        Node *prev;  /**<! 指向前一个节点的指针. */
        Node *next;  /**<! 指向后一个节点的指针. */
        /**
         * @brief 列表型节点的构造函数. 注意这里的默认参数的使用.
         * 使我们同时拥有一个默认构造函数和一个带参数的构造函数.
         *
         * @param d 节点内存放的数据. 默认为空.
         * @param p 前一个节点的指针. 默认为空.
         * @param n 后一个节点的指针. 默认为空.
         */
        Node(const Object &d = Object{}, Node *p = nullptr,
             Node *n = nullptr)
            : data{d}, prev{p}, next{n} {}

        /**
         * @brief 移动构造函数. 唯一的区别是节点数据是一个右值引用. 因此以 std::move 的方式传递.
         *
         * @param d 节点内存放的数据. 只有这里是一个显式的右值引用,才会调用这个构造函数.
         * @param p 前一个节点的指针. 默认为空.
         * @param n 后一个节点的指针. 默认为空.
         */
        Node(Object &&d, Node *p = nullptr, Node *n = nullptr)
            : data{std::move(d)}, prev{p}, next{n} {}
    };

public:
    /**
     * @brief 一个静态的迭代器类. 用于创建一个可以随机访问 List 的迭代器.
     * 迭代器可以看作是一个被封装的指针.
     *
     */
    class const_iterator
    {
    public:
        /**
         * @brief 默认构造函数. 用于初始化迭代器.
         *
         */
        const_iterator() : current{nullptr}
        {
        }

        /**
         * @brief 返回当前节点的数据. 注意这里是只读的.
         *
         * @return const Object& 当前节点的数据.
         */
        const Object &operator*() const
        {
            return retrieve();
        }

        /**
         * @brief 迭代器的前置自增运算符. 用于将迭代器指向下一个节点.
         *
         * @return const_iterator& 下一个节点的迭代器.
         */

        const_iterator &operator--(){
            current = current->prev;
            return *this;
        }

        const_iterator operator--(int){
            const_iterator old = *this;
            --(*this);
            return old;
        }

        const_iterator &operator++()
        {
            current = current->next;
            return *this;
        }

        /**
         * @brief 迭代器的后置自增运算符. 用于将迭代器指向下一个节点.
         *
         * @return const_iterator 下一个节点的迭代器.
         */
        const_iterator operator++(int)
        {
            const_iterator old = *this;
            ++(*this);
            return old;
        }

        /// 这里我们看到前自增和后自增的区别. 就是 std::cout << *iter++ << std::endl;
        /// 和 std::cout << *++iter << std::endl; 的区别. 前者是先输出当前值,再自增;后者是先自增,再输出.
        /// 这里也可以看到,为什么尽量用前自增,因为后自增需要一个临时变量来保存当前值,效率低.

        /**
         * @brief 判断两个迭代器是否相等.
         *
         * @param rhs 右操作数.
         * @return true 相等.
         * @return false 不相等.
         */
        bool operator==(const const_iterator &rhs) const
        {
            return current == rhs.current;
        }

        /**
         * @brief 判断两个迭代器是否不相等.
         *
         * @param rhs 右操作数.
         * @return true 不相等.
         * @return false 相等.
         */
        bool operator!=(const const_iterator &rhs) const
        {
            /// 这里直接调用 == 运算,多了一次调用,但能确保一致性.
            return !(*this == rhs);
        }

    protected:
        /// 在继承中,protected 修饰的成员变量和成员函数,可以被子类访问,但不能被外部访问.
        /// 因此它实际上应该看作是内部的和私有的.

        Node *current; /**<! 当前节点的指针. */

        /**
         * @brief 返回当前节点的数据.
         *
         * @return const Object& 当前节点的数据.
         */
        Object &retrieve() const
        {
            return current->data;
        }

        /**
         * @brief 一个带参数的构造函数. 用于快速调整迭代器的位置. 不宜对外开放.
         *
         * @param p 当前节点的新位置.
         */
        const_iterator(Node *p) : current{p}
        {
        }

        friend class List<Object>; /**<! 使 List 类可以访问到迭代器的私有成员和 protected 成员. */

        /// 注意到 const_iterator 并没有提供析构函数,因为它不需要也不应该释放内存.
    };

    /**
     * @brief 动态的迭代器,可以修改节点的数据. 它继承自 const_iterator. 体现 
     * iterator IS-A const_iterator 的关系.
     * 
     */
    class iterator : public const_iterator
    {
    public:
        /**
         * @brief 默认构造函数. 用于初始化迭代器. 为何这里不需要初始化 current 呢?
         * 因为它继承自 const_iterator,而 const_iterator 已经初始化了 current 为 nullptr.
         * 子类的构造函数会调用父类的构造函数,所以这里不需要再初始化 current 了.
         *
         */
        iterator()
        {
        }

        /**
         * @brief 返回当前节点的数据. 注意这里是可读可写的. 因为它直接调用了父类的 retrieve 函数. 
         * 而父类的 retrieve 函数返回的是一个引用,并没有限制不能修改. 而父类的 retrieve 函数后缀的 const 
         * 只限制了它不能修改父类的成员变量. 因此这里所有的规则都是可以遵守的.
         *
         * @return Object& 当前节点的数据.
         */
        Object &operator*()
        {
            return const_iterator::retrieve();
        }

        /**
         * @brief 这其实是一个重载. 一般我们规定重载必须通过参数列表来区分. 
         * 但通过 return 的 const 修饰符来区分是一个例外. 这使得我们可以给同一个函数分别提供动态和静态的版本,
         * 然后根据它们的实际使用情况来选择调用哪一个. 注意这里直接调用的父类的对应静态运算. 严格说我们也可以直接调用父类的运算符, 
         * 但这样会使代码更加清晰.
         * 
         * @return const Object& 
         */
        const Object &operator*() const
        {
            return const_iterator::operator*();
        }

        /**
         * @brief 迭代器的前置自增运算符. 用于将迭代器指向下一个节点.
         *
         * @return iterator& 下一个节点的迭代器.
         */
        iterator &operator++()
        {
            this->current = this->current->next;
            return *this;
        }

        /**
         * @brief 迭代器的后置自增运算符. 用于将迭代器指向下一个节点.
         *
         * @return iterator 下一个节点的迭代器.
         */
        iterator operator++(int)
        {
            iterator old = *this;
            ++(*this);
            return old;
        }

    protected:
        /**
         * @brief 一个带参数的构造函数. 用于快速调整迭代器的位置.
         *
         * @param p 当前节点的新位置.
         */
        iterator(Node *p) : const_iterator{p}
        {
        }

        friend class List<Object>;  /**<! 同样使 List 类可以访问到迭代器的私有成员和 protected 成员. */
    };

public:
    /**
     * @brief 默认构造函数.
     * 
     */
    List() { init( ); }

    /**
     * @brief 初始化列表构造函数. 用于将一个初始化列表的数据插入到 List 中.
     * 
     * @param il 初始化列表.
     */
    List(std::initializer_list<Object> il) : List()
    {
        for (const auto & x : il)
            push_back(x);
    }

    /**
     * @brief 拷贝构造函数. 用于将一个 List 的数据拷贝到另一个 List 中.
     * 
     * @param rhs 右操作对象.
     */
    List(const List &rhs)
    {
        /// 先初始化一个空的 List.
        init( );
        /// 然后遍历右操作对象,将右操作对象的数据一个一个插入到 List 中.
        /// 这里的 auto 指针会自行推断类型. 这里的 x 是一个引用,所以不会发生拷贝.
        /// 而这里的 for 循环的方式实际上让我们可以遍历一个容器,而不需要知道容器的具体类型.
        /// 也就是说,实际上这里是通过 iterator 来遍历的. 这里的 rhs 是一个 List,所以它有 begin 和 end 函数.
        /// 这段代码的实际效果等价于
        ///     for (auto it = rhs.begin(); it != rhs.end(); ++it) {  
        ///         auto &x = *it;  
        ///         push_back(x);  
        ///     }
        for (auto & x : rhs)
            /// 调用成员函数从后方(为何)插入到 List 中.
            push_back(x);
    }

    /**
     * @brief 析构函数,用于释放 List 中的内存.
     * 实际工作是调用 clear 函数,然后释放头尾节点的内存. 别忘了这两个哨兵.
     * 
     */
    ~List()
    {
        clear( );
        delete head;
        delete tail;
    }

    /// ???
    // List &operator=(const List &rhs)
    // {
    //     List copy = rhs;
    //     std::swap( *this, copy );
    //     return *this;
    // }

    /**
     * @brief 赋值运算符. 用于将一个 List 的数据赋值给另一个 List.
     * 这里采用了 copy-and-swap 的技术. 通过传值的方式来调用拷贝构造函数,
     * 然后通过 swap 来交换数据. 而 copy 这个临时对象会在函数结束时被销毁.
     * 这个技术使得整个过程逻辑简单,并且效率更高. 但是一些实现的关键技术细节都是在 
     * C++ 14 起引入的,因此只实现了 C++ 11 的版本的课本并没有做到.
     */
    List &operator=(List copy)
    {
    //    std::swap( *this, copy );
    /// 这里直接 swap 两个对象是错误的,因为 List 并未定义 swap 操作.
    /// 当发生右值引用时, 会导致一个对象被析构,而另一个对象被赋值.
        std::swap( theSize, copy.theSize );
        std::swap( head, copy.head );
        std::swap( tail, copy.tail );
        return *this;
    }

    /**
     * @brief 移动构造函数. 用于将一个右值引用的 List 的数据移动到另一个 List 中.
     * 
     * @param rhs 必须是一个右值引用. 因此不存在右操作数不存在的情况,不需要考虑缺省.
     */
    List(List &&rhs) : theSize{ rhs.theSize }, head{ rhs.head }, tail{ rhs.tail }
    {
        /// 因为 rhs 是一个右值引用,所以我们最好直接将其数据置空. 
        /// 从而实现移动. 不然如果 rhs.head 还管理原来的数据,那么当它被析构时,
        /// 就可能释放掉已经移交给新对象的数据.
        rhs.theSize = 0;
        rhs.head = nullptr;
        rhs.tail = nullptr;
    }
    
    // // 这个功能已经被上面的实现所覆盖. 所以不再需要.
    // List &operator=(List &&rhs)
    // {
    //     std::swap( theSize, rhs.theSize );
    //     std::swap( head, rhs.head );
    //     std::swap( tail, rhs.tail );
    //     return *this;
    // }

    /**
     * @brief 返回一个迭代器,指向 List 的第一个元素.
     * 
     * @return iterator 
     */
    iterator begin()
    {
        /// head 指向的是一个哨兵节点,它的 next 指向的是第一个数据节点.
        return { head->next };
    }

    /**
     * @brief 返回一个只读的迭代器,指向 List 的第一个元素.
     * 
     * @return const_iterator 
     */
    const_iterator begin() const
    {
        return { head->next };
    }

    /**
     * @brief 返回一个迭代器,指向 List 的最后一个元素之后(哨兵元素).
     * 
     * @return iterator 动态迭代器,可修改元素.
     */
    iterator end()
    {
        return { tail };
    }

    /**
     * @brief 返回一个迭代器,指向 List 的最后一个元素之后(哨兵元素).
     * 
     * @return const_iterator 静态迭代器,只读.
     */
    const_iterator end() const
    {
        return { tail };
    }

    /// 这里调用静态还是动态,实际上取决于调用的环境. 如果调用的是 const 对象,那么就会调用 const 的版本. 

    /**
     * @brief 返回 List 中数据节点的总数.
     * 
     * @return int 
     */
    int size() const
    {
        return theSize;
    }

    /**
     * @brief 判断 List 是否为空.
     * 
     * @return true 空.
     * @return false 非空.
     */
    bool empty() const
    {
        return size() == 0;
    }

    /**
     * @brief 清空 List 中的数据.
     * 
     */
    void clear()
    {
        /// 这里其实用计数器来判断 List 是否为空.
        while (!empty())
            pop_front();
    }

    /**
     * @brief 返回 List 中第一个数据节点的数据.
     * 
     * @return Object& 可以修改的第一个数据节点的数据.
     */
    Object &front()
    {
        return *begin();
    }

    /**
     * @brief 返回 List 中第一个数据节点的数据.
     * 
     * @return const Object& 只读的第一个数据节点的数据.
     */
    const Object &front() const
    {
        return *begin();
    }

    /**
     * @brief 返回 List 中最后一个数据节点的数据.
     * 
     * @return Object& 可以修改.
     */
    Object &back()
    {
        return *--end();
    }

    /**
     * @brief 返回 List 中最后一个数据节点的数据.
     * 
     * @return const Object& 只读.
     */
    const Object &back() const
    {
        return *--end();
    }

    /**
     * @brief 将一个左值数据节点插入到 List 的头部.
     * 
     * @param x 数据节点.
     */
    void push_front(const Object &x)
    {
        insert(begin(), x);
    }

    /**
     * @brief 将一个右值数据节点插入到 List 的头部.
     * 
     * @param x 数据节点.
     */
    void push_front(Object &&x)
    {
        insert(begin(), std::move(x));
    }

    /**
     * @brief 将一个左值数据节点插入到 List 的尾部.
     * 
     * @param x 数据节点.
     */
    void push_back(const Object &x)
    {
        insert(end(), x);
    }

    /**
     * @brief 将一个右值数据节点插入到 List 的尾部.
     * 
     * @param x 数据节点.
     */
    void push_back(Object &&x)
    {
        insert(end(), std::move(x));
    }

    /**
     * @brief 删除 List 的第一个数据节点.
     * 
     */
    void pop_front()
    {
        erase(begin());
    }

    /** 
     * @brief 删除 List 的最后一个数据节点.
     * 
     */
    void pop_back()
    {
        erase(--end());
    }

    /**
     * @brief 插入一个左值数据节点到指定位置.
     * 
     * @param itr 插入位置的迭代器.
     * @param x 数据节点.
     * @return iterator 返回插入位置的迭代器.
     */
    iterator insert(iterator itr, const Object &x)
    {
        Node *p = itr.current;
        theSize++;
        /// 仔细想一下这个过程.
        return { p->prev = p->prev->next = new Node{ x, p->prev, p } };
    }

    /**
     * @brief 插入一个右值数据节点到指定位置.
     * 
     * @param itr 插入位置的迭代器.
     * @param x 数据节点.
     * @return iterator 返回插入位置的迭代器.
     */
    iterator insert(iterator itr, Object &&x)
    {
        Node *p = itr.current;
        theSize++;
        return { p->prev = p->prev->next = new Node{ std::move( x ), p->prev, p } };
    }

    /**
     * @brief 删除指定位置的数据节点. 由于哨兵节点的存在,不用担心删完了以后最后变成一个 nullptr.
     * 
     * @param itr 指定位置的迭代器.
     * @return iterator 返回删除位置的下一个迭代器.
     */
    iterator erase(iterator itr)
    {
        Node *p = itr.current;
        iterator retVal{ p->next };
        p->prev->next = p->next;
        p->next->prev = p->prev;
        delete p;
        theSize--;
        return retVal;
    }

    /**
     * @brief 删除指定范围的数据节点. 比一个个删除效率高么?
     * 
     * @param from 起始位置的迭代器.
     * @param to 结束位置的迭代器.
     * @return iterator 返回删除位置的下一个迭代器.
     */
    iterator erase(iterator from, iterator to)
    {
        /// 这里其实就是重复调用了单个节点删除的 erase 函数.
        /// 因此这个内循环和外部循环完全一致. 
        /// 这里可以做适当的优化么?
        for( iterator itr = from; itr != to; )
            itr = erase( itr );
        return to;
    }

private:
    int theSize;    /**<! 数据节点总数. */
    Node *head;     /**<! 头指针. */
    Node *tail;     /**<! 尾指针. */
    
    /**
     * @brief 初始化 List. 用于构造函数中初始化 List. 构建一张空表.
     * 
     */
    void init()
    {
        theSize = 0;
        head = new Node;
        tail = new Node;
        head->next = tail;
        tail->prev = head;
    }
};

#else
// DO NOTHING.
#endif

异常机制

直接看这个博客,附记一些需要注意的:https://blog.csdn.net/Dawn_Lillian/article/details/138450231

image

image

image

image

也就是现在的C++一般都用noexpect,

image

image

image

noexpect限定的函数如果抛异常,不会等到你接,就会直接终止程序。

image

image

image

标准exception,后面可以自己加一个字符串,他就是这个exception的.what()

BST

感觉换到指针版有好多需要注意的啊。

image

image

posted @ 2024-10-18 19:36  Soresen  阅读(70)  评论(10编辑  收藏  举报