前言
"打牢基础,万事不愁" .C++的基础语法的学习."学以致用,边学边用",编程是实践性很强的技术,在运用中理解,总结.
引入
STL(标准模板库)的学习.以<C++ Prime Plus> 6th Edition(以下称"本书")内容理解 .算法是STL中的一个重要内容.C++基础语法:STL之算法(一)-CSDN博客的续篇
函数和容器方法
本书P716:有时可以选择使用STL方法或STL函数。通常方法是更好的选择。 首先,它更适合于特定的容器;其次,作为成员函数,它可以使用模板类的内存管理工具,从而在需要时调整容器的长度。
----解读:
容器方法优于算法.更重点的是概念的变化:用"方法"表示容器类对象方法,STL函数表示"算法".此前一律称函数---非对象函数,对象函数,静态函数等.这里把"方法"分出来了,和类有关的称"方法",类似Java.STL函数(算法)强调的是"通用"
例如,假设有一个由数字组成的链表,并要删除链表中某个特定值 (例如4)的所有实例。如果la是一个list对象,则可以使用链表的 remove( )方法:
//本书代码
la.remove(4); //remove all 4s from the list
调用该方法后,链表中所有值为4的元素都将被删除,同时链表的长度将被自动调整。
还有一个名为remove( )的STL算法(见附录G),它不是由对象调用,而是接受区间参数。因此,如果lb是一个list对象,则调用该函数的代码如下:
//本书代码
remove(lb.begin(),lb.end(),4);
然而,由于该remove( )函数不是成员,因此不能调整链表的长度。它将没被删除的元素放在链表的开始位置,并返回一个指向新的超尾值的迭代器。这样,便可以用该迭代器来修改容器的长度。例如,可以使用链表的erase( )方法来删除一个区间,该区间描述了链表中不再需要的部分。程序清单16.18演示了这是如何进行的。
----解读:
演示了容器方法比算法好的原因,容器方法可以自动调整链表长度.容器方法比算法更"纯粹"
两种写法很好区分:容器方法是由对象调用:对象.方法名(参数列表)
STL函数调用:函数名(迭代器对象参数,其他参数)
本书代码16.18listrmv.cpp,注释是自己加的
#include <iostream>
#include <list>
#include <algorithm>
void Show(int);
const int LIM = 10;
int main()
{
using namespace std;
int ar[LIM] = { 4, 5, 4, 2, 2, 3, 4, 8, 1, 4 };
list<int> la(ar, ar + LIM); //生成list对象
list<int> lb(la); //复制对象
cout << "Original list contents:\n\t";
for_each(la.begin(), la.end(), Show);
cout << endl;
la.remove(4); //容器删除
cout << "After using the remove() method:\n";
cout << "la:\t";
for_each(la.begin(), la.end(), Show); //打印
cout << endl;
list<int>::iterator last; //声明迭代器,接收算法删除后的迭代器
last = remove(lb.begin(), lb.end(), 4); //和上一句可以合在一起,算法删除
cout << "After using the remove() function:\n";
cout << "lb:\t";
for_each(lb.begin(), lb.end(), Show); //打印
cout << endl;
lb.erase(last, lb.end()); //删除迭代器区间的结点
cout << "After using the erase() method:\n";
cout << "lb:\t";
for_each(lb.begin(), lb.end(), Show);
cout << endl;
// cin.get();
return 0;
}
void Show(int v)
{
std::cout << v << ' ';
}
从中可知,remove( )方法将链表la从10个元素减少到6个元素。但对链表lb应用remove( )后,它仍然包含10个元素。最后4个元素可任意处理,因为其中每个元素要么为4,要么与已经移到链表开头的值相同。
尽管方法通常更适合,但非方法函数更通用。正如您看到的,可以将它们用于数组、string对象、STL容器,还可以用它们来处理混合的容器类型,例如,将矢量容器中的数据存储到链表或集合中。
输出结果:
Original list contents:
4 5 4 2 2 3 4 8 1 4 //原始数据
After using the remove() method:
la: 5 2 2 3 8 1 //容器方法remove删除后的结果
After using the remove() function:
lb: 5 2 2 3 8 1 4 8 1 4 //算法删除remove后的结果
After using the erase() method:
lb: 5 2 2 3 8 1 //容器方法erase删除结点后的结果
---解读:
容器方法更适合,函数更通用.
容器方法,一步达到效果,删除元素和结点.算法也可以,但是要达到同样的效果,需要多一步调用erase.因此操作元素首选容器方法,次选算法.在容器之间传递数据,多半只能靠算法(STL函数)--假设
使用STL
本书:STL是一个库,其组成部分被设计成协同工作。STL组件是工具, 但也是创建其他工具的基本部件。我们用一个例子说明。假设要编写一个程序,让用户输入单词。希望最后得到一个按输入顺序排列的单词列表、一个按字母顺序排列的单词列表(忽略大小写),并记录每个单词被输入的次数。出于简化的目的,假设输入中不包含数字和标点符号。
----解读:原书附带的示例,学习别人的编程思路
本书:输入和保存单词列表很简单。可以按程序清单16.8和程序清单16.9 那样创建一个vector对象,并用push_back( )将输入的单词添加到矢量中:
//本书代码
vector<string> words;
string input;
while(cin>>input&&input !="quit")
words.push_back(input);
----解读:常规写法,建立个string集合,数据结构使用vector,添加输入的string对象
输入结束条件: input!="quit".当字符串输入quit即表示输入结束.
本书:如何得到按字母顺序排列的单词列表呢?可以使用sort( ),然后使用unique( ),但这种方法将覆盖原始数据,因为sort( )是就地算法。有一 种更简单的方法,可以避免这种问题:创建一个set对象,然后将矢量中的单词复制(使用插入迭代器)到集合中。集合自动对其内容进行排序,因此无需调用sort( );集合只允许同一个键出现一次,因此无需调用unique( )。这里要求忽略大小写,处理这种情况的方法之一是使用transform( )而不是copy( ),将矢量中的数据复制到集合中。使用一个转换函数将字符串转换成小写形式。
----解读:
1>首先提出了解决办法1:sort()+unique().因为sort()是就地算法,会覆盖原始数据,所以没用上.要想不覆盖原始数据,可以先做个备份,这样写:
vector<string> words_bak(words); //复制一个string对象
//以下代码用了算法,可能有出入
sort(words_bak.begin(),words_bak.end()); //排序
unique(words_bak); //假设有个叫unique的算法去除多余元素,没有自己写
但是他没有解决忽略大小写的问题,如果要这样写,应该先调用下面transform()的函数,因为书上不支持这种写法,所以不展开了,理解意思就行.
2>因为需求的集合是排序而且要求不重复,set符合这个要求,即创建了一个set对象.
copy()和transform():copy()复制一个新的集合,不能改变数据;transform()可以在复制时候更改数据,这里要求把大小写转换,所以选择了transform()
//本书代码
set<string> wordset;
transform(words.begin(),words.end(),
insert_iterator<set<string> >(wordset,wordset.begin()),ToLower);
解读函数transform():4个参数的transform(),表示某区间(前两个参数表示,类型为迭代器)里每个元素被函数符(第4个参数)处理,结果发给迭代器(第3个参数).
函数规定了第4个参数的函数指针的输入值和返回值类型,和迭代器解引用后时类型一致(可以是引用类型,他们不构成函数重载).内在逻辑不管.transform()还规定了t值是遍历words后的值---也不算特别,因为函数指针做参数的函数里,其使用参数一般也在形参列表内.参考"函数指针"
//伪代码:T表示迭代器解引用后的类型
//函数的输入值类型和输出值类型已确定
T fun(T t); //t值已确定,从*words.begin()开始往后取值,直到word.end()前一个值
T fun(T& t); //形式2
T& fun(T& t); //形式3
//几种形式取其一,因为两种形参类型都能接收t,返回值不管是T或者T&,在送到迭代器的时候都是T类型
函数指针的优点:不管函数怎样编写,输入值类型和输出值类型已确定.可以起"逻辑占位"的作用,在不确定他代表的逻辑的情况下,继续程序的编写
transform()中使用的函数指针ToLower正是使用了从顶向下的编程思路,暂时放下实现细节.对程序员而言,编程时可以用这种思路把整体和局部分开(难道是架构师与工程师的区别?)
================================内容分割线=================================
编程提供抽象的表达和抽象的实现,罗列一些程序中的抽象和具体
变量是数据的抽象,值是具体的数据
函数指针是逻辑的抽象.函数是具体的逻辑
接口是需求的抽象,实现类是具体需求的满足.
抽象和具体的共通点:抽象=具体 这个等式恒成立(实现类转换成接口的引用).
当抽象被具体描述后,该部分代码完成.
================================内容分割线================================
本书:ToLower( )函数很容易编写,只需使用transform( )将tolower( )函数应用于字符串中的各个元素,并将字符串用作源和目标。记住,string对象也可以使用STL函数。将字符串按引用传递和返回意味着算法不必复制字符串,而可以直接操作原始字符串。下面是函数ToLower( )的代码:
//本书代码
string& ToLower(string& st){
transform(st.begin(),st.end(),st.begin(),tolower);
return st;
}
----解读:
string对象也可以使用STL函数.string对象实现了begin(),end(),operator*(),operator++()这些函数,所以string对象可以使用STL接口函数 .---string类型事实上属于"字符容器"类型.
ToLower函数的内部逻辑:传入一个string对象,遍历每个字符,.调用函数tolower将其每个字符大写转换成小写,因为string类是字符容器,所以遍历和修改容器元素,再次使用STL函数transform()
本书:一个可能出现的问题是:tolower( )函数被定义为int tolower(int),而一些编译器希望函数与元素类型(即char)匹配。一 种解决方法是,使用toLower代替tolower,并提供下面的定义:
//本书代码
char toLower(char ch){return tolower(ch);}
----解读:有的编译器tolower函数使用int类型做形参和返回值.所以这样做是为了保险.
这部分有一个小问题:ToLower函数修改了原值,所以不能重现最初输入(解决办法做备份).但在这个版本的要求中,没有重现的需求,而是改了原值再继续操作.
本书:要获得每个单词在输入中出现的次数,可以使用count( )函数。它将一个区间和一个值作为参数,并返回这个值在区间中出现的次数。可以使用vector对象来提供区间,并使用set对象来提供要计算其出现次数的单词列表。即对于集合中的每个词,都计算它在矢量中出现的次数。要 将单词与其出现的次数关联起来,可将单词和计数作为pair对象存储在map对象中。单词将作为键(只出现一次),计数作为值。这可以通过一个循环来完成:
//本书代码
map<string,int> wordmap;
set<string>::iterator si;
for(si=wordset.begin();si!=wordset.end();si++)
wordmap.insert(pair<string,int>(*si,count(words.begin(),words.end(),*si)));
----解读:新的STL函数count(),提供区间和值作为参数,求得值在区间出现的次数.
然后创建一个map对象,键类型string,值类型int,调用insert方法,存储string对象出现的次数.
本书:map类有一个有趣的特征:可以用数组表示法(将键用作索引)来访问存储的值。例如,wordmap[“the”]表示与键“the”相关联的值,这里是字符串“the”出现的次数。因为wordset容器保存了wordmap使用的全部键,所以可以用下面的代码来存储结果,这是一种更具吸引力的方法:
//本书代码
for(si=wordset.begin();si!=wordset.end();si++)
wordmap[*si]=count(words.begin(),words.end(),*si);
因为si指向wordset容器中的一个字符串,所以*si是一个字符串,可以用作wordmap的键。上述代码将键和值都放到wordmap映象中。
----解读:强烈推荐用这种方法来插入map类型元素:对象名[键]=值; 替代insert方法插入元素,写法更优雅,易懂.
同样,也可以使用数组表示法来报告结果:
//本书代码
for(si=wordset.begin();si!=wordset.end();si++)
cout<<*si<<":"<<wordmap[*si]<<endl;
如果键无效,则对应的值将为0。
这里的寓意在于,使用STL时应尽可能减少要编写的代码。STL通用、灵活的设计将节省大量工作。另外,STL设计者就是非常关心效率的算法人员,算法是经过仔细选择的,并且是内联的。
----解读:尽量使用STL函数来解决问题.
小结
STL函数(算法)的优点有:
1>为访问和修改容器元素提供了统一的接口(一个transform()函数就能解决大部分问题),迭代器区间提取数据,函数符提供数据处理方法.
2>在不同容器间传递数据很方便.