本文将主要介绍C++提供给我们的可重用代码:string类、标准模板库(STL)即处理各种容器对象的模板、其他库`。
string类:
定义在头文件string中,而头文件cstring包含了许多处理C风格的字符串的函数。
string类所包含的方法大致可分为:构造函数、赋值、比较、访问字符串中的各个元素、查找字符或是字符串、大量的运算符重载函数。
string实际是模板basic_string<char>具体化后的别名。所以我们使用的string不过是一个模板的实例化。具体来说有四种具体化:基于char、基于wchar_t、基于char16_t、基于char32_t。
类型参数traits描述如何对该字符类型进行比较等操作,allocator描述的是内存管理
1、构造函数(其中string支持STL???):参数可分为string对象、C风格字符串、char类型、迭代器、初始化列表、右值引用,再加一个指定复制的长度(对于c风格字符串来说,若指定的长度大于已知字符串的长度,编译器会继续搜索内存地址之后的字符复制给新的string对象)。
#include "stdafx.h"
#include<string>
#include<iostream>
using namespace std;
int main()
{
string a("dkfjjfdif");
cout << a << endl;
string b(20,'a');
cout << b << endl;
string c(a);
cout << c << endl;
char d[] = "bdfe";
string e(d, 3);
cout << e << endl;
string f(d, d + 4);//使用迭代器(封装指针的类型)
cout << f << endl;
string g(b, 2, 6);
cout << g << endl;
cin.get();
return 0;
}
2、string对象的输入
对于C风格的字符串,输入函数定义在istream类内。
cin.get(char * s ,int n)和cin.getline(char *s,int )区别:第一个方法保留空白符;第二个抛弃空白符。
对于string对象来说,输入流的值存入到string对象,是以友元函数声明,而不是在类iostream定义。此外,根据输入流中字符的个数动态调整string对象的大小,因而他也没有输入字符个数的限制。但是在string类中还是用常量string::npos指定了最多字符个数,若是想将一个很大的文件存入到一个string对象可能出现问题。
cin>>s或是 getline(cin,s)
此外,对于getline方法,还有一个重载函数有三个参数,第三个参数用于表示输入的边界。不声明分界符,将默认空白符为分界符,但声明了分界符,空白符就是普通的字符来处理。
简单的来分析一下:何时从停止读取输入流字符到string对象?
1、文件末尾
2、分界符
3、超过最大长度
#include "stdafx.h"
#include<iostream>
#include<fstream>
#include<cstdlib>
#include<string>
using namespace std;
int main()
{
ifstream fs;
fs.open("1.txt");
string str;
if (fs.is_open()==false)
{
cout << "file open error";
exit(EXIT_FAILURE);
}
getline(fs, str, ':');
int count = 1;
while (fs)
{
cout << count++<<str << endl;
getline(fs, str, ':');
}
fs.close();
cin.get();
return 0;
}
获取字符串的长度:
length()和size(),size()是为STL的兼容性而提供的。
string是根据实际字符的大小分配内存空间的,但起初呢,也会分配一个稍大的空间免得之后挪动,capacity()可显示出当前位该字符串分配的空间大小。reserve()可请求内存空间。
string test;
cout<<test.capacity()<<endl;
test.reserve(50);
cout << test.capacity();
string转化为C风格字符串,比如open()打开文件时要求C风格的字符串,为此string类提供c_str()成员函数,返回一个指向该字符串的指针。
string类重载了> < ==等运算符,使得能进行字符串、C风格字符串、字符串之间的比较操作。
在字符串中寻找特定的子串和字符,同样重载了特征标,可以是C风格字符串、字符串、从特定的位置开始、甚至是给定字符串的子串、字符:
rfind查找特定字符串最后出现的位置。
find_first_of():特定字符串中所有字符中最早出现在待匹配字符串的索引。
find_last_of():特定字符串中所有字符中最后出现在待匹配字符串的索引。
find_first_not_of():特定字符串中所有字符中最早不包含在待匹配字符串的字符索引
find_last_not_of():特定字符串中所有字符中最后不包含在待匹配字符串的字符索引
注意上述的方法在找不到特定的子串时将返回string::npos常量
#include "stdafx.h"
#include<iostream>
#include<string>
#include<cstdlib>
#include<ctime>
using namespace std;
const int SIZE = 5;
const string choice[SIZE] = { "hello","C++","and ","love ","you!" };
int main()
{
srand(time(0));
cout << "would you continue?";
char y;
cin >> y;
while (y == 'y') {
string result = choice[rand() % SIZE];
int len = result.length();
string attempt(len,'-');
cout << result << endl;
cout << attempt << endl;
string badchar;
char c;
cout << "you need to guess a " << len << "string" <<"and you have "<<len<<" choices"<< endl;
while (len > 0 && attempt != result)
{
cout << "choose one character:";
cin >> c;
if (attempt.find(c) != string::npos || badchar.find(c) != string::npos)
{
cout << "you have already guess one! try new one";
continue;
}
int pos = result.find(c);
if (pos == string::npos)
{
len--;
badchar += c;
cout << "bad guess " << endl;
//continue;
}
else
{
attempt[pos] = c;
int temp = pos;
int reindex = result.find(c, temp + 1);
while (reindex!=string::npos)
{
attempt[reindex] = c;
temp = reindex + 1;
reindex = result.find(c, temp );
}
}
cout << "after this try ,current string is " << attempt << " and you have" << len << " choices left" << endl;
}
if (attempt == result)
cout << "succeed!" << endl;
else
cout << "what a pity " << endl;
cout << " would you continue this game:";
cin >> y;
}
cout << "goodbye!" << endl;
return 0;
}
智能指针类模板(指针指向动态内存空间,此时可利用智能指针模板类将此指针封装在类中,免去为该片内存空间的管理)
智能指针的另一面是什么呢?常规指针!智能只是个修饰词,重在指针!智能体现在哪里呢?将指针类型嵌入在一个类,特别注意的是这些指针类型指向的都是运行阶段动态生成的内存空间,我们将其嵌入到类中就是希望当对象(包含含动态分配的空间)被释放时,由于类的机制,将自动调用析构函数,而我们析构函数中使用delete释放空间,这样一来,我们就不用担心使用常规指针分配空间后还得释放空间。而C++已经实现了智能指针的模板类,我们可以直接使用而不必自己设计。
C++为我们提供了auto_ptr<>、unique_ptr<>、shared_ptr<>三种智能指针模板类,根据我们需要指向类型来实例化模板参数。智能指针模板类的定义在头文件memory中。
何时采用智能指针?指针指向动态内存空间(运行时确定的)。
构造函数的实参要求是一个动态分配的内存空间地址。初始化智能指针对象时实参必须是new或是new[] 代表动态的内存空间地址,不然干嘛用智能指针呢!试想一下,如果实参是静态内存空间地址,会发生什么?当指针对象被释放时将导致该静态内存地址被释放,将带来悬空指针。绝对不能是静态地址。
在设计指针模板类,为了能使编程人员使用该智能指针以常规指针的方式,重载了*、->、=等运算符。记住把这个指针对象视作指针来用。
#include "stdafx.h"
#include<string>
#include<iostream>
#include<memory>
class Report {
std::string str;
public:
Report(const std::string &s) :str(s){
std::cout << "create!" <<std:: endl;
}
~Report()
{
std::cout << "destory!" << std::endl;
}
const std::string & get_str() const
{
return str;
}
};
int main()
{
{
std::unique_ptr<Report > p(new Report("hello"));//创建一个指向动态生成Report类对象的智能指针 动态指针是智能指针类的成员
std::cout<<p->get_str();
}
std::cin.get();
return 0;
}
关于智能指针可能产生的问题:我们知道智能指针间支持赋值运算,这使得多个不同的智能指针指向同一内存空间,相当于同一个空间将被释放两次,肯定会产生异常!类模板设计时是如何解决这个问题?
1、auot-ptr和unique_ptr采用了指针所有权转移策略,赋值的同时所有权发生了转移。只有拥有所有的权的指针对象才能释放所指向空间。但是又有问题了,所有权转移了,那么之前的指针对象就不能访问该片空间了,但是auot_ptr无法检测出这类问题,编译时不会纠正这类潜在的错误,而运行时发生空间不可访问,崩了。但是unique_ptr在编译的时候,就会阻止可能出错的赋值操作,更安全一些。这里用了可能出错的赋值操作,那么就有不会出错的赋值操作了:如果赋值对象是临时右值(函数返回值),此类智能指针的赋值编译器就会认为是安全的!对于unique_ptr编译器能识别出智能指针间赋值是否安全!(检查安全性的机制利用了移动构造函数和引用)
或是:
对于unique_ptr智能指针的赋值操作,我们希望能进行且后续为先前的指针赋予新的地址,为了能支持这一个想法,C++为我们提供了move()语法,使得move中的参数值可以赋给别的智能指针变量且之后能被重新再赋值利用。
unique_ptr支持new和new[],而shared_ptr不支持new[]
2、shared-ptr使用计数机制,多一个指向这片空间的指针,计数变量加1,计数变量为0时才能释放这片空间。
如何选取这些不同指针?
多个指针指向同一个空间,用shared-ptr模板类。当分配的空间是以new[]方式,只能使用unique_ptr模板类。
shared_ptr的构造函数支持unique_ptr类型的右值(临时)。
标准模板库:(STL)
泛型编程
提供了关于容器、迭代器、函数对象、算法的模板。使用STL可以构造各种容器和执行各种操作。
容器:存储多个同类型值的“数组”。
算法:完成特定任务的“处方”。
迭代器:遍历容器对象(数组元素),类似于数组指针。
函数对象:函数对象,可以是类对象(重载了()运算符的类),可以是函数指针(可以是函数名)。
所有的STL容器模板,都提供了一些通用的方法,比如:size()获取容器中元素的个数;swap()交换两个容器内容;begin()返回指向容器第一个元素的迭代器;end()一个返回指向超尾元素的迭代器。
迭代器是什么?广义指针,每个模板容器类都有一个别名为iterator的迭代器,指向容器中的元素。类似于指针,为迭代器实现了*、++等运算符重载函数。
如何利用迭代器访问容器中的所有对象?(注意这里容器对象的元素类型是double ,故而可使用*获取double元素的值直利用cout接输出,若是自定义类型,则需要进一步重载cout才可以输出,或是编写函数来输出元素的成员值)
栗子:
vector模板类在头文件vector中,其声明为:
vector模板的实例化方式(动态分配内存):
vector模板重载了[]获取容器元素、=运算符进行容器对象(同类型元素)间的赋值。
vector除了拥有STL容器模板所共同拥有的size 、begin、end方法外,还有些特有的方法,比如:push_back()在末尾添加元素,erase()删除给定区间的容器元素(一提到给定区间,想到使用begin和end方法),insert()方法在原容器对象的给定位置之前插入另一容器的给定区间的元素值。
#include "stdafx.h"
#include<string>
#include<vector>
#include<iostream>
struct Book
{
int id;
std::string name;
};
bool fillBook(Book & b);
void showBook(const Book &b);
int main()
{
std::vector<Book> book1;
Book temp;
while (fillBook(temp))
{
book1.push_back(temp);
}
int len=book1.size();
for (int i = 0; i < len; i++)
showBook(book1[i]);
std::vector<Book>::iterator pd;
for (pd = book1.begin(); pd != book1.end(); pd++)
{
showBook(*pd);
}
std::vector<Book> book2(book1);
for (pd = book2.begin(); pd != book2.end(); pd++)
{
showBook(*pd);
}
book2.erase(book2.begin(), book2.begin() + 3);
for (pd = book2.begin(); pd != book2.end(); pd++)
{
showBook(*pd);
}
book2.insert(book2.end(), book1.begin(), book1.end());
for (pd = book2.begin(); pd != book2.end(); pd++)
{
showBook(*pd);
}
book1.swap(book2);
for (pd = book1.begin(); pd != book1.end(); pd++)
{
showBook(*pd);
}
std::cin.get();
return 0;
}
bool fillBook(Book & b)
{
using std::cout;//总算是深刻体会到getline()和 cin.get()的用法了!
using std::cin;
std::string temp;
cout << "input book name ,or quit:";
//cin >> temp;1
getline(cin, temp);
if (temp == "quit")
return false;
b.name = temp;
cout << "you book id:";
cin >> b.id;
while (std::cin.get() != '\n')
continue;
return true;
;
}
void showBook(const Book &b)
{
std::cout << b.id << " :" << b.name << std::endl;
}
STL函数:
对于不同的容器来说,可能需要相同的操作,STL采用了更通用高效的方法,并没有为每一个模板容器类都逐个定义这些方法,而是采用非成员函数的方式,定义一系列的操作!这些非成员方法可针对不同的容器,减少了冗余外,还可以支持多个不同的容器对象间的操作。比起成员函数,功能更强大(只支持同种容器间的操作)。为了使用这些STL函数,在设计模板容器类的时候需要根据函数特性来设计模板容器类。(!!!从这里我们可以充分的感受到面向对象编程和泛型编程的差别,一个重在数据的设计,在有了数据结构的基础上设计类的方法,而泛型重在STL函数设计,在有了函数设计之后再来设计模板容器类,虽然思路不一样,但是终极目标相同!!!代码的可重用性,一个是通过继承、包含来重用类方法;一个是利用已有的模板容器和算法来存储数据调用函数来重用代码)此外,STL也确实在容器模板类定义了与STL函数相同的操作,这些操作往往效率更高。STL函数定义在头文件algorithm中。
几个常用的STL函数:for_each()、random_shuffle()、sort()。
for_each(),三个参数:两个迭代器以表示容器元素区间,函数对象以表示对该区间内的元素所应用的函数。
C++11专门为容器类提出了范围for和for_each()功能差不多,但写起来更方便,最重要的是支持修改容器元素。而for_each ()对函数对象的要求是不能修改容器元素的值。当然范围for也可用在数组上,其实是为了STL所设计的。
for (auto &x : book1)
inflate(x);
for (auto x : book1)
showBook(x);
void inflate(Book & b)
{
b.id++;
}
random_shuffle,两个参数,两个迭代器以表示容器元素区间,但这个函数比较特别地地方在于他要求容器元素可随机访问。
sort(),两个参数时,两个迭代器以表示所比较的容器元素区间,默认采取operator<()方法(这个方法可以是在元素的成员函数或是包含该元素类型的非成员函数),三个参数时,将显示的采用第三个函数对象来比较元素进而排序,排序选择何种算法是在STL函数内部所确定的,在使用sort函数时,我们需要告诉他如何确定两个元素对象的大小。sort函数算是就地算法,直接改变容器的值。同样要求容器元素可随机访问。
std::for_each(book1.begin(), book1.end(), showBook);
std::sort(book1.begin(), book1.end());
std::for_each(book1.begin(), book1.end(), showBook);
std::sort(book1.begin(), book1.end(), compare);
std::for_each(book1.begin(), book1.end(), showBook);
std::random_shuffle(book1.begin(), book1.end());
std::for_each(book1.begin(), book1.end(), showBook);
bool operator<(const Book & b1, const Book & b2)
{
if (b1.id < b2.id)
return true;
else if (b1.id == b2.id&&b1.name < b2.name)
return true;
else
return false;
}
bool compare(const Book & b1, const Book & b2)
{
if (b1.id < b2.id)
return true;
else
return false;
}
泛型编程:
泛型编程是编写独立于数据类型的代码。对于通用算法来说,模板使得可以实现独立于容器所存储的数据类型,而迭代器使得可以独立于容器类型(数据结构)。模板可迭代器是泛型编程的重要组成部分。
迭代器:指向数据结构中所存储的元素。比如,数据结构是数组,那么迭代器的数据成员指向的就是某一个类型的空间。若数据结构是链表,那么迭代器指向的可能是一个结构体空间(包含某一类型的值和指向下一个结构体空间的指针)。
为何需要迭代器?
以终为始!目标是使得STL函数可适用于任何类型的容器对象,书中给出了两个例子,分别是数据结构为数组和链表的,不同的数据结构,但数据类型都是double,针对这两种结构下的数据,编写了查找特定元素值的函数,其实这个函数的思路是一致的,匹配容器里的元素值,相同则返回该元素的存储位置,但是由于数据结构的不同,使得两个方法存在差异,现在我们就要想办法使得针对不同的数据结构算法却是一样的:
能否在容器类中嵌套一个类(即迭代器),利用这个迭代器类(封装了与底层数据结构有关的获取元素的操作、根据底层数据结构遍历元素的操作)能够获取容器的元素,这个迭代器类的成员就是指向数据结构元素的指针,成员方法有*(数据结构中实际存储的值)、++(获取数据结构中下一个元素的地址),通过这个类,进而重新设计模板容器类,使其增加begin()、end(),其返回类型为迭代器,设置超尾元素,为了达到通用STL函数的目标,我们需要重新设计容器模板类,使其包含迭代器对象、和一些固定的方法,如此一来,在STL函数中,我们便可利用所有容器类都含有的迭代器,对该迭代器进行操作(此时根据不同的数据结构使用不同的实现方法),最终达到对容器元素的操作。
虽然我们可以直接利用迭代器,但是好的编程风格是能不用就不用,如利用for_each()或是for()来代替使用迭代器访问容器元素的操作。
能看出来!是根据算法的特性设计迭代器和容器!不同算法对迭代器的要求也是不同的,因此可将迭代器类型分为5大类,根据算法的需求选择合适的迭代器,同样对于模板容器类的设计,为了使其满足所有需要的算法对迭代器的要求,选用能匹配到所有需要的算法对迭代器的最高要求。
迭代器类型:输入、输出、正向、双向、随机。
迭代器通用特性:==、!=、*
输入迭代器:
被程序用来读取容器中的元素,通过解除引用操作无法写入容器元素,只能读出。单向,不可倒退。
输出迭代器:
向容器中写入元素值,通过解除引用操作无法读出容器元素,只能修改。单向,不可倒退。
正向迭代器:
可以根据需要使得容器元素可读、可写。
双向迭代器:比如reverse函数需要交换容器元素的次序。
除了++、还支持–
随机迭代器:
能使得算法支持对于任意迭代器元素的访问。迭代器内容更为丰富!
不同迭代器所支持的操作的比较!这些操作都是在迭代器类中所定义的。而不同的容器类模板可选用不同的迭代器。如果一个算法支持的迭代器类型为输入迭代器,那么具有随机迭代器类型的容器类自然也可以使用该算法!
指针是迭代器的一种实现,满足迭代器的要求,比如int *p,指针类型已经实现了++、*、[]等运算,因此这是随机迭代器的一个模型。STL函数将迭代器作为接口,为此数组(C++实现了超尾元素的设定)也可使用STL方法
STL算法使用的接口是迭代器,书中结合算法copy()为我们提供了几个常见的预定义迭代器:
copy()第一、二个参数是输入迭代器、第三个参数是输出迭代器。从这里还可以看出,算法的优点在于不限制容器的类型,只要容器提供了满足参数的迭代类即可,因此可以把数组复制到vector对象中。copy函数假设被赋值容器空间足够大,且会覆盖原来容器元素值,不能根据发送的容器元素实际大小调整被赋值容器的大小。
istream_iterator、ostream_iterator能使输入流、输出流使用STL函数!不然STL的只能用于模板容器类,有点受限啊,感觉有了输入输出流参数的STL函数更加完整了!
ostream_iterator输出流迭代器模板:
是输出迭代器类型的模型,不是定义在具体的容器类模板中,可以将其它接口转化为STL接口,有点类似于适配器,通常是将STL接口定义在容器类模板中,但我们现希望用于输出对象类型不是容器类,因此需要这样一个适配器,告诉编译器输出的目标是什么,同时得定义一个输出流迭代器接口,使其满足算法的要求。
istream_iterator输入流迭代器模板:
用于STL函数的数据并不一定来自容器,而是输入流,为此,定义了ostream_iterator迭代器,进行接口的转化,使得输入流中的数据使用STL函数。
reverse_iterato反向迭代器:
在容器类模板中声明,要显示使用这个迭代器需要显示给出具体的容器类!
反向迭代器的一种模型,为什么要有他呢?有了正向迭代器不就行了,从指向超尾元素的指针对其执行–不就是对反向迭代器执行++。但是对于同一种算法来说,就要重载更麻烦。比如copy(),内部实现了正序的复制,但我们希望以逆序来复制,此时难道要重新设计copy()吗?我们可以将copy()函数的迭代器参数改为相较于输入迭代器更高级的反向迭代器,这样一来,我们可以传递给copy函数反向迭代器,而不改变copy函数内部实现,此时对迭代器++的操作对于一个反向迭代器来说就是–,这样使得copy函数根据所传递的迭代器类型就能实现正序或是逆序的复制,神了!迭代器太牛!牛在封装了底层的数据结构,不同的迭代器定义了不同的操作。
对于使用反向迭代器的容器类来说,设计了rbegin()rend()方法返回容器的超尾元素和容器的第一个元素。
反向迭代器的解除引用操作的实现与正向相比,稍有不同!先对指针–,再取出值。
int a[] = { 3,1,2,4,5,6,7 };
std::vector<int >b(7);//人生一大错误,没有分配空间就直接复制,想成push_back方法了。。。可以不分配内存,来一个元素给一个空间再复制。
copy(a, a + 7, b.begin());
copy(b.begin(), b.end(), std::ostream_iterator<int, char>(std::cout, " "));
copy(b.rbegin(), b.rend(), std::ostream_iterator<int, char>(std::cout, " "));
std::vector<int>::reverse_iterator c;
for (c = b.rbegin(); c != b.rend(); c++)
std::cout << *c << " ";
依旧是STL函数copy(),我们知道copy()函数是假设分配了内存空间,不能动态变化,为此我们希望能够支持内存空间的动态变化,copy()函数内部实现无法改变了,只能从迭代器出发想办法。因此提出了插入迭代器。其构造函数形如:必须要为其指明容器类型和容器标识符。为什么要指明容器类型呢?因为有三种插入迭代器,但不是所有的容器类型都可以建立这些插入迭代器,比如vector就不能创建front_insert_iterator的迭代器。此外,创建插入迭代器要求容器类是包含push_back()方法的,否则也无法实现内存动态增长。迭代器类通过这样的构造函数可以调用容器类的push_back()方法,进而将copy()函数从简单的复制数据函数转为插入数据函数。
具体来说,插入迭代器是输出迭代器,有三种类型:back_insert_iterator、front_inset_iterator、insert_iterator。分别是插入在容器末尾、前端、指定位置。
总结一下这些预定义的迭代器,对于一个内部实现好的STL函数copy来说呢,只要改变迭代器的类型,同一个函数就能为我们做不同的事情,这是因为迭代器内部已经封装了一些方法,这使得具体的转化时由迭代器来做的,故而copy函数内部实现是一样的。所以迭代器完全丰富了STL的功能!
int a[] = { 3,1,2,4,5,6,7 };
int c[] = { 2,2,3,3,4,4 };
std::vector<int >b(7);//人生一大错误,没有分配空间就直接复制,想成push_back方法了。。。可以不分配内存,来一个元素给一个空间再复制。
copy(a, a + 7, b.begin());
copy(c, c + 6, std::back_insert_iterator < std::vector<int>>(b));
for_each(b.begin(), b.end(), show);
copy(a, a + 3, std::insert_iterator<std::vector<int>>(b, b.begin()));
for (auto x : b)
show(x);
容器种类:
容器是存储其他类型对象的对象!要求存储类型必须是一样的,当容器过期时,其存储的对象也自动过期。要求存储的对象支持可赋值和复制构造。(一般都能满足,除非将赋值运算符和复制构造函数声明为私有成员)
所有容器设计时需要满足的方法:
这里的时间复杂度体现了容器长度对操作的影响。
在基本的容器基础上,添加新的要求:序列!(元素具有确定的顺序,在进行操作时,不会影响其余元素的顺序),迭代器至少是正向迭代器,容器满足存在第一个元素,最后一个元素,除了第一个和最后一个元素外,其余元素前后只有一个元素。由于元素具有特定的位置,因此可支持指定的元素插入到特定的位置,删除特定区间的元素等操作,构造函数也更加多样性!
那么STL所实现的序列容器有哪些呢?vector 、list、deque、priority_deque、stack、queue、forward_list。这些容器模型除了支持上面的基本序列操作之外,根据自身操作的和实现的要求,分别实现了下述方法:
这里写出的是执行方法时间复杂度为固定时间。为此,对于push_front ()vector没有列入;对于a[n]操作来说,要求容器是可随机访问的,为此list容器没列入。且对于a.at(n) 和 a[n]方法来说,区别在于a.at(n)检查了是否越界,会引发out_of_range异常。
(1)vector容器
动态管理内存,自动改变容器长度,支持对元素的随机访问。处理满足序列概念外,还满足可反转的容器概念(实现了rbegin()和rend()),作为最简单的序列容器模型,实现各种操作的效率是最高的,因此,除了需要满足特殊的需要,否则最好都用vector容器来存储对象!
(2)deque容器
double-end-queue,实现与vector 类似,支持随机访问,优点在于在容器的头部插入和删除的时间复杂度是固定的,因此对于常需要在容器头进行插入和删除,最好使用deque容器模板。
(3)list容器
实现了双向链表,对于在任意位置的插入和删除操作时间复杂度都是固定的,因此,对于常需要在容器中间进行插入删除,可使用list容器,但它不支持随机访问。也是可反转容器。删除操作(容器内部任意位置),若是发生在vector上,当前迭代器指向的元素值会发生变化的,而发生在list上,迭代器指向元素值不变。
sort()、unique()、merge()函数另一重载函数接受一个函数对象,用于指示如何比较list元素大小。
#include "stdafx.h"
#include<list>
#include<iostream>
#include<algorithm>
void show(int x) {
std::cout << x << " ";
}
int main()
{
std::list <int >one(6, 2);
int a[] = { 6,6,1,2,4,6,2,4,2 };
std::list <int> b;
b.insert(b.begin(), a, a + 9);
std::list <int> c(b);
one.insert(one.end(), b.begin(), b.end());
std::cout << "list one:";
for_each(one.begin(), one.end(), show);
std::cout << " list b:";
for_each(b.begin(), b.end(), show);
std::cout << "list c:";
for_each(c.begin(), c.end(), show);
std::cout << "list one:";
one.remove(6);
for_each(one.begin(), one.end(), show);
std::cout << "list one:";
one.splice(one.end(), b);
for_each(one.begin(), one.end(), show);
std::cout << "list b:";
for_each(b.begin(), b.end(), show);
std::cout << "list b:";
b.unique();//忘记了splice之后 b为空啊!!!!
for_each(b.begin(), b.end(), show);
std::cout << "list b:";
b.sort();
for_each(b.begin(), b.end(), show);;
c.sort();
b.merge(c);
std::cout << "list b:";
for_each(b.begin(), b.end(), show);
std::cout << "list c:";
for_each(c.begin(), c.end(), show);
std::cin.get();
return 0;
}
(4)forward_list
单链表,不可反转容器。
(5)queue
适配器类,底层是deque,为了实现队列进而限制序列容器上的部分操作,比如不可随机访问,只能从头出、尾入。
(6)priority_queue
在queue的基础上,保证对头元素值为最大。底层是vector。
(8)stack
底层是vector,类似隐藏了部分容器类的操作,使得满足栈的要求。
(9)array类
并不是STL容器模板类,因为其大小固定,不支持动态变化,但由于其定义了[]、at()成员函数,因此适用于部分STL函数。
在基本容器概念上的另一个改进是关联容器。将键和值所关联,用键来查找值。关联容器采取了特定的方法来确定插入元素的位置,因此在插入元素时不可指定位置。STL提供了四种关联容器:set、multiset、map、multimap。前两种吧,键与值属于同一类型,后两种键和值类型不相同。带不带multi意味着一个键所对应值的个数,有multi的代表一个键有多个值与之对应。
set关联容器:
可排序、可反转、值唯一。STL为集合类容器提供了非成员函数的求交、求并、求差运算。函数接口是迭代器。对于集合容器来说,begin()和end()方法返回的是常量迭代器(将键视为常量),因此不能用作输出迭代器。为此可使用插入迭代器将常量迭代器转化为输出迭代器。
lower_bound()、upper_bound(),将键作为参数传递给函数,在set中找到第一个不小于键的值和第一个大于键的值的迭代器。
对于关联容器来说,insert()指出插入内容或是迭代器位置。
using std::cout;
std::string a[] = { "one","two","three" ,"one"};
std::string b[] = { "one","three","five" };
std::set<std::string> one(a,a+4);
std::set<std::string> two;
std::set<std::string>three;
two.insert(b,b+3);
cout << "set one is:";
copy(one.begin(), one.end(), std::ostream_iterator<std::string, char>(std::cout, " "));
std::set_union(one.begin(), one.end(), two.begin(), two.end(), std::ostream_iterator<std::string, char>(std::cout, " "));
std::set_intersection(one.begin(), one.end(), two.begin(), two.end(), std::insert_iterator<std::set<std::string>>(three, three.begin()));
for (auto x : three)
cout << x << " ";
multimap容器:
键和值类型不一样,为了表示键和值的关联性,STL提供了模板类pair存储关联性数据。pair类提供了first和second两个成员来访问值。灵活的使用pair,比如之后的例子使用了pair存储了两个头尾迭代器。(multimap存数据键值相同的存一起,equal_range方法的由来)
由于multimap的键和值的类型不同,因此声明时要显示给出具体的类型,要知道存的时候以pair类对象存储的,可以说multimap有多个pair对象组成,begin()和end()得到也是指向这些pair的迭代器,为了取出键和值,还需要进一步使用first和end:
multimap类提供了count()、lower_bound()、upper_bound()、equal_range()方法可获取存储数据的相关信息。
typedef std::multimap <int, std::string> Map;
typedef std::pair<int, std::string> Pair;
Map a;
Pair one(1, "lxy");
Pair two(2, "lxx");
Pair three(1, "llxy");
a.insert(one);
a.insert(two);
a.insert(three);
Map::iterator it;
for (auto x : a)
cout << x.first << " " << x.second << std::endl;
Map b(a.begin(),a.end());
cout<<b.count(1)<<std::endl;
std::pair <Map::iterator, Map::iterator> range = b.equal_range(1);
for (it = range.first; it != range.second; it++)
cout << (*it).second << std::endl;
函数对象:
包括:函数名、指向函数的指针、重载了()运算符的类。STL的一些非成员函数、成员函数包含了函数对象。对于重载了()的类来说,STL函数内部实现会通过该对象调用()函数,因此,我们在设计函数对象时,把需要进行的操作定义在()方法内。
template<class T>
class Func
{
T val;
public:
Func(T v):val(v){}
bool operator()(T x)
{
return x > val;
}
};
int main()
{
using std::cout;
std::list <int> a = { 1,2,3,4,6 };
for (auto x : a)
cout << x << " ";
cout << std::endl;
a.remove_if(Func<int>(3));
for (auto x : a)
cout << x << " ";
std::cin.get();
return 0;
}
函数对象时单个参数还是两个参数,STL函数的参数个数明显不一样:
预定义函数对象:
头文件functional中定义了多个模板类函数对象:
使用这些模板类的函数对象,自己就不用编写函数或是函数对象类了。
函数适配器类作用:二元函数转化为一元函数。前提是函数适配器可以使用函数对象并知道这些函数对象的参数类型、返回值类型。具体来说,函数适配器分为两个类型:bind1st和bind2st。
bind1st类:能够将指定的函数对象的第一个参数用指定值替代。
bind2st类:能够将指定的函数对象的第二个参数用指定值替代。
适配器再去调用指定的函数。适配器也是一个函数对象。
std::list <int> a = { 1,2,3,4,6 };
std::list<int > b = { 2,3,4,6,7 };
std::vector<int >c;
std::transform(a.begin(), a.end(), b.begin(), std::ostream_iterator<int, char>(cout, " "), std::plus<int>());
cout << std::endl;
std::transform(a.begin(), a.end(), std::insert_iterator<std::vector<int>>(c, c.begin()), std::bind1st(std::multiplies<int>(), 2));
for (auto x : c)
cout << x << " ";
算法:
非成员的STL函数,通过使用迭代器打破了各种不同容器类型间的隔阂,比如就copy函数而言,使得容器的复制操作不局限在同种容器类型间,可以是各种类型间。真正的复制操作就交由不同容器类的迭代器来实现!
以操作类型划分:
不修改序列算法
修改序列算法(值、顺序)
排序
数字运算:比如对给定区间的元素做累加等,定义在numeric头文件中
以是否修改原容器的值划分:
就地算法
复制算法
一些STL函数包含就地和复制两个版本的!
对于复制算法,STL函数规定返回类型为指向被复制对象超尾元素的迭代器。
string类与STL函数的关系,虽然string不是容器类,但由于string类设计了begin()、end()、rbegin()等方法,因此也可以用STL函数。所以说数组、字符串string虽然不是容器类,但依旧可使用STL函数。
#include "stdafx.h"
#include<string>
#include<algorithm>
#include<iostream>
int main()
{
std::string s;
using std::cout;
cout << "your string:";
std::cin >> s;
sort(s.begin(), s.end());
while (std::next_permutation(s.begin(),s.end()))
{
cout << s << std::endl;
}
// std::cout << std::endl;
std::cin.get();
std::cin.get();
return 0;
}
容器方法和通用函数如何选取?
针对特定容器实现的方法性能优于通用函数!特定容器方法可以实现内存管理,比如,对于list容器,同时包含remove()方法和非成员方法remove()方法,成员remove()可以释放多余空间,而非成员remove不会释放多余空间,只是不再用这些空间了。
//输入许多单词(存入到vector) 记录每个单词出现次数(map)
std:: list <int> a = { 1,2,3,5,6,6,5,5 };
std::list <int> b(a);
a.remove(5);
copy(a.begin(), a.end(), std::ostream_iterator<int, char>(std::cout, " "));
std::cout << std::endl;
std::list<int>::iterator it=std::remove(b.begin(), b.end(), 5);
copy(b.begin(), b.end(), std::ostream_iterator<int, char>(std::cout, " "));
std::cout << std::endl;
copy(it, b.end(), std::ostream_iterator<int, char>(std::cout, " "));
//std::remove(it,b.end())
std::cout << std::endl;
b.erase(it, b.end());
copy(b.begin(), b.end(), std::ostream_iterator<int, char>(std::cout, " "));
#include "stdafx.h"
#include<string>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<iostream>
#include<iterator>
using namespace std;
char tolow(char c)
{
return tolower(c);
}
string & Tolower(string & c)
{
transform(c.begin(), c.end(),c.begin(), tolow);
return c;
}
void show(string & c) {
std::cout << c << std::endl;
}
int main()
{
vector<string> words;
string s;
cout << "input string or quit:";
//cin >> s;
getline(cin, s);
while (s != "quit")
{
words.push_back(s);
cout << "input string or quit:";
//cin >> s;
getline(cin, s);
}
for (auto x : words)
show(x);
set<string> onlywords;
transform(words.begin(), words.end(), insert_iterator<set<string>>(onlywords, onlywords.begin()), Tolower);
for (auto x : onlywords)
show(x);
map<string, int> m;
set<string> ::iterator it;
for (it = onlywords.begin();it != onlywords.end();it++)
m[*it] = count(words.begin(), words.end(), *it);
for (auto it = m.begin(); it != m.end(); it++)
cout << (*it).first << " " << (*it).second << endl;
cin.get();
return 0;
}
其他库:
之前我们一直在讨论容器模板类,那么除了STL还有一些其他的类库,比如:valarray、array,他们也是数组模板,但是关注点不同,第一个在于实现数值元素,但长度固定,且使用数值运算的方法更简便,定义了begin(valarray对象)函数这使得可使用STL非成员函数。第二个是为了代替内置数组而实现的,接口更安全,但是与vector相比,其长度固定,不支持push_back。但是array实现了begin()、end()因此是适用于STL非成员函数。