C++学习笔记(五)(STL简单介绍)
答:这里的置位指的并不是failbit值变化,而是指让程序知道了发生什么错误(更像是一种开关),也可以认为另有一个strm::iostate变量,它是用来保存流的状态的,rdstate()函数就是用来返回流的状态的也就是返回这个变量,比如如果io流对象没问题,那么它就是0(0000);首先我们要明确:IO库定义了4个strm::iostate类型的constexpr值(strm是一种IO类型),既
一、IO类
1.IO对象条件状态的访问和操作
IO类锁定一的一些函数和标志可以帮助我们访问和操纵IO流对象的条件状态。
具体提供了哪些可以看primer P279.
这里主要讲一下这些函数和标志在程序中到底有什么用?怎么用?
首先我们要明确:IO库定义了4个strm::iostate类型的constexpr值(strm是一种IO类型),既然是constexpr值,那么就表明这四个值是不会改变的,这4个值是一种标志,分别是
strm::goodbit (0) (0000)b
strm::badbit (1) (0001)b
strm::eofbit (2) (0010)b
strm::failbit (4) (0100)b
以failbit为例,它表示可恢复/可修复错误,badbit被置位,流还可以继续使用。
此时我有一个问题:既然strm::failbit都是constexpr值了怎么置位?不是值不能变吗?
答:这里的置位指的并不是failbit值变化,而是指让程序知道了发生什么错误(更像是一种开关),也可以认为另有一个strm::iostate变量,它是用来保存流的状态的,rdstate()函数就是用来返回流的状态的也就是返回这个变量,比如如果io流对象没问题,那么它就是0(0000);如果io流出现了可以修复的错误,那么s.rdstate()返回的就是4(0100);如果io流出现了badbit和failbit错误(置位),那么就会返回5(0101) 。
int main()
{
int i;
cin >> i; // 如果输入字符,则会使failbit置位,发生可修复的错误。
cin.setstate(iostream::badbit); // 将badbit置位
cout << iostream::goodbit << " " // 0
<<iostream::badbit << " " // 1
<< iostream::eofbit << " " // 2
<< iostream::failbit << " " // 4
<< cin.fail() << " " // 1
<< cin.rdstate(); // 5
return 1;
}
注:以上只讲了一小部分知识,其余的部分还需要看书,在此不赘述。
2.输出缓冲区
每个输出流都管理一个输出缓冲区,用来保存程序读写的数据,想要数据真正打印出来或写到输出设备/文件,必须要刷新缓冲区。
缓冲区刷新是保证目前为止程序所产生的输出都真正写入输出流中(真正打印出来),而不是仅停留在内存(缓冲区)中等待写入流(刷新)。
比如:
int main()
{
int i ;
cout << "test" << "123";
while(1);
return 1;
}
//把"test"和"123"放到缓冲区中,但是并没有刷新缓冲区,所以不会打印出来。
int main()
{
int i ;
cout << unitbuf; //所有输出操作后都会立即刷新缓冲区
cout << "test" << "123";
while(1);
return 1;
}
// 输出 test123
二、STL的基本知识
1.标准库与标准模板库

标准库大于STL,STL是六大部件,STL+其他的零部件构成了C++标准库。
C语言中的头文件可以被include,有两种方式,<cstdio>不带.h的情况必须前面加上c以表示这是C语言中的头文件;<stdio.h>带.h的情况。
2.STL六大部件及它们的关系

注1:分配器用来支持容器,因为把数据放进容器里无需知道内存是如何存放分配的。而把数据放到容器里要对这些数据进行操作,虽然容器类中也会有处理数据的操作,但是其他一些操作被独立出来生成模板函数即算法。OOP一般遵循数据和操作数据的函数都定义在类中并且有继承的关系,而此处遵循的是数据在类内,操作数据的函数定义在类外,这种方式叫做泛型编程。那么如何通过类外的函数具体来操作数据呢,这就需要一个桥梁,而迭代器正是这个桥梁。
allocator:分配器可以不写,但是会使用各个容器源码中默认的那个分配器,一般都会使用默认的。分配器也是一个模板要告诉它每次分配的内存是什么类型的数据,当然分配器的数据类型与容器的数据类型必须是匹配的。
algorithm:count-if表示满足某条件的元素的计数,在这里条件是大于等于40的数。
仿函数:仿函数中的less是将两个数比大小,但是这里是将一个数与40比大小,所以原本仿函数的接口可能就不适用了,那么就要使用适配器bind2nd(绑定第二参数),之前是a和b比较,现在将b绑定为40,not1这个适配器是取反的意思,less代表小于40的数,经过取反之后就是大于等于40的数。
3.时间复杂度

