======STL与泛型编程 第二周====== 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ 因个人水平有限,我撰写的笔记不免出现纰漏。如果您发现错误,请留言提出,谢谢! ---- ====面向对象 V.S 泛型编程==== 面向对象编程与泛型编程的区别主要在于两点: * **面向对象编程强调数据和方法的整合性,而泛型编程强调数据和方法的独立性**。 * 面向对象编程和泛型编程实现抽象的手段不同。 ===组合上的区别=== 首先我们来看看面向对象编程组织数据的特点: * 数据成员 + 成员方法(封装) * 实例化(类型特定) 上述的特点造成了一个结果:面向对象编程整合了数据和方法;也就是说,特定的方法只能对特定的成员使用。 \\ \\ 再来看看通过泛型编程实现的功能有什么样的特点: * 数据和方法独立(//Container// 和 //Algorithm//) * 类型泛化 很显然,泛型编程希望将数据和方法分离开。 ===抽象上的区别=== STL 的的整体设计思路是基于泛型 * 普通列表项目编程的思想。泛型编程,所谓的泛,指的是类型的泛化,这一点可以通过其语言实现机制:模板很好的体现出来。很容易看出来的是,泛型编程的核心思想是**抽象**,即将特定于某些类型的算法中那些与类型无关的共性抽象出来。比如,//Array// 和 //Vector// 都可以抽象为容器;函数和自定义类都可以抽象为可调用的对象(仿函数)。通过这样一层一层的抽象,最终抽象出来的一个最大公约数,就是我们现在看到的标准库。 \\ \\ 面向对象编程也是一种抽象。但不同的是,面向对象是通过**多态**来实现抽象的;我们需要使用一些共通的功能,只能通过继承这样的间接调用的手段来实现;而在继承以后,我们一定会从具体化的子类中得到一个类型,这个类型是特指的,不是泛化的。 ===泛型编程和迭代器=== 你一定很好奇,既然在标准库中,容器和算法都是独立的,那标准库是如何把他们联系起来的?\\ \\ 大致步骤是这样的: - 对于标准库中所有的部分,C++ 统一了接口。 - 这个接口是通过迭代器来实现的,即前闭后开的区间。 - 这个区间是通过两个迭代器维护(''begin()'', ''end()'')。 - 既然容器的范围可以用区间表示,那么算法只需要围绕着两个迭代器来实现就可以了。 ===面向对象和迭代器=== 有一个和特殊的例子可以说明面向对象编程和泛型编程的区别:标准库的中的 //List//,不能使用泛型算法的 ''sort()'' 排序。\\ \\ 在标准库的 ''sort()'' 中有这么一段代码: RandomAccessIterator cut = __unguarded_partition (first, last, T(__median(*first, *(first + (last - first) / 2), *(last - 1)))); 对于可以进行“跳跃” 的这些迭代器,比如可以做 ''last - first'' 这样操作的迭代器,我们一般称之为 //Random Access Iterator//。这样的迭代器要求元素在内存里是连续的。反观 //List// 中的迭代器,因为 //List// 是通过链表实现的,这就注定了在 //List// 中,迭代器是不能跳转的。因此 //List// 容器不得不使用自己的成员函数 ''sort()''。 \\ \\ 从上面的例子我们可以看出,泛型算法也是有局限性的;特定的情况下我们必须使用面向对象编程来解决特定的问题。 ===泛型算法的作用=== 泛型算法是一个标准库独立的部分,它只需要考虑对元素本身的操作。这些操作从本质上来说就是比较元素的大小。元素大小的定义并不由算法决定,而是由容器决定;因此我们可以通过自定义大小规则实现不同功能的算法。 \\ \\ 比如我们即将比较两个 ''string'' 的大小:默认的比较方法是按位比较,即首字母大的 ''string'' 比较大。我们通过算法按默认的规则比较 ''zoo'' 和 ''hello'': template inline const T& max(const T& a, const T& b) { return a < b ? b : a; } // "zoo" is greater than "hello" cout << max(string("zoo"), string("hello") << endl; } 很显然这里 ''zoo'' 是比 ''hello'' 大的。 如果希望按照 ''string'' 的大小来比较的话,我们可以把 ''max()'' 的定义改写一下: temlate inline const T&max(const T&a, const T&b, Compare comp) { return comp(a, b) ? b : a; } 我们在这里实际添加了一个谓词,用于接收一个自定义规则;这个规则可以以函数的方式表现出来: bool strLonger(const string& s1, const string& s2) { return s1.size() < s2.size(); } 于是我们就可以在新的 ''max()'' 函数调用的时候使用这个规则了: cout << max(string("zoo"), string("hello"), strLonger) << endl; 因为是按 ''string'' 的长度比大小,这里的 ''zoo'' 是小于 ''hello'' 的。 ====运算符重载,模板,特化==== 该节内容绝大部分已经于[[cs:programming:cpp:boolan_cpp:oop_b_week1|C++面向对象高级编程(下)第一周]]中讨论过,就不再赘述。 \\ \\ 运算符重载规则的参考地址:[[http://en.cppreference.com/w/cpp/language/operators|Operator overloading]] ====分配器==== 分配器(//Allocator//)主要与容器配合使用,在容器申请内存空间上起作用。分配器在底层实现上主要通过调用 ''operator new()'' 和 ''operator delete()'' 来完成内存分配和释放的任务;而从前几周的课程里我们也知道,这两个函数实际上是通过 ''malloc()'' 和 ''free()'' 在操作。 ===分配器的效率=== 分配器的不同设计,是会影响容器的效率的;而效率主要体现在速度和空间占用两个方面。我们可以先看看各大编译器的情况: \\ \\
\\ \\ 从上表看出,除了 GCC 2.9 版本没有直接使用 ''malloc()'' 和 ''free()'',其他编译器都是直接调用上述两个C语言函数实现的内存分配。那为什么有 GCC 2.9 这个例外? \\ \\ 我们来看看 ''allocator'' 的调用是怎么样的: int* p = allocator().allocate(512, (int*)0); allocator().deallocate(p, 512); 根据先前的图,我们知道 ''allocate'' 是以元素为单位操作的。以 ''malloc()'' 为例子,这个函数会对每一个元素都进行空间申请;但申请的空间并不止元素所占用的空间,而会加上一系列的其他信息(比如 //Cookies//)。那么当元素的大小和这些额外开销(overhead)的大小相近,而元素又特别多的时候,这时的空间浪费是非常恐怖的:每一个元素都带来了额外的开销。对于普通的内存申请来说,这是必须的,因为元素的大小可能不一样;但对于容器来说,因为容器元素的大小是相等的,因此 //Cookies// 就没有必要添加到每个元素中了。 \\ \\ GCC 2.9 中是定义了自己的 ''allocator'' 的,但是没有使用。取而代之的是使用了 //SGL STL// 中的 ''alloc''。相比之下,该函数更有效的利用了空间:该函数通过一个有16个单元的链表来代替 //Cookies// 来维护元素的大小,每一个链表的单元代表了一个长度: \\ \\ \\ {{ cs:programming:cpp:boolan_cpp:stlalloc.jpg?600 |}} \\ \\ \\ 单元代表的长度和图示的编号有关,长度可以表示为//(编号 + 1)* 字节//。当使用 ''alloc'' 申请内存的时候,元素的大小不再由 //Cookies// 维护了,而是由这些链表单元所维护。编译器通过链表单元来查找指定大小的容器。而申请的时候,''malloc'' 只需要负责管理区块总的大小就可以了。当然,为了配合这样的链表,我们需要把容器的大小调整为字节的倍数。 ===Alloc在GCC 4.9中的使用=== ''alloc'' 在设计时能节省不少的空间;但很遗憾是在 GCC 4.9 中,默认的 ''allocator'' 又开始直接调用 ''malloc'' 和 ''free'' 了。不过 ''alloc'' 并没有被直接删除;我们可以通过如下的方法类接着使用 ''alloc'': vector> vec; 需要注意的是,GCC 4.9 中 ''alloc'' 已经改名为 ''__pool_alloc'',而该头文件存放于 ''gnu_cxx'' 中,而不在 ''std'' 里。 ===相关的技术点=== * ''allocator()'' 尾部的空括号实际上是建立了一个临时对象 * 不推荐直接使用 ''allocator'', 因为在 ''allocator'' 释放空间的时候,不但要指定入口指针,还必须指定当时申请的空间大小作为参数才行做释放操作。 ====容器之间的关系==== 容器之间的关系可以如下图表示: \\ \\
====List详解==== //List// 容器是通过**双向环状**链表的数据结构来实现的: \\ \\
\\ \\ 从上图可以看出,需要实现一个 //List// 容器,我们需要实现以下几个部分的内容: * 节点部分: * 指向前一个节点的指针 ''prev''。 * 指向后一个节点的指针 ''next''。 * 存放数据的空间 ''data''。 * 迭代器部分: * ''->'' / ''*'' 操作符重载,实现传统指针的功能。 * ''++'' 迭代器自增操作符重载,实现迭代器移位功能。 * 结构维护部分: * 空节点 ===节点部分的实现=== //GCC 2.9// 中通过了一个结构体模板实现了节点,名称为 ''list_node'',具体代码如下: