本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:boolan_cpp:stl_generic_2 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:stl_generic_2 [2024/01/14 13:47] (当前版本) – ↷ 链接因页面移动而自动修正 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======STL与泛型编程 第二周====== | ||
+ | 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | |||
+ | ====面向对象 V.S 泛型编程==== | ||
+ | |||
+ | 面向对象编程与泛型编程的区别主要在于两点: | ||
+ | * **面向对象编程强调数据和方法的整合性,而泛型编程强调数据和方法的独立性**。 | ||
+ | * 面向对象编程和泛型编程实现抽象的手段不同。 | ||
+ | |||
+ | ===组合上的区别=== | ||
+ | |||
+ | 首先我们来看看面向对象编程组织数据的特点: | ||
+ | * 数据成员 + 成员方法(封装) | ||
+ | * 实例化(类型特定) | ||
+ | 上述的特点造成了一个结果:面向对象编程整合了数据和方法;也就是说,特定的方法只能对特定的成员使用。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 再来看看通过泛型编程实现的功能有什么样的特点: | ||
+ | * 数据和方法独立(// | ||
+ | * 类型泛化 | ||
+ | 很显然,泛型编程希望将数据和方法分离开。 | ||
+ | |||
+ | ===抽象上的区别=== | ||
+ | |||
+ | STL 的的整体设计思路是基于泛型 | ||
+ | \\ | ||
+ | \\ | ||
+ | 面向对象编程也是一种抽象。但不同的是,面向对象是通过**多态**来实现抽象的;我们需要使用一些共通的功能,只能通过继承这样的间接调用的手段来实现;而在继承以后,我们一定会从具体化的子类中得到一个类型,这个类型是特指的,不是泛化的。 | ||
+ | |||
+ | ===泛型编程和迭代器=== | ||
+ | |||
+ | 你一定很好奇,既然在标准库中,容器和算法都是独立的,那标准库是如何把他们联系起来的?\\ | ||
+ | \\ | ||
+ | 大致步骤是这样的: | ||
+ | - 对于标准库中所有的部分,C++ 统一了接口。 | ||
+ | - 这个接口是通过迭代器来实现的,即前闭后开的区间。 | ||
+ | - 这个区间是通过两个迭代器维护('' | ||
+ | - 既然容器的范围可以用区间表示,那么算法只需要围绕着两个迭代器来实现就可以了。 | ||
+ | |||
+ | ===面向对象和迭代器=== | ||
+ | |||
+ | 有一个和特殊的例子可以说明面向对象编程和泛型编程的区别:标准库的中的 // | ||
+ | \\ | ||
+ | 在标准库的 '' | ||
+ | <code cpp linenums: | ||
+ | RandomAccessIterator cut = __unguarded_partition | ||
+ | (first, last, T(__median(*first, | ||
+ | </ | ||
+ | 对于可以进行“跳跃” 的这些迭代器,比如可以做 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上面的例子我们可以看出,泛型算法也是有局限性的;特定的情况下我们必须使用面向对象编程来解决特定的问题。 | ||
+ | |||
+ | ===泛型算法的作用=== | ||
+ | |||
+ | 泛型算法是一个标准库独立的部分,它只需要考虑对元素本身的操作。这些操作从本质上来说就是比较元素的大小。元素大小的定义并不由算法决定,而是由容器决定;因此我们可以通过自定义大小规则实现不同功能的算法。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 比如我们即将比较两个 '' | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | inline const T& max(const T& a, const T& b) { | ||
+ | return a < b ? b : a; | ||
+ | } | ||
+ | // " | ||
+ | cout << max(string(" | ||
+ | }</ | ||
+ | 很显然这里 '' | ||
+ | <code cpp linenums: | ||
+ | temlate< | ||
+ | inline const T& | ||
+ | return comp(a, b) ? b : a; | ||
+ | } | ||
+ | </ | ||
+ | 我们在这里实际添加了一个谓词,用于接收一个自定义规则;这个规则可以以函数的方式表现出来: | ||
+ | <code cpp linenums: | ||
+ | bool strLonger(const string& s1, const string& s2) { | ||
+ | return s1.size() < s2.size(); | ||
+ | } | ||
+ | </ | ||
+ | 于是我们就可以在新的 '' | ||
+ | <code cpp linenums: | ||
+ | cout << max(string(" | ||
+ | </ | ||
+ | 因为是按 '' | ||
+ | |||
+ | ====运算符重载,模板,特化==== | ||
+ | |||
+ | 该节内容绝大部分已经于[[cs: | ||
+ | \\ | ||
+ | \\ | ||
+ | 运算符重载规则的参考地址:[[http:// | ||
+ | |||
+ | ====分配器==== | ||
+ | |||
+ | 分配器(// | ||
+ | |||
+ | ===分配器的效率=== | ||
+ | |||
+ | 分配器的不同设计,是会影响容器的效率的;而效率主要体现在速度和空间占用两个方面。我们可以先看看各大编译器的情况: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上表看出,除了 GCC 2.9 版本没有直接使用 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们来看看 '' | ||
+ | <code cpp> | ||
+ | int* p = allocator< | ||
+ | allocator< | ||
+ | </ | ||
+ | 根据先前的图,我们知道 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | GCC 2.9 中是定义了自己的 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | {{ cs: | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | 单元代表的长度和图示的编号有关,长度可以表示为// | ||
+ | |||
+ | ===Alloc在GCC 4.9中的使用=== | ||
+ | |||
+ | '' | ||
+ | <code cpp linenums: | ||
+ | vector< | ||
+ | </ | ||
+ | 需要注意的是,GCC 4.9 中 '' | ||
+ | |||
+ | ===相关的技术点=== | ||
+ | |||
+ | * '' | ||
+ | * 不推荐直接使用 '' | ||
+ | |||
+ | ====容器之间的关系==== | ||
+ | |||
+ | 容器之间的关系可以如下图表示: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | ====List详解==== | ||
+ | |||
+ | //List// 容器是通过**双向环状**链表的数据结构来实现的: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上图可以看出,需要实现一个 //List// 容器,我们需要实现以下几个部分的内容: | ||
+ | * 节点部分: | ||
+ | * 指向前一个节点的指针 '' | ||
+ | * 指向后一个节点的指针 '' | ||
+ | * 存放数据的空间 '' | ||
+ | * 迭代器部分: | ||
+ | * '' | ||
+ | * '' | ||
+ | * 结构维护部分: | ||
+ | * 空节点 | ||
+ | |||
+ | ===节点部分的实现=== | ||
+ | |||
+ | //GCC 2.9// 中通过了一个结构体模板实现了节点,名称为 '' | ||
+ | <code cpp linenums: | ||
+ | < | ||
+ | struct list_node { | ||
+ | typedef void* void pointer; | ||
+ | void_pointer prev; | ||
+ | void_pointer next; | ||
+ | T data; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===迭代器部分的实现=== | ||
+ | |||
+ | 迭代器部分需要实现的除了**运算符重载**意外,还有一大堆 '' | ||
+ | |||
+ | ==迭代器自增的实现== | ||
+ | |||
+ | '' | ||
+ | - 访问节点,查看 '' | ||
+ | - 将当前迭代器指向 '' | ||
+ | 由于 当前 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 不过这里出现了一个问题:在C++中拥有着两种自增运算的存在:一种是**前自增**(// | ||
+ | <code cpp linenums: | ||
+ | link_type node; //link_type is the type of node | ||
+ | /* ++i */ | ||
+ | self& operator++() { | ||
+ | node = (link_tpye) ((*node).next); | ||
+ | return *this; | ||
+ | } | ||
+ | /* i++ */ | ||
+ | self operator++(int) { | ||
+ | self tmp = *this; | ||
+ | ++*this; | ||
+ | return *tmp; | ||
+ | } | ||
+ | </ | ||
+ | 前自增版本通过迭代器自身的**拷贝构造函数**将迭代器指向的当前地址转换到了指向下一个节点的地址('' | ||
+ | \\ | ||
+ | \\ | ||
+ | 而**带参数的版本即为后自增版本**,该版本在调用过程中使用了重载的前自增版本: | ||
+ | - 首先建立了一个迭代器的临时变量,装载原有的值,用于稍后返回,实际上是在调用迭代器的拷贝构造函数。 | ||
+ | - 其次通过调用前自增版本的重载将现有的地址指向下一个节点。 | ||
+ | - 最后返回先前存储的临时变量,此处因为是值传递,所以依然调用了拷贝构造函数。 | ||
+ | \\ | ||
+ | 有几点需要注意的是: | ||
+ | * 相对于前自增版本,后自增版本并不改变当前迭代器的值;他会记录原来的迭代器值,然后将指向目前节点的指针往下挪一位,然后再返回原来的迭代器值。 | ||
+ | * 第一步和第二步中的 '' | ||
+ | \\ | ||
+ | 很显然,后自增的这个逻辑与 C++ 中默认的后自增运算的逻辑是相同的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 除此之外,以上两个版本还有一个很重要的区别:前自增版本返回的是**引用**,而后自增版本返回的是**值**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 这也是因为设计者按照 C++ 中默认的自增运算逻辑来设计的。试想一下对整型 '' | ||
+ | <code cpp linenums: | ||
+ | ++(++i); | ||
+ | (i++)++; // error: lvalue required as increment operand | ||
+ | </ | ||
+ | 很显然第二个表达式是错误的。C++ 标准(section 5.2.6)说明了默认的后自增版本返回一个**右值**,右值是不能作为自增的操作对象的。而前自增返回了引用,引用是**左值**,因此就可以作为第二个前自增操作的操作对象了。因此这里为了匹配默认的自增运算标准,我们在前自增版本的重载实现中,也采用了返回引用。 | ||
+ | |||
+ | ==迭代器*运算和-> | ||
+ | |||
+ | 这个已经在以前的课程中讨论过了。实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | reference operator*() const { return (*node).data; | ||
+ | pointer operator-> | ||
+ | </ | ||
+ | 其中 ''& | ||
+ | \\ | ||
+ | > | ||
+ | |||
+ | ===G4.9版本的改进=== | ||
+ | |||
+ | //GCC 4.9// 版本对 //GCC 2.9// 版本做出了一些改进: | ||
+ | * 迭代器要求的参数从 ''< | ||
+ | * 从'' | ||
+ | * 重写了 '' | ||
+ | |||
+ | ====迭代器的设计规则==== | ||
+ | |||
+ | 迭代器是算法和容器沟通的桥梁。在算法和容器沟通的过程中,算法需要知道迭代器的细节来完成沟通工作。具体的来说,C++ 规定了算法必须获取迭代器的以下五个特征: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 下面的例子就是一个典型迭代器类中声明这些需要传递的信息: | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | struct _List_iterator { | ||
+ | typedef bidirectional_iterator_tag iterator_category; | ||
+ | typedef T value_type; | ||
+ | typedef Ptr pointer; | ||
+ | typedef Ref reference; | ||
+ | typedef ptrdiff_t difference_type; | ||
+ | } | ||
+ | </ | ||
+ | \\ | ||
+ | 而算法通过以下的方法就可以访问这些信息了: | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | algorithm(I first, I last) { | ||
+ | I:: | ||
+ | .... | ||
+ | } | ||
+ | </ | ||
+ | 我们把以上这些算法需要知道的特征类型,称为 // | ||
+ | |||
+ | ===Iterator Traits=== | ||
+ | |||
+ | 上节说到 C++ 要求迭代器必须带有五个基本特征信息。但有时候我们遇到的迭代器并不是以类的形式表现的,比如指针;这个时候我们就没有办法通过指针来携带这些信息了。怎么办呢?这时候就需要使用**萃取机** (// | ||
+ | \\ | ||
+ | \\ | ||
+ | 不难判断的是, //Traits// 必须有能力分辨得到的迭代器是否是类。实际上, //Traits// 通过了**模板的偏特化** 来实现了这个分辨的能力。下面是一个 //Traits// 的实现(使用 '' | ||
+ | <code cpp linenums: | ||
+ | /* if iterator is a normal class */ | ||
+ | template <class I> | ||
+ | struct iterator_traits { | ||
+ | typedef typename I:: | ||
+ | } | ||
+ | /* if iterator is a pointer */ | ||
+ | template <class T> | ||
+ | struct iterator_traits< | ||
+ | typedef T value_type; | ||
+ | }; | ||
+ | /* if iterator is a pointer that point to a const */ | ||
+ | template <class T> | ||
+ | struct iterator_traits< | ||
+ | typedef T value_type; | ||
+ | }; | ||
+ | </ | ||
+ | 上面的代码实现了对不同类型的迭代器的分别处理。当我们需要使用迭代器的 '' | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | void algorithm(...) { | ||
+ | typename iterator_traits< | ||
+ | } | ||
+ | </ | ||
+ | 有一点需要注意的是,上面的例子中,无论是指针还是常量指针,定义的类型都是 '' | ||
+ | |||
+ | ===Traits的种类=== | ||
+ | |||
+ | Traits 大致可以分为下面几类: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | ====Vector详解==== | ||
+ | |||
+ | //Vector// 容器可以被看作是一种可以动态增长的数组。我们先来看看 //Vector// 在内存中的结构: | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 由上图所示,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 通过以上这三个指针,我们可以实现 //Vector// 中的一部分访问功能函数: | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | class vector { | ||
+ | public: | ||
+ | typedef T value_type; | ||
+ | typedef value_type* iterator; | ||
+ | typedef size_t size_type; | ||
+ | .... | ||
+ | protected: | ||
+ | iterator start; | ||
+ | iterator finish; | ||
+ | iterator end_of_storage; | ||
+ | public: | ||
+ | iterator begin() { return start; } | ||
+ | iterator end() { return finish; } | ||
+ | iterator size() const { return size_type(end() - begin()); } | ||
+ | iterator capacity() const { return end_of_storage - begin()); } | ||
+ | bool empty() const { return begin() == end(); } | ||
+ | reference operator [] (size_type n) { return *(begin() + n); } | ||
+ | .... | ||
+ | </ | ||
+ | 需要注意的是, 在这些实现中,不直接使用指针,而使用其访问函数实现,是为了更好的统一接口设计。 | ||
+ | |||
+ | ===Vector的增长=== | ||
+ | |||
+ | //Vector// 的增长发生在添加元素的时候。// | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | ==Vector 增长的实现== | ||
+ | |||
+ | 我们来看一看这种扩充是怎么实现的(以 '' | ||
+ | \\ | ||
+ | //Vector// 在做 '' | ||
+ | <code cpp linenums: | ||
+ | if(finish != end_of_storage) { | ||
+ | construct(finish, | ||
+ | ++finish; | ||
+ | } else { //no enough space | ||
+ | insert_aux(end(), | ||
+ | } | ||
+ | </ | ||
+ | 如果没有足够的空间,'' | ||
+ | \\ | ||
+ | 第一步,计算新的 //Vector// 需要的空间。这一步又分为以下部分: | ||
+ | - 存放原来 //Vector// 的大小。 | ||
+ | - 判断原来 //Vector// 大小是否为 '' | ||
+ | 实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | const size_type old_size = size(); | ||
+ | const size_type len = old_size != 0 ? 2 * old_size : 1; //allocate strategy | ||
+ | iterator new_start = data_allocator:: | ||
+ | iterator new_finish = new_start; | ||
+ | </ | ||
+ | 第二步,复制所有内容到新的 // | ||
+ | - 复制旧 //Vector// 中元素到新的 //Vector// 中。 | ||
+ | - 为新添加的元素设置初始值,添加到 //Vector// 中。 | ||
+ | 实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | new_finish = uninitialized_copy(start, | ||
+ | construct(new_finish, | ||
+ | </ | ||
+ | \\ | ||
+ | 需要额外注意的有两点: | ||
+ | * '' | ||
+ | * //Vector// 由于其内存增长策略,在数据规模较大的情况下很容易溢出。因此在此用一个异常捕获是一个不错的保护措施。 | ||
+ | |||
+ | ===Vector的迭代器=== | ||
+ | |||
+ | 因为 //Vector// 是顺序容器,因此 | ||
+ | <code cpp linenums: | ||
+ | value_type* iterator; //T* | ||
+ | </ | ||
+ | 而因为是顺序容器,迭代器的种类(category)应该被设置为随机访问的类型,即 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 在 //G 4.9// 版本中,因为具体实现的复杂化,该迭代器的实现已经从指针转变为对象了,因此会按类的情况来处理。 | ||
+ | |||
+ | ====Array ==== | ||
+ | |||
+ | C++ 11种将 //Array// 也包装成了容器,使得算法可以直接操作 // | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | struct array { | ||
+ | typedef _Tp; | ||
+ | typedef _Tp*; | ||
+ | typedef _value_type* iterator; | ||
+ | value_type _M_instance[_Nm ? _Nm : 1]; | ||
+ | .... | ||
+ | </ | ||
+ | 我们直接传一个 '' | ||
+ | \\ | ||
+ | 因为 //Array// 是连续的,因此我们也可以使用指针来作为迭代器。而有了这个 '' | ||
+ | <code cpp linenums: | ||
+ | iterator begin() { return iterator(& | ||
+ | iterator end() { return iterator(& | ||
+ | </ | ||
+ | 当然,为了匹配算法的查询,我们当然也要对 //Array// 的迭代器进行特殊处理;这个处理过程和 //Vector// 类似,也需要用到 // | ||
+ | \\ | ||
+ | //注:G 4.9 的内容因侯老师并不推荐学习,我就不针对那些复杂的内容一一分析了。// | ||
+ | |||