容器是放元素的地方,那么选哪一个容器的效率比较好比较高这个问题是没有答案的选择那个容器取决于你的数据的分布和数据的特性,比如说有一堆数据,你会经常在中间插入数据还是会在头部或者尾部插入数据,你会经常查找一个数据并将它杀掉吗,根据这些状况来选择最适合你的容器。这就是为什么提供了这么多的容器来供你使用。比如可以查到某个容器在中间插入一个数据的复杂度。需要注意的是必须当n很大的时候复杂度才有意义。
4.前闭后开区间

注1:有7个元素,灰色的表示看不见这个元素。所谓的前闭后开指的是迭代器的end表示的是最后一个元素的下一个位置的地址。所以不能对容器的end()解引用,会得到一个未定以的值。
注2:利用迭代器遍历容器; 利用范围for循环遍历容器;
利用size()和for还有[]来遍历容器并对其元素进行操作。
以上3种为常用的遍历容器元素的方法。
三、容器的基本知识
1.begin和end成员
与const指针与引用类似,可以将一个普通的iterator转换为对应的const_iterator,但反之不行。
int main()
{
list<int> l;
list<int>::iterator ite = l.begin();
list<int>::iterator ite2 = l.cbegin(); //错误,常量不能转换成非常量
list<int>::const_iterator ite3 = l.begin(); //使用了const迭代器,就不能对非常量对象l进行修改了
list<int>::const_iterator ite4 = l.cbegin();//使用了const迭代器,就不能对非常量对象l进行修改了
const list<int> l2;
list<int>::const_iterator iter1 = l2.begin();
list<int>::const_iterator iter2 = l2.cbegin();
list<int>::iterator iter3 = l2.begin(); //错误,
//l2.begin返回的是const_iterator,常量不能转换为非常量
list<int>::iterator iter4 = l2.cbegin(); //错误,常量不能转换成非常量
return 1;
}
当使用auto与begin和end结合时,获得的迭代器类型依赖于容器类型,与我们想要如何使用迭代器毫不相干。就是容器的类型的const的,则返回const_iterator;如果容器的类型不是const 的,则返回iterator。但是如果auto和以c开头的cbegin和cend结合使用,还是可以获得const_iterator的,而不管容器的类型是什么,即不论容器是否是const的,使用了c开头的版本就会得到const_Iterator.
2.容器初始化(指定元素个数)
当给容器初始化指定元素个数的时候,如果元素是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。如果类类型中没有默认构造函数只有成员变量默认初始值也是不行的,因为container<type> i(5); 会调用默认构造函数来创建type类型的对象,所以会调用默认构造函数,如果光有默认初始值而没有默认构造函数则会报错。
3.容器的初始化
当传递迭代器参数来拷贝一个范围时,就不要求容器的类型是相同的了,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换成要初始化的容器的元素类型即可。
这里主要说明一下string类型和C风格字符串类型(字符数组)以及指向字符数组的指针之间的关系。
允许字符数组或指向字符数组的指针为string对象赋值或者拷贝,string类中会有各自的重载operator=和拷贝构造函数来接受数组参数和数组指针参数这两个不同类型的参数。
char i[5] = "abcd";
//C风格字符串中的此种方式最好,因为不需要确定字符数组的大小,而且字符串的大小本身也说不好
char *pp = "abcd";
char *p = i;
string s = "abcd";
string s1 = i; //i是数组的地址
string s2(i);
string s3 = p;
string s4(p);
string s5 = pp;
string s6(pp);
以上都是正确的。具体可看primer321页。
所以
vector<const char *> articles = {"a","an","the"};
forward_list<string> words(articles.begin(),articles.end());
容器的类型:1.是否有const 2.容器本身的类型vector/deque/list等
4.容器的赋值和swap
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效(右边容器的迭代器不会有影响)。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。 这里的失效指的是定义在赋值和swap操作之前的迭代器会失效。
int main()
{
int i;
vector<int> a = {6,7,8,9};
vector<int>::iterator ite = a.begin();
vector<int> b = {1,2,3,4,5};
vector<int>::iterator ite1 = b.begin();
cin >> i;
if (i==1)
{
a = b;
cout << *++ite << endl; //0,a的迭代器失效
cout << *++ite1 << endl; // 2
}
else if (i == 2)
{
a.swap(b);
cout << *++ite << endl; //7,a的迭代器依然有效,并未指向2
cout << *++ite1 << endl; //2
cout << a[1] << endl; // 2
cout << b[1] << endl; //7
}
return 1;
}
可以看出来使用了swap后,a的迭代器还是指向原来的元素,因为除了array外,swap不会对任何元素进行拷贝、删除或者插入操作;元素本身并未交换,swap只是交换了两个容器的内部数据结构。就是把两个容器的名字给交换了?
如果在赋值或者交换完之后在定义迭代器,那么迭代器就会指向交换后的容器。就没有失效这一说了,所以在这里主要讨论研究在赋值和swap操作之前定义的迭代器。
int main()
{
int i;
vector<int> a = {6,7,8,9};
vector<int> b = {1,2,3,4,5};
cin >> i;
if (i==1)
{
a = b;
vector<int>::iterator ite = a.begin();
cout << *++ite << endl; // 2
}
else if (i == 2)
{
a.swap(b);
vector<int>::iterator ite = a.begin();
cout << *++ite << endl; //2
}
return 1;
}
array类型:swap两个array会真正交换它们的元素。赋值操作也不会使a的迭代器指向未知地址,而是使它指向新的容器的元素(也可以称此为失效)。
int main()
{
int i;
array<int, 5> a = {5,6,7,8,9};
array<int, 5>::iterator ite = a.begin();
array<int,5> b = {1,2,3,4,5};
array<int, 5>::iterator ite1 = b.begin();
cin >> i;
if (i==1)
{
a = b;
cout << *++ite << endl; //2
cout << *++ite1 << endl; //2
}
else if (i == 2)
{
a.swap(b);
cout << *++ite << endl; //2
cout << *++ite1 << endl; //6
cout << a[1] << endl; //2
cout << b[1] << endl; //6
}
return 1;
}
可以看出array使用swap之后,a的迭代器指向了原来b的元素。
string类型:与array同理。
int main()
{
int i;
string a = "abcde";
string::iterator ite = a.begin();
string b = "fghij";
string::iterator ite1 = b.begin();
cin >> i;
if (i==1)
{
a = b;
cout << *++ite << endl; // g
}
else if (i == 2)
{
a.swap(b);
cout << *++ite << endl; //g
}
return 1;
}

使用assign:
int main()
{
int i;
vector<int> a = {5,6,7,8,9};
vector<int>::iterator ite = a.begin();
vector<int> b = {1,2,3,4,5};
vector<int>::iterator ite1 = b.begin();
a.assign(b.begin(), b.end());
cout << *++ite <<endl; // 2,由此可见
cout << *++ite1 << endl; // 2
return 1;
}
由于旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。
assign后a的迭代器指向的是b的元素,由此可见assign并不像swap那样只改变数据结构(只改变了变量名),而是像swap数组array类型那样进行了真正的拷贝删除操作。
也可以从赋值的角度来说,assign与赋值不同的是赋值后原容器的迭代器失效,而assign后原容器的迭代器没有失效而是指向了新容器。
四、容器的结构与分类(不涉及源码)
三类:1. 顺序容器 2.关联容器分为有序和无序的。
1.容器的分类

注1:使用容器最重要的一点是要清楚容器在内存中是什么样的,元素与元素之间的关系是怎样的(连续,树,哈希表等等)。所以容器可以分为以下几类:序列式的容器(内存中的存放不一定是连续的),关联式的容器(即容器的元素有key和value,通过key找到value,一般用在快速查找),不定序容器(由hash table实现)(也可以划分到关联式容器)。
注2:
Array的元素个数是确定的,无法扩充,在C++11中把数组封装成了类。
Vector的元素个数不定,会自动向后添加元素,这个添加元素分配内存的动作是由分配器执行的。
Deque双向队列,两端都是可以扩充的。
List链表,双向链表,每个元素带有两个指针。(不一定是连续存储的)
Forward-List,单向链表,每个元素只有一个指针。(不一定是连续存储的)
注3:在STL中并没有规定set和map应该用什么数据结构来实现,但是由于红黑树(在数据结构里面再讨论)非常好,所以各个编译器都用红黑树来做set和map。map的每一个节点都有key和value,而set的value就是key,key就是value。set/map代表元素中的key不能重复(注意是key不能重复,value可以重复的);而multiset/multimap可以。
注4:不定序容器是用哈希表数据结构实现的(有多个basket,每个basket带有一个链表,链表的元素个数是动态的,哈希表有多种做法/形式,现在被采用的最好的做法是seperate chaining,什么是seperate chaining?比如说有20个内存,有abcde五个元素,这五个元素经过某种运算来决定放入到哪个内存中,比如a和e经过计算都要放到第3个内存中这就会发生碰撞所以就要把它们分开:把a放到第3个,e本来也应该放到第三个但是硬把它放到第七个,但是这种分离方法不够理想,所以最后发展出seperate chaining把碰撞的元素都放在同一个内存中,只不过这个内存是一个链表。但是每个basket的链表的长度不能太长否则在查找一个basket中链表中某个数据所花费的时间太久所以会有一些法则,具体什么规则在此不讲)。
在介绍容器之前先引入一个辅助函数

注:这几个辅助函数是为了配合下面讲容器时所使用的代码。
RAND_MAX表示target的最大范围;snprint表示将随机数转换成随即的字符串。
void *表示任何指向任何类型的指针都可以传入?
compareLongs的参数是指针类型的原因是因为使用算法的函数时,算法函数的参数要求。
2.容器array的使用

注1:
在写测试程序的时候把每段单元写在单独的namespace里面,避免与其他的namespae的变量或者定义冲突。每个namespace上面会有这个单元所需要的头文件,即使头文件重复也没有关系,因为头文件有一个保护机制,所以每个单元所需要的头文件在这个单元上方即可查找到,而不需要再到程序的最上方查找了。
在写正规程序的时候一般将变量的声明或者定义写在起始处,在写测试程序时则是在使用处定义变量,为了方便找到定义的变量,定义的时候采取顶格写。
注2:array<type,ASIZE>中的ASIZE是一个固定的值,因为数组的大小是固定的。
注3:
放50w个数需要消耗的总时间。
size表示array的元素个数。
array.front()和array.back()表示第一个元素和最后一个元素的值。
data返回数组在内存中开始的地址。
注4:
下面的代码是将这50w个数值进行排序,采用的算法是qsort函数。
然后采用二分查找法bsearch(使用二分查找法的前提是必须对元素进行排序)查找一个我要的目标,并且计算排序并查找这个目标花费了多少时间。
3.容器vector的使用


注1:push_back表示向后添加元素,因为当一个容器占用了内存之后不能在接着这个内存之后扩充的,因为可能会扩充到别人已经用掉的内存了,所以当这个容器的元素是在连续内存中存放的时候如vector,应该在其他没有使用的空间开辟出两倍于之前空间大小的内存空间以达到扩充的目的, vector实际分配的内存空间是成两倍增长的(这里的两倍增长指的不是在原来的那一段内存增长,而是在重新分配了一段两倍于之前大小的内存然后把之前内存存放的数据迁移过来),所以实际的内存空间一定比存放的元素个数大(可能会造成内存的浪费)。
vector.size()表示实际的元素个数。
vector.capacity()表示vector的实际内存空间有多大。
try和catch的目的在与当元素 太多,内存不够时报出异常。
注2:::find返回的类型时iterator。
采用了两种不同的查找方法,第一个查找方法是find循序查找,所花费的时间取决于元素在容器的中位置,靠前则花费时间短,反之长。第二个方法是排序然后二分查找法,返回的是指针类型,如果没找到就返回NULL。
4.容器list的使用

注1:list的push_back(dynamic allocate too)与vector的不同,这里是一个元素(节点)一个元素放入到后面的空间中的(链表可以在内存不是连续存放的,所以与vector内存是连续的扩充时是重新开辟一个两倍于之前内存的空间有所区别)。list不会造成内存的浪费,但是在运用一些查找等算法时所花费的时间较多(关联式容器的查找速度会快很多几乎为0ms)。
max_size表示在list中最多可以存放多少个Tp类型的元素。
注2:注意这里的197行调用的sort排序算法是容器内部自带的,不是调用的标准库中的。某些容器内会自带sort算法时应该用自带的,它的速度一定比调用标准库的快。
注:list不可以使用标准库提供的算法::sort(),只能使用list内部自带的sort算法。

只有这种RandomAccessIterator的迭代器才可以获取位置不定的元素,而list是一个链表,它是一个节点一个节点组成的,所以它的迭代器不是RandomAccessterator,具体可看list的迭代器类的定义,不能ierator+n这样使用,所以只能使用类内定义的sort。
vector和deque都是提供RandomAccessIterator的。
5.容器forward-list的使用

注:只有push_front,只允许从头扩大/扩充,与vector只允许从尾部扩充只好相反。
6.容器slist的使用
与forward list一样都是单向链表,只不过这个slist时GNU编译器提供的,而forward-list是c++11标准库提供的,两者功能一样。 这里想要使用slist必须要#include<ext/slist> (linux下使用/,win下使用\) , 定义变量时也与forward-list<string> c;不同。

7.容器deque的使用
deque

注:deque是两边都可以扩充,那么它的具体结构是什么样的?如下图所示。
deque的每一个方格内其实存放的是一个指针,这个指针指向一段数据(可以存放n个元素),这一段叫作buffer。所以deque就是由这一段一段构成的,deque实际上是分段连续(deque号称是连续的,其实这是一种假象,段与段之间实际在空间中并不是连续的,只有段内的空间是连续的,但是必须要做出deque整个是连续的这种假象让使用者感觉这些段与段之间是连续的)。
push_back 和 push_front如图所式,会分配新的buffer(每次扩充一个buffer)。

stack

注1:从左上图可以看出一个deque涵盖了stack的功能,所以stack的源码是以deque作为支撑的并没有涉及什么数据结构。
注2:stack只有push()和pop()。
queue
同样以deque作支撑。

注:stack和queue不允许有各自的iterator,因为迭代器(泛化指针)可以改变中间的某个值,这就违背了先进先出,先进后出的规则了。
8.容器multiset的使用

注1:没有push_back/push_front,安插元素就是insert,元素该安插的位置遵循一定的规则(red-black tree)。
注2:find不论是标准库提供的还是容器提供的在查找的时候速度都是比较快的,所以当不在乎元素存放的速度,只需要在查找的时候速度快就可以使用此容器。
注3:这里multiset可以放入相同的元素,所以可以放入100w个数据,如果是set容器不允许放入相同的元素,那么最多只能放入32768个数据,因为在此例中只有32768个不同的随机数。
9.容器multimap的使用

注1: multimap<long,string> c; 中的long表示key的类型,string表示value的类型。
注2: 使用insert的时候要自己将key和value组合起来。实际上这里的key是从0~999999(100w个元素),key不会重复,key对应的value是可能重复的。
10.unordered_multiset的使用


注:篮子的数量是一定比元素多的。而每个篮子拉出去的元素不能太长,因为查找的时候,在每个篮子上采用的是循序查找,如果链表太长会导致查找时间很长。
如果元素的个数大于等于篮子的个数,那么篮子的个数会增加到两倍于元素的个数,并且会将元素打散,然后重新挂在篮子上。所以最大的篮子数一般与最大的元素个数相同。
c.insert()是根据某种运算/某种规则后再决定放到哪个basket中的。
11.unordered_multimap的使用

12.容器set的使用
注:在100w个元素中存放0~32767中的数,一定会有很多重复,又因为set的key是不可以重复的,所以不可能放入100w个数,最多放32768个数。
13.容器map的使用

注:multimap是不可以用[]插入元素的。在map的使用中是可以的,此处i就是key,等式右端就是value。所以key不会重复,只有value会重复。
当然insert也是可以使用的。
14.unordered_set、unordered_map的使用



注: 
四、分配器的简单介绍
容器的背后需要一个东西来支持它对内存的使用,这个东西就是分配器。
以下这张图片中使用的都是标准库中的分配器



注:c1~c6是装有不同分配器的list容器,每次push_back都会分配内存,而分配内存就会用到分配器。
那么可不可以直接使用分配器来为数据分配内存?当然可以,因为分配器就是一个class,它有两个函数一个allocate,一个deallocate。但是没有什么必要,一般都是使用容器时使用。平常如果想分配内存都会使用new [](malloc) 和 delete[] (free),因为只有malloc的时候会指明分配多少个元素,而free不需要指明;但是如果使用分配器就需要在分配和释放的时候都指明元素的个数很麻烦。

五、分配器工作原理的简单介绍
1.operator new和malloc()回顾
分配器是一个class类,所有的分配器都会向下调用到operator new 最后归根都会调用 malloc()
而malloc()除了会分配数据存储的内存之外,还会消耗一些内存用来存放别的东西。

下面以默认的分配器为例介绍其源码,主要观察operator new和malloc。
2.VC6编译器下STL对分配器的使用及源码


注1:一个分配器中最核心的两个函数就是allocate和deallocate。
注2:第二个参数是一个tip,表示传入的数据是什么类型的。
注3:匿名对象. 一般不推荐写程序时直接使用分配器,因为要记住分配的元素个数,而容器在使用分配器时不会出现这种情况,不需要认为记忆。
注4:这里的_Allocate函数调用的operator new指的是全局作用下的operator new函数。::operator new函数是可以被显式调用的,但是其他的有关内置类型的operator+等不可以被显式调用(笔记四中的运算符重载的前几张图片中也说明过)。
int main()
{
int a = 10, b = 15;
operator new(sizeof(int)); // true
operator+(a,b); //错误
a.operator+(b); //错误
return 1;
}
3.BC5下STL对分配器的使用及源码


注:这里的=0代表默认为int类型 ?
4.G2.9下STL对分配器的使用

注:GNU的容器不会使用这个分配器,虽然这个分配器也符合标准规格。容器实际会使用的分配器是下面slide中的alloc。因为alloc比此allocator分配器节省内存。

注:这里并没有给出alloc类的代码,只讲述alloc是如何工作的 。
运行模式。在前面的slide中表明malloc分配内存的时候会产生额外的开销,而我们不想要这个额外的开销,当然cookie这个记录分配了多少内存的开销还是要保留的,因为在回收的时候会用到,malloc在为大小不同的数据分配内存时情况是这样的。然而容器的元素大小都是相同的,所以不必为每个元素分配内存时都用cookie记录分配内存的大小(多少个字节),所以这个分配器alloc就从这个方向出发着手减少malloc的次数。

工作方式:设计了一个有16条的链表,每一个链表负责一个特定大小的区块并用链表串起来。每个链表负责8的倍数个字节。当分配器需要内存的时候都来跟这个链表要内存,容器的元素大小会被调整到8的倍数,比如说某个容器的一个元素是50个字节的,那么系统会把它变成56个字节,然后向第7个链表要内存,这第七个链表会分配一大块内存(根据元素的个数来的),然后把这一大块内存做分割,分割成n(代表元素个数),这样切出来的每一块小区块就不带cookie,但是malloc出来的一大块内存还是带cookie的,尽管如此还是会节省很大一部分内存。
5.G4.9下STL对分配器的使用


注1: 这个allocator是定义在std命名空间下的。 它所继承的父类是定义在_gnu_cxx命名空间下的。
注2: 在GNU4.9下却没有使用上一个slide所使用的那个很棒的alloc分配器,为什么不使用侯捷也不知道,但是那个很棒的alloc还在,只不过名字变了,从之前的alloc变成了_pool_alloc。

注1:这些扩展的分配器在第一讲中已经提到过range allocator等,都需要#include <ext/range_allocator.h>。
注2:这些扩展的分配器都是定义在__gnu_cxx命名空间下的。
注3:8指的就是上面的第1个链表的所代表的8个字节,而128指的就是第16个链表代表的128个字节,最后的代表共有16个链表。
每种分配器的大小如下所示:

六、OOP与GP
OOP:企图将method和data关联在一起,method和data都定义在一个类中。
GP:企图将method和data分开,method单独定义在一个类中,data单独定义在一个类中,然后用迭代器联系起来。

所有的algorithm,其内最终涉及元素本身的操作,无非就是比大小。
更多推荐


所有评论(0)