【String类和标准模板库】

1.string类

2.智能指针模板类

3.标准模板库

4.泛型编程

5.函数对象

6.算法

7.其他库


1.string类

string类是由头文件string支持的,要使用类,关键要知道它的公有接口。

1.1构造字符串

【String类和标准模板库】_第1张图片

#include
#include

using std::string;
using std::cout;
using std::endl;
int main()
{
	string one("Lottery Winner!");
	string two(20, '-');
	string three(one);
	string four;
	const char* s = "All's well that ends well";
	char sc[] = "All's well that ends well";
	string five(s, 20);
	string six(sc + 5, sc + 10);
	string six_2(&five[5], &five[10]);
	string seven = { 'L','O','P','T' };
	cout << one << endl << two << endl
		<< three << endl << four << endl << five << endl
		<< six << endl << six_2 << endl << seven << endl;
	one[0] = 'P';
	three = one + two;
	cout << one << endl << three << endl;
	return 0;
}

 注:数组名相当于指针,所以:

string six(sc + 5, sc + 10);

而对象名不是指针,所以要下标索引加取地址符号:

	string six_2(&five[5], &five[10]);

移动构造函数再第18章讲解,最后一个构造函数使得能够将列表初始化方法用于string类,本章后边更深刻讨论initializer_list。 

1.2string类输入

1.2.1C风格

3种输入方式:

char info[100];
	cin >> info;
	cin.getline(info, 100);
	cin.get(info, 100);

1.2.2string对象

2种方式:

string lname;
	cin >> lname;
	getline(cin, lname);

string具有自动调整大小的功能,不需要指定长度,并且string类,所以cin作为参数。

string函数停止读入条件:

1.到达string对象最大允许长度:(string::npos或可供分配的内存字节数中较小的一个) 

2.到达文件末尾

3.遇到分界字符(默认为\n)

对于>>输入字符,程序在读到空白字符(空格 换行 制表)就会停止。

#include
#include
#include
#include
using namespace std;
int main()
{
	ifstream fin;
	fin.open("C:\\Users\\HXY\\Desktop\\abc.txt");
	string item;
	int count = 0;
	getline(fin, item, ':');
	while (fin)
	{
		++count;
		cout << count << ":" << item << endl;
		getline(fin, item, ':');
	}
	cout << "Done!";
	fin.close();
	return 0;
}

当指定了分界字符后,换行符就成了普通字符。

【String类和标准模板库】_第2张图片

第4行第一个是换行符。

1.3使用字符串

1.字符串可以用6个关系运算符直接比较;

2.类方法 size()和length()返回字符串长度;

3. = += 赋值和附加字符串;

4.查找方法:

【String类和标准模板库】_第3张图片现在不返回std::npos,而是返回-1.(2022年)

 rfind()方法和find()类似只不过查找最后一次出现的位置;
find_first_of()查找参数中任何一个字符第一次出现的位置;
find_last_of()查找参数中任何一个字符最后一次出现的位置;
find_first_not_of()查找不在参数中的任何一个字符第一次出现的位置;
find_last_not_of()查找不在参数中的任何一个字符最后一次出现的位置;

#include
#include
using namespace std;
int main()
{
	string s1("hark");
	string s2("cobra");
	string s3 = "shark";
	int i = s3.find(s1, 0);
	int j = s2.find('b', 0);
	int j2 = s2.find('j', 0);
	int k = s3.find_first_of(s2);
	cout << i << endl
		<< j << endl
		<< j2 << endl
		<< k << endl;
	return 0;
}

【String类和标准模板库】_第4张图片

1.4string其他功能

方法capacity()返回当前分配给字符串的内存块大小,reserve()请求内存块的最小长度。

#include
#include
using namespace std;
int main()
{
	string s1("hark");
	string s2("cobra");
	string s3 = "shark";
	cout << "size:\n";
	cout << s1.size() << endl << s2.size() << endl << s3.size() << endl;
	cout << "capacity:\n";
	cout << s1.capacity() << endl << s2.capacity() << endl << s2.capacity() << endl;
	s2.reserve(70);
	cout << s2.capacity();

	return 0;
}

【String类和标准模板库】_第5张图片

c_str()方法返回一个指向C风格字符串的指针,内容与对象相同。

1.5字符串种类

模板basic_string有4个具体化,除了使用char类型,还可以使用wchar_t,char16_t,char32_t.

2.智能指针模板类

2.1智能指针的引入

智能指针是行为类似于指针,但是是一个类对象,可用于帮助管理动态内存分配的智能指针模板。

智能指针模板的存在是为了在存在动态分配函数终止(正常或异常),使得动态分配的内存清除掉,需要一个析构函数,所以智能指针就出现了。

2.2使用智能指针

以下三个智能指针模板(auto_ptr、unique_prt、shared_ptr)位于名称空间std中,定义在头文件memory中,使用通常的模板语法来实例化所需类型的指针,我们便不需要记住稍后释放这些内存。

auto_ptr pd(new double);//使用指向double的内存地址来初始化智能指针对象
auto_ptr ps(new string);
#include
#include
#include
using namespace std;
class report
{
private:
	string str;
public:
	report(const string s) :str(s) { cout << "Object created.\n"; };
	~report(){ cout << "Object deleted.\n"; }
	void comment() { cout << str; }

};
int main()
{
	{
		auto_ptr p1(new report("using auto_ptr"));
		p1->comment();
	}//离开代码块,指针将过期
	
	{
		unique_ptr p2(new report("using unique_ptr"));
		p2->comment();
	}
	
	{
		shared_ptr p3(new report("using shared_ptr"));
		p3->comment();
	}
	
	return 0;
}

 智能指针很多地方都类似于常规指针,解引用、->访问结构成员,赋值给相同类型的常规指针(显式类型转换),赋值给同类型的智能指针。无论如何,都不能将智能指针用于非动态存储的对象或变量。

显式类型转换:

shared_ptr p1;
	double* p2 = new double;
	p1 = p2;   不允许
	p1 = shared_ptr(p2);  这才正确
	shared_ptr pshared = p2;  不允许
	shared_ptr pshared(p2);  这才正确(复制构造函数)
	return 0;

2.3有关智能指针的注意事项

智能指针在复制的时候,如果执行浅复制,则会导致内存释放两次,严重错误。

shared_ptr创建智能更高的指针,跟踪引用特定对象的智能指针数,仅当最后一个指针过期时,才会调用析构函数;

auto_ptr 和 unique_ptr会建立所有权概念,对于特定的对象,只能有一个指针指向它,如果进行赋值操作,则所有权会转移,auto_ptr被转移的所有权的指针指向空指针,不安全;而unique_ptr不允许赋值操作,除非存在一个临时对象,临时对象的所有权转让给新对象,老对象被销毁,这时赋值是可以用的。

unique_ptr可用于数组变体。new  delete、new[]   delete[]要配合使用,unique_ptr可以满足这种情况。

2.4选择智能指针

如果程序要使用多个指向同一个对象的指针,用shared_ptr;否则用unique_ptr(auto_ptr被C++11抛弃)。

3.标准模板库(STL)

STL提供了一组表示容器(存储同类型若干值)、迭代器(遍历容器的广义指针)、函数对象(类似于函数的对象)和算法(完成特定任务)的模板。STL不是面对对象的编程,而是泛型编程。

3.1模板类vector

vector在头文件vector中定义里模板,存储了一组可以随机访问的值,使用索引来直接访问元素。要创建vector模板对象,通常指出要使用的类型,可以使用动态内存分配,可以使用变量来指出需要多少矢量。

	vector p1(5);
	int n;
	cin >> n;
	vector p2(n);

默认使用new和delete来动态分配内存,可以选择分配器(STL模板容器接受可选的模板参数)。

#include
#include
#include
using namespace std;

int main()
{
	vector ratings(5);
	vector titles(5);
	for (int i = 0; i < 5; i++)
	{
		cout << "Enter title #" << i + 1 << ":";
		getline(cin, titles[i]);
		cout << "Enter your ratings:";
		cin >> ratings[i];
		cin.get();
	}
	for (int i = 0; i < 5; i++)
	{
		cout << titles[i] << endl;
		cout << "Ratings:" << ratings[i] << endl;
	}
}

3.2可对矢量执行的操作

所有的STL容器都提供了一些基本的方法:

size()——返回容器元素数目

swap()——交换两个容器的内容 

begin()——返回一个指向容器中第一个元素的迭代器

end()——返回一个表示超过容器尾的迭代器

迭代器:广义指针,可以对其使用解引用和递增,迭代器的存在让STL存在能够为各种不同的容器类提供统一的接口。每个容器都定义了一个合适的迭代器,该迭代器的类型是一个名为iterator的typedef,作用域为整个类。

vector::iterator pd;
	pd = ratings.begin();
	pd[2] = 23;
	pd++;

auto存在让一切变得简单
	auto p = ratings.begin();

所有容器都有上述方法,vector包含一些只有某些STL容器才有的方法。

push.back()将元素添加到矢量的末尾,在编写或运行程序时,无需了解元素数目。

erase()删除矢量中给定区间的元素,接受两个迭代器参数。

insert()在矢量中插入元素;,接受三个迭代器参数,第一个为插入的位置。

#include
#include
#include
using namespace std;
struct Review
{
	string title;
	int rating;
};
bool FillReview(Review& rr);
void ShowReview(const Review& rr);
int main()
{
	vector books;
	Review temp;
	while (FillReview(temp))
	{
		books.push_back(temp);       //添加元素
	}
	int num = books.size(); //返回长度

	auto  pd = books.begin();   //指针
	for (pd; pd != books.end();pd++) 
	{
		ShowReview(*pd);
	}
	vector Oldbooks(books);  //赋值操作
	if (num > 3)
	{
		books.erase(books.begin() + 1, books.begin() + 3); //位置[1]开始删除 删到位置[2],位置[3]保留
			cout << "After earse:\n";
			for (pd = books.begin(); pd != books.end(); pd++)
			{
				ShowReview(*pd);
			}
			books.insert(books.begin(), Oldbooks.begin() + 1, Oldbooks.begin() + 2);//第一个元素是插入位置,后边是插入区间,依然是往后一个,[2]没插入
			cout << "After insert:\n";
		for (pd = books.begin(); pd != books.end(); pd++)
		{
			ShowReview(*pd);
		}
	}
	books.swap(Oldbooks);  //整个对象交换
	for (pd = books.begin(); pd != books.end(); pd++)
	{
		ShowReview(*pd);
	}
}

bool FillReview(Review& rr)
{
	cout << "Enter book title ";
	getline(cin, rr.title);
	if(rr.title=="quit")
		return false;
	cout << "Enter your ratings:";
	cin >> rr.rating;
	if (!cin)
		return false;
	while (cin.get() != '\n')
		continue;
	return true;
}
void ShowReview(const Review& rr)
{
	cout << rr.title << endl;
	cout << "Ratings:" << rr.rating << endl;
}

3.3对矢量可执行的其他操作——最后一个都是多一个

对数组除了上述操作外,通常还需要搜索、排毒、随机排序等,STL定义了非成员函数来执行这些操作,特定类算法要比通用算法高,但是非成员函数可以接受类型不同的容器的内容。

for_each()——可用于很多容器,用来遍历,不能改变容器元素的值,可代替for循环;

for_each(books.begin(),books.end(),Showreview)

random_shuffle()——随机排列元素,接受两个参数;

sort()——排序,可接受三个类型(要求容器有<的重载函数 Bool类型)。

#include
#include
#include
#include
using namespace std;
struct Review
{
	string title;
	int rating;
};
bool FillReview(Review& rr);
void ShowReview(const Review& rr);
bool WorseThan(const Review& r1, const Review& r2);
bool operator<(const Review& r1, const Review& r2);
int main()
{
	vector books;
	Review temp;
	while (FillReview(temp))
	{
		books.push_back(temp);       //添加元素
	}
	int num = books.size(); //返回长度
	if (num > 0)
	{
		for_each(books.begin(), books.end(), ShowReview);
		sort(books.begin(), books.end());   //默认<排序
        for_each(books.begin(), books.end(), ShowReview);

		sort(books.begin(), books.end(),WorseThan); //采用worseThan排序
		for_each(books.begin(), books.end(), ShowReview);


		random_shuffle(books.begin(), books.end());  //随机排序
		for_each(books.begin(), books.end(), ShowReview);
	}
	cout << "Done.";
	return 0;
	
}

bool FillReview(Review& rr)
{
	cout << "Enter book title ";
	getline(cin, rr.title);
	if (rr.title == "quit")
		return false;
	cout << "Enter your ratings:";
	cin >> rr.rating;
	if (!cin)
		return false;
	while (cin.get() != '\n')
		continue;
	return true;
}
void ShowReview(const Review& rr)
{
	cout << rr.title << endl;
	cout << "Ratings:" << rr.rating << endl;
}
bool operator<(const Review& r1, const Review& r2)
{
	if (r1.title < r2.title)
		return true;
	else if (r1.title == r2.title && r1.rating < r2.rating)
		return true;
	else
		return false;
}

bool WorseThan(const Review& r1, const Review& r2)
{
	if (r1.rating < r2.rating)
		return true;
	else
		return false;
}

3.4基于范围的for循环(C++11)

基于for循环是为用于STL而设计的。

for(type 变量名: 容器名);

for (auto x : books) ShowReview(x);

别忘了auto应用,如果采用变量名,for和for_each一样,不能改变容器内的元素;

如果采用变量的引用,for就可以改变变量的元素。

for (auto &x : books) InflateReview(x);

4.泛型编程

STL是一种泛型编程,关注算法,面向对象的编程关注的是编程的数据方面。C++中,完成通用程序的工具是模板,STL中,完成通用的工具是迭代器。

4.1为何使用迭代器

C++模板的存在使得算法独立于存储的数据,但是不独立于数据类型,也就是说用于数组的算法不能用于链表,而迭代器存在就是要使得STL不仅独立于存储的数据,还要独立于数据类型。

每个容器类定义了相应的迭代器类型,不管迭代器实现的方式如何,迭代器都将提供所需的操作,如* ++等;其次,每个迭代器都有一个超尾标记,每个容器类都有begin()和end()方法。

使用容器的时候,不只需要知道迭代器怎么实现的,只需要知道容器有并且学会使用迭代器(iterator)即可。最好避免直接使用迭代器,尽可能使用STL函数。

vector::iterator pr = scores.begin();
vector::iterator pr = scores.begin();
auto pr = scores.begin();  //用auto简单且不易错

总结STL方法:1.处理容器的算法;2.定义满足算法需求的迭代器,并把要求加上容器设计上;3.基于算法的要求,设计基本迭代器的特征和容器特征。 

4.2迭代器类型

不同的算法对迭代器要求不同。STL定义了5种迭代器。

1.输入迭代器

输入迭代器可以访问容器内的值,不能修改容器内的值。支持++操作(访问数据)。此外,我们并不能保证输入迭代器第二次遍历容器时,顺序不变,且在迭代器递增后,也不能保证前一个值依然可以被接触引用。即输入迭代器是个单行的,可以递增,不能倒退,也不依赖以前的值。

2.输出迭代器

输出迭代器只能解引用让程序改变容器内的值,但是不能读取数据。输出迭代器也是单行的。

3.正向迭代器

正向迭代器只能使用++运算符来遍历容器,但是它总是按相同的顺序遍历一系列值,将正向迭代器递增后,仍然可以对前边迭代器值解引用(如果保存了它),并得到相同的值。正向迭代器既可以读取,也能修改数据。

4.双向迭代器

双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。

5.随机访问迭代器

随机访问迭代器具有双向迭代器所有特性,此外还增加了指针增加运算和关系运算符。

4.3迭代器层次结构

我们可以看待迭代器具有一个层次结构,根据特定迭代器类型编写的算法可以使用该种迭代器,也可以使用具有所需功能的任何其他迭代器。所以具有随机访问迭代器的容器可以使用为输入迭代器编写的算法。

但在编写编写算法时,要尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。迭代器的类型并不是确定的,只是一种概念描述。矢量迭代器时随机访问迭代器,它允许使用基于任何迭代器类型的算法。具有双向迭代器则不能使用基于随机访问迭代器算法,但可以使用基于要求较低的迭代器的算法。可以高适应低,低不能适应高。

4.4概念、改进和模型

STL有很多无法用C++语言表达的特性,因此在STL中,我们采用概念来描述这一系列要求。概念具有类似继承的关系(迭代器的层次结构),但不能将C++继承机制用于概念,实现可能完全不同,我们采用改进来表示这种概念上的继承;概念的具体实现被称为模型,例如,指向int的常规指针就是一个随机访问迭代器模型。

4.4.1将指针用作迭代器

迭代器是广义指针,而指针满足所有的迭代器要求。STL算法可以使用指针来对基于指针的非STL容器进行操作。由于指针是迭代器,所以可将STL算法用于常规数组,同样也可以用到自己设计的数组形式(提供合适的迭代器和超尾指示器即可)。

STL提供了一些预定义迭代器:

copy():

将数据从一个容器复制到另一个容器,这种算法是以迭代器方式实现的。前两个迭代器参数表示要复制的范围,最后一个参数表示将第一个元素复制到什么位置,会覆盖目标容器中已有的数据。

ostream_inerator:

表示输出流的迭代器,提供了ostream_inerator的模板,是一个适配器,可以将一些其他的接口转换为STL使用的接口,可以通过包含头文件iterator来声明和创建这种迭代器。

#include

ostream_iterator out_iter(cout, " ");

out_iter迭代器现在是一个接口,可以使用cout显式信息。第一个参数指出被发送给输出流的数据类型,第二个参数指出了输出流使用的字符类型,构造函数的第一个参数指出了要使用的输出流,最后一个参数是发送给输出流没想数据后显示的分隔符。

可以这样使用分隔符:

*out_iter++ = 15;

cout<<15<<" "
for_each(books.begin(), books.end(), out_iter);  //显式使用
for_each(books.begin(), books.end(),ostream_iterator(cout, " ") );  //隐式使用

istream_iterator:

iterator头文件还定义了istream_iterator模板,使istream输入做迭代器接口。

copy(istream_iterator(cin),istream_iterator(),dice.begin())

第一个模板参数指出要读取的数据类型,第二个参数指出输入流使用的字符类型。使用构造函数cin意味着使用由cin管理的输入流,省略构造函数表示输入失败,上边语句从输入流读取,直到文件结尾、类型不匹配或出现其他输入故障为止。 

4.4.2其他有用的迭代器

头文件iterator还提供了其他专用的预定义迭代器类型。

reverse_iterator——用于反向打印内容,rbegin()和rend(),迭代器递增操作将导致它被递减。

三种插入迭代器实际上完成的是复制的功能(依然用到copy函数)

back_insert_iterator——元素插到容器尾部,只能用在允许尾部快速插入的容器,vector满足。

front_insert_iterator——元素插到容器前端只能用在允许起始位置快速插入的容器queue满足,vector不满足

insert_iterator——元素插到指定位置,无限制要求。

这些迭代器将容器类型作为模板参数(寻找对应的push_back()的方法),将实际的容器标识符作为构造函数参数。要为名为dice的vector容器创建一个back_insert_iterator:

back_insert_iterator< vector > back_iter(dice);
#include
#include
#include
#include
#include
using namespace std;

void output(const string& s) { cout << s << " "; }
int main()
{
	string s1[4] = {"fine","fish","fashion","fate"};
	string s2[2] = {"busy","bats"};
	string s3[2] = {"singer","silly"};

	vector word(4);
	copy(s1, s1 + 4, word.begin());
	for_each(word.begin(), word.end(), output);
	cout << endl;

	back_insert_iterator> back_iter(word);
	copy(s2, s2 + 2, back_iter);
	for_each(word.begin(), word.end(), output);
	cout << endl;

	insert_iterator> front_iter(word,word.begin());//插入的字符串和插入的位置
	copy(s3, s3 + 2, front_iter);
	for_each(word.begin(), word.end(), output);
	cout << endl;

	return 0;
}

 为什么copy时不用word.begin()和word.end(),如果用这两个迭代器,无法实现内存自动变化,word可能没有足够的空间容纳字符,程序会由于内存违规而异常终止。

4.5容器种类

4.5.1容器概念

容器概念指定了所有STL容器类都必须满足的一系列要求。

容器时存储其他对象的对象,要求被存储的对象必须是同一种类型,被存储的对象必须是可复制构造和可赋值的(类定义在公有部分或友元部分声明复制构造函数和赋值运算符的重载)。容器过期时,存储在容器中的数据也会过期(如果数据是指针,指针指向的数据不一定会过期)。

基本容器不能保证其元素都按特定的顺序存储,也不能保证元素的顺序不变,对概念进行改进后,可以增加这样的保证。

一些基本容器特征
表达式 返回类型 说明 复杂度
X::iterator 指向T的迭代器类型 满足正向迭代器要求的任何迭代器 编译时间
X::value_type T T的类型 编译时间
X u; 创建名为u的空容器 固定
X(); 创建匿名空容器 固定
X u(a); 调用复制构造函数后 u ==a 线性
X u = a; 调用复制构造函数后 u ==a 线性
r = a; X& 调用赋值运算符后 r ==a 线性
(&a)->~X(); void 对容器中每一个元素应用析构函数 线性
a.begin() 迭代器 返回指向容器第一个元素的迭代器 固定
a.end(); 迭代器 返回超尾迭代器 固定
a.size(); 无符号整型 返回元素个数 固定
a.swap(b); void 交换a和b的内容 固定
a == b; 可转换为bool 长度且元素都相同,返回真 线性
a != b; 可转换为bool 作用同上相反 线性

如果复杂度为编译时间,操作将在编译时执行,固定复杂度意味着操作发生在运行阶段,线性复杂度意味着时间与元素数目成正比。

4.5.2C++11新增的容器要求

C++11新增的容器要求
表达式 返回类型 说明 复杂度
X u(rv); 调用移动构造函数后,u的值与rv的原始值相同 线性
X u = rv; 同上
a = rv; 调用移动赋值运算符后,u的值与rv的原始值相同 线性
a.cbegin(); 返回指向容器第一个元素的const迭代器 固定
a.cend(); 返回超尾const迭代器 固定

 复制构造和复制赋值以及移动构造和移动赋值之间的差别在于,复制操作保留原对象,移动操作可以修改原对象,还可能转让所有权,而不做任何赋值,如果源对象是临时的,移动操作的效率高于常规操作,具体18章介绍。

4.5.3序列

可以通过添加要求来改进基本容器概念。序列的概念增加了迭代器至少是正向迭代器这样的要求,保证了元素按特定顺序排列,序列还要求元素按严格的线性顺序排列,因此可以执行注入将值插入到特定的位置、删除特定区间等操作。

序列的要求
表达式 返回类型 说明
X a(n,t); 声明一个名为a的由n个t组成的序列
X(n,t); 创建一个由n个t组成的序列
X a(i,j) 声明一个名为a的序列,并将其初始化为区间[i,j)的内容
X(i,j) 声明一个匿名序列,并将其初始化为区间[i,j)的内容
a.insert(p,t) 迭代器 将t插到p的前面
a.insert(p,n,t) void 将n个t插到p的前面
a.insert(p,i,j) void 将区间[i,j)中的元素插到p的前面
a.erase(p) 迭代器 删除p指向的元素
a.erase(p,q) 迭代器 删除区间[p,q)中的元素
a.clear() void 删除所有元素

因为模板类deque,list,queue,priority_queue,stack和vector都是序列概念的模型,所以它们都支持上表的运算符。除此之外,6个模型中还可以使用其他操作,在允许的情况下,他们的复杂度为固定时间。 

序列的可选要求
表达式 返回类型 含义 容器
a.front() T& *a.begin() vector、list、deque
a.back() T& *--end() vector、list、deque
a.push_front(t) void a.insert(a.begin(),t) list、deque
a.push_back(t) void a.insert(a.end(),t) vector、list、deque
a.pop_front(t) void a.erase(a.begin()) list、deque
a.pop_back(t) void a.erase(--a.end()) vector、list、deque
a[n] T& *(a.begin()+n) vector、deque
a.at(t) T& *(a.begin()+n) vector、deque

说明:

1.a[n]和a.at(n)的区别在于,如果n落在容器的有效区间外,a.at(n)会执行边界检查,并引发out_of_range异常。

2.vector有些方法不可用,是因为可选要求复杂度要求为固定时间,vector大量移动数字不是固定时间,而是线性时间。 

接下来详细介绍这7种容器。

1.vector

该模板是在vector头文件中声明的,提供了对元素的随机访问,vector还是可翻转容器概念的模型,rbegin()和rend()这两个类方法便可对vector使用。vector是最简单的序列类型,除非其他类型的特殊优点能够很好的满足程序的要求,否则应该默认使用vector。

2.deque

该模型在deque头文件中声明,表示双端序列,如果多数操作发生在序列的起始和结尾处,应该考虑使用该模型,该模型也提供对元素的随机访问。

3.list

该模型在头文件list中声明,表示双向链表,vector强调通过随机访问进行快速访问,list强调元素快速插入和删除,list也是可翻转容器,但list不支持数组表示法和随机访问。在容器中插入或删除元素后,链表迭代器指向元素将不变(是链表,地址指向,当然不会变)。list模板类还包含了链表专用的成员函数,下表列出部分函数。

list成员函数
函数 说明
void merge(list& x) 将链表x与调用链表合并。两个链表必须已经排序。合并后的链表保存在调用链表中,x为空。时间复杂度为线性
void remove(const T & val) 链表中删除val的所有实例。复杂度为线性时间
void sort() 使用<运算符对链表进行排序,复杂度为NlogN
void splice(iterator pos,listx) 将链表x的内容插到pos的前边,x为空。复杂度为固定时间
void unique() 将连续的相同元素压缩为单个元素,复杂度为线性
#include
#include
#include
using namespace std;
void output(const int n) { cout << n << " "; }
int main()
{
	list one(5, 2);
	int stuff[5] = { 1,2,4,6,8 };
	list two;
	two.insert(two.begin(),stuff,stuff+5);
	int more[6] = { 6,4,2,3,4,5 };
	listthree(two);
	three.insert(three.end(), more, more + 6);

	cout << "List one:";
	for_each(one.begin(), one.end(), output);

	cout << endl << "List two:";
	for_each(two.begin(), two.end(), output);

	cout << endl << "List three:";
	for_each(three.begin(), three.end(), output);

	three.remove(2);
	cout << endl << "List three after remove 2:";
	for_each(three.begin(), three.end(), output);

	three.splice(three.begin(), one);
	cout << endl << "List three after splice one:";
	for_each(three.begin(), three.end(), output);
	cout << "List one:";
	for_each(one.begin(), one.end(), output);
	
	three.unique();
	cout << endl << "List three after unique:";
	for_each(three.begin(), three.end(), output);

	three.sort();
	cout << endl << "List three after sort:";
	for_each(three.begin(), three.end(), output);

	two.sort();
	three.merge(two);
	cout << endl << "List three after merge:";
	for_each(three.begin(), three.end(), output);
	return 0;
}

unique()只能将相邻的相同值压缩为单个值。不能将非成员函数sort()用于链表。

sort() merge() unique()方法还各自拥有接受另一个参数的版本,该参数用于指定比较元素的函数。remove() 方法也有接受另一个参数的版本,该参数用于指定用来确定是否删除元素的函数。 

4.forward_list(C++11)

它实现了单链表,只需要正向迭代器,不可反转的容器。

5.queue

该模型在头文件queue中声明,是一个适配器类,queue模板让底层类(默认为deque)展示典型的队列接口。queue模板限制比deque更多,不允许随机访问队列元素,不允许遍历队列。但可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。

6.priprity_queue

该模型在头文件queue中声明,是一个适配器类,它支持的操作与queue相同。两者主要的区别在于,在priority_queue中,最大的元素被移到队首,内部的区别在于,默认的底层是vector.可以修改用于确定哪个元素放到队首的比较方式,方法是提供一个可选的构造函数参数:

priority_queue pq1;

priority_queue pq2(greater);greater函数是一个预定义的函数对象。

7.stack

该模型在头文件stack中定义,是一个适配器类,他给底层类(默认情况为vector)提供典型的栈接口。栈接口不允许随机访问栈元素,不允许遍历栈,可以压入推到栈顶,从栈顶弹出元素,查看栈顶的值,检查元素数目和测试栈是否为空。

8.array

该模型在头文件array中声明,他并非STL容器,因为其长度是固定的,但定义了对它来说有意义的成员函数,如[] at(),可将很多标准STL算法用于array对象,如copy和for_each().

4.6关联容器

关联容器是对容器概念的另一个改进。关联容器将键和值关联在一起,并使用键来查找值。X::value::type指出存储在容器中的值的类型。对于关联容器来说,表达式X::key_type指出了键的类型。

关联容器优点在于提供了对元素的快速访问,也允许插入新元素,但不能指定插入位置,关联容器通常是使用树实现的。

STL提供了set、multiset、map、multimap。前两种在头文件set中定义,后两种在头文件map中定义。最简单的关联容器是set,其值类型与键相同,键是唯一的。multiset 类似于set,只是可能有多个值的键相同。在map中,值与键的类型不同,键是唯一的,每个键只对应一个值。multimap与map相似,只是一个键可以与多个值相关联。

这里只介绍使用set和multimap简单的例子。

4.6.1set示例

STL set键是唯一的,所以不能存储多个相同的值。与vector和List相似,set也使用模板参数来指定要存储的值类型:

set A;

第二个模板参数是可选的,用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板less<>。set也有一个将迭代器区间作为参数的构造函数,初始化后,键是唯一的,且集合被排序。STL提供了相关操作的算法。算法是通用函数,而不是方法,所有set对象自动满足使用这些算法的先决条件,因为它们是排过序的。

set_union()函数接受5个迭代器参数。前两个迭代器定义了第一个集合的区间,接下来的两个定义了第二个集合的区间,最后一个迭代器是输出迭代器,指出将结果复制到什么位置。假设要将结果放在C中,我们不能使用C.begin(),因为这是一个常量,不是输出迭代器,第二个原因是C.begin()使得C大小固定,无法存入数据,所以我们应该使用insert_iterator。set_intersection()和set_difference()分别查找交集和获得两个集合的差,接口与set_union相同。lower_bound()和upper_bound()将键作为参数返回一个得带起,该迭代器指向集合中第一个不小于键参数的成员和第一个大于键参数的成员。

#include
#include
#include
#include
#include
using namespace std;

int main()
{
	const int N = 6;
	string s1[N] = { "buffoon","thinkers","for","heavy","can","for" };
	string s2[N] = { "metal","any","food","elegant","deliver","for" };

	set A(s1, s1 + N);
	set B(s2, s2 + N);

	ostream_iterator out(cout, " ");
	cout << "Set A:";
	copy(A.begin(), A.end(), out);
	cout << endl;

	cout << "Set B:";
	copy(B.begin(), B.end(), out);
	cout << endl;

	cout << "Union of A and B:\n";
	set_union(A.begin(), A.end(), B.begin(), B.end(), out);
	cout << endl;

	cout << "Intersection of A and B:\n";
	set_intersection(A.begin(), A.end(), B.begin(), B.end(), out);
	cout << endl;

	cout << "Difference of A and B:\n";
	set_difference(A.begin(), A.end(), B.begin(), B.end(), out);
	cout << endl;


	set C;
	cout << "Set C:\n";
	set_union(A.begin(), A.end(), B.begin(), B.end(), insert_iterator>(C,C.begin()));
	copy(C.begin(), C.end(), out);
	cout << endl;

	string s3("grungy");
	C.insert(s3);   //插入数据
	cout << "set C after intersection:\n";
	copy(C.begin(), C.end(), out);
	cout << endl;

	cout << "Showing a range:\n";
	copy(C.lower_bound("ghost"), C.upper_bound("spook"), out);
	cout << endl;
	return 0;
}

【String类和标准模板库】_第6张图片

4.6.2multimap示例

与set相似,multimap也是可反转的、经过排序的关联容器,但是键和值类型不同,且同一个键可能与多个值相关联。

基本的multimap声明使用模板参数指定键的类型和存储的值的类型,下面声明创建一个对象,键的类型为int,存储的值类型为string,对象的值类型为pair(值与键对应,所以要结合为一对)。

multimap codes;

第三个模板参数是可选的,指出用于对键进行排序的比较函数或对象,默认情况使用less<>.

假设要存入数据,可以创建pair,再将它插入,也可以使用一条语句创建匿名pair对象将它插入,对于pair对象,可以使用first和second成员来访问其两个部分。成员函数count()接受键作为参数,并返回具有该键的元素数目。成员函数lower_bound()和upper_bound()将键作为参数,工作原理与处理set时相同。成员函数equal_range()用键作为参数,且返回两个迭代器,他们表示的区间与该键匹配。

#include
#include
#include
#include
using namespace std;

typedef int KeyType;
typedef std::pair Pair;
typedef std::multimap MapCode;

int main()
{
	MapCode  codes;

	codes.insert(Pair(415, "San Francisco"));
	codes.insert(Pair(510, "Oakland"));
	codes.insert(Pair(718, "Brooklyn"));
	codes.insert(Pair(415, "Staten Island"));
	codes.insert(Pair(510, "San Rafael"));
	codes.insert(Pair(510, "Berkeley"));

	cout << "Number of cities with area code 415:" << codes.count(415) << endl;
	cout << "Number of cities with area code 718:" << codes.count(718) << endl;
	cout << "Number of cities with area code 510:" << codes.count(510) << endl;

	cout << "Area Code City\n";
	MapCode::iterator it;
	for (it = codes.begin(); it != codes.end(); ++it)
		cout << " " << (*it).first << " " << (*it).second << endl;

	pair range = codes.equal_range(718);
	cout << "Cities with area code 718:\n";
	for (it = range.first; it != range.second; ++it)
		cout << " " << (*it).second  << endl;

	return 0;
}

【String类和标准模板库】_第7张图片

4.7无序关联容器

无序关联器是对容器概念的改进,也是将值与键关联起来,关联容器基于树结构的,无需关联器基于数据结构哈希表的,为了提高添加和删除元素的速度以及提高查找算法的效率。

5.函数对象

函数符时可以以函数方式与()结合使用的任意对象。这包括函数(模板,为了适配不同类型的输入)名、指向函数的指针和重载了()运算符的类对象。

5.1函数符的概念

生成器是不用参数就可以调用的函数符;

一元函数是用一个参数就可以调用的函数符;

二元函数是用两个参数可以调用的函数符;

返回bool值的一元函数是谓词;

返回bool值的二元函数是二元谓词。

对于一元函数,如果我们想传递两个参数,但是函数只能是一元函数怎么办?设计一个类,类构造的时候传递第一个参数,然后函数只需要传递一个参数即可。

#include
#include
#include
#include

template
class TooBig
{
private:
	T cutoff;
public:
	TooBig(const T& t) :cutoff(t) {}
	bool operator()(const T& v) { return v > cutoff; }
};
void outint(int n) { std::cout << n << " "; }
	int main()
	{
		using namespace std;
		TooBig f100(100);
		int vals[10] = { 50,123,422,23,564,432,154,65,342,3 };
		list yadayada(vals, vals + 10);
		cout << "Original lists:\n";
		for_each(yadayada.begin(),yadayada.end(), outint);

		yadayada.remove_if(f100); //等价于yadayada.remove_if(TooBig(100));
		cout << "Now:\n";
		for_each(yadayada.begin(), yadayada.end(), outint);
		return 0;
	}

【String类和标准模板库】_第8张图片

5.2预定义的函数符

STL定义了多个基本函数符,它们执行如两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的STL函数。头文件functional定义了多个模板类函数对象,其中包括plus<>()。可以用plus<>类来完成常规的相加运算。 

plus  add;

double y = add(2.2,3.4);

它使得将函数对象作为参数很方便。对于所有内置的算术运算符、关系运算符和逻辑运算符,STL 都提供了等价的函数符。

【String类和标准模板库】_第9张图片

【String类和标准模板库】_第10张图片

5.3自适应函数符和函数适配器

上表列出的预定义函数符都是自适应的。STL有5个相关概念,自适应生成器、自适应一元函数、自适应二元函数、自适应谓词和自适应二元谓词。使函数成为自适应的原因是,它携带了标识 参数类型 和返回类型的typedef成员。这些成员分别是result_type、first_argument_type和second_argument_type。函数自适应符的意义在于:函数适配器对象可以使用函数对象,并认为存在这些typedef成员。例如:

multiplies()函数符可以执行函数运算,但是他是二元函数,因此需要一个函数适配器,将接受两个参数的函数符转换为接受1个参数的函数符。除了前边使用类,STL使用binder1st和binder2nd类自动完成这一过程,它们将自适应二元函数转换为自适应一元函数。

binder1st(f2,val) f1;   f1(x) = f2(val,x).STL提供了函数bind1st(),以简化binder1st类的使用。对于上边乘法函数:

binder(binder(multiplies(),2.5));

binder2nd类与此类似,只是将常数赋给第二个参数,而不是第一个参数。

#include
#include
#include
#include
#include
void show(double n) { std::cout.width(6); std::cout << n << ' '; }
int main()
{
	using namespace std;
	
	double arr1[10] = {50,123,422,23,564,432,154,65,342,3};
	double arr2[10] = { 5,123,42,23,64,432,15,65,42,3 };
	vector gr8(arr1, arr1 + 10);
	vector m8(arr2, arr2 + 10);


	cout << "gr8:\n";
	for_each(gr8.begin(), gr8.end(), show);
	cout << "m8:\n";
	for_each(m8.begin(), m8.end(), show);


	vector sum(10);
	cout << "sum:\n";
	transform(gr8.begin(), gr8.end(), m8.begin(), sum.begin(), plus());
	for_each(sum.begin(), sum.end(), show);

	vector prod(10);
	cout << "prod:\n";
	transform(gr8.begin(), gr8.end(),prod.begin(),bind1st(multiplies(),2.5));
	//transform(gr8.begin(), gr8.end(), prod.begin(), bind2nd(2.5, multiplies()))不等价这个
	for_each(prod.begin(), prod.end(), show);

	
	return 0;
}

6.算法

STL包含很多处理容器的非成员函数,对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来提供访问容器中数据的通用表示。

6.1算法组

STL将算法库分成4组: 非修改式序列操作,修改式序列操作,排序和相关操作,通用数字运算。

前三组都在头文件algorithm中定义,第4组在头文件numeric中定义。

非修改式序列操作对区间中每个元素进行操作,这些擦欧总不修改容器的内容;

修改式序列操作既可以修改值,也可以修改值的排列顺序;

排序和相关操作包括多个排序函数和其他各种函数,包括集合操作;

数字操作包括将区间内容累计、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数,通常为数组的操作特性。

6.2算法的通用特征

对算法进行分类的方式之一是按结果放置的位置进行分类。有些算法就地工作,有些则创建拷贝。有些算法有两个版本:就地版本和复制版本。STL约定是,复制版本的名称将以_copy结尾。复制版本将接受一个额外的输出迭代器参数,该参数指定结果的放置位置。对于复制算法,STL统一约定是:返回一个迭代器,该迭代器指向复制的最后一个值后面的位置。

另一个常见变体是:有些函数有这样的版本,根据将函数应用于容器元素得到的结果来执行操作。这些范本的名称通常为_if结尾。

虽然文档可指出迭代器或函数符需求,编译器不会对此举进行检查,如果用了错误迭代器,会显式大量错误信息。

6.3STL和string类

string虽然不是STL组成部分,但设计时考虑到了STL,它包含了begin()、end()、rbegin()、rend()等成员。next_permutation()算法将区间内容转换为下一种排列方式。对于字符串,按照字母递增顺序进行,如果成功,返回true,如果区间已经处于最后的序列中,算法返回false,要得到区间内容所有的排列组合,应从最初的顺序开始,为此程序使用了sort()。

#include
#include
#include

int main()
{
	using namespace std;
	string letters;
	cout << "Enter the letter group(quit to quit):";
	while (cin >> letters && letters != "quit")
	{
		cout << "Permutations of " << letters << endl;
		sort(letters.begin(), letters.end());
		cout << letters << endl;
		while(next_permutation(letters.begin(),letters.end()))
			cout << letters << endl;
		cout << "Enter next sequence:(quit to quit)";
	}
	cout << "Done!";
	return 0;
}

【String类和标准模板库】_第11张图片 

6.4函数和容器方法

有时可以选择使用STL方法或STL函数。通常方法是更好的选择,因为它更适合于特定的容器,其次,作为函数成员,它可以使用模板类的内存管理工具,从而在需要时调整容器的长度。尽管方法通常更合适,但是非方法函数更通用,可以将它们用于不同的容器。 

6.5使用STL

STL是一个库,其组成部分被设计成协同工作。STL组件是工具,但也是创建其他工具的基本部件。

假设要编写一个程序,让用户输入单词,希望最后得到一个按输入顺序排列的单词列表、一个按字母顺序排列的单词列表(忽略大小写),并记录每个单词被输入的次数。

字母表用set对象,输入次数和单词可以存到map中。

#include
#include
#include
#include
#include
#include
#include
#include

using namespace std;
char toLower(char ch) {return tolower(ch);}
string& ToLower(string& st);
void display(const string& s);

int main()
{
	vector words;
	cout << "Enter words,quit to quit:\n";
	string input;

	//存入向量
	while (cin >> input && input != "quit")
		words.push_back(input);

	//按输入顺序输出向量
	cout << "You entered the following words:\n";
	for_each(words.begin(), words.end(), display);
	cout << endl;

	//放入集合中,并全改为小写
	set wordset;
	transform(words.begin(), words.end(), insert_iterator>(wordset, wordset.begin()), ToLower);
	cout << "alphbetic list of words:\n";
	for_each(wordset.begin(), wordset.end(), display);
	cout << endl;

	//和次数一起放到map里
	map wordmap;
	set::iterator si;
	for (si = wordset.begin(); si != wordset.end(); si++)
		wordmap[*si] = count(words.begin(), words.end(), *si);

	cout << "Word frequency:\n";
	for (si = wordset.begin(); si != wordset.end(); si++)
		cout << *si << " :" << wordmap[*si] << endl;
	return 0;

}
string& ToLower(string& st)
{
	transform(st.begin(), st.end(), st.begin(), toLower);
	return st;
}

void display(const string& s)
{
	cout << s << " ";
}

7.其他库

C++还提供了其他库,它们比本章讨论前面的例子更为专用。头文件complex为复数提供了类模板complex,包含用于float、long 和 long double 的具体化。C++11新增的头文件random提供了更多的随机数功能。

7.1vector、valarray和array

vector模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜索、将数据转移到其他容器中等。

valarray类模板是面向数值计算的,不是STL的一部分,没有push_back()和insert()方法。可以将STL功能用于valarray对象吗?答案是不能,因为没有begin()和end()方法,因此不能将它们用作指定区间的参数;另外,valarray是对象,不是指针,也没办法像处理常规数组那样,使用数组名 和数组名+10(长度)来作为区间参数;然后想到取地址,但是valarray没有超尾定义,会报错。为解决这个问题,C++11提供了接受valarray对象作为参数的模板函数begin()和end().因此,应该使用begin(valarray对象名)。

array是为代替内置数组设计的,它通过更好、更安全的接口让数组更紧凑,效率更高。Array表示长度固定的数组,因此不支持push_back()和insert(0,但提供了多个STL方法,包括begin()、end()、rbegin()、rend()。

下属程序演示了vector 和valarray类各自的优势。使用vector的push_back()方法和自动调整大小的功能来收集数据,然后对数字进行排序,将它们从vector复制到同样大小的valarray对象中,在执行数学运算。

#include
#include
#include
#include

int main()
{
	using namespace std;
	vector data;
	double temp;

	cout << "Enter numbers(<= 0 to quit):\n";
	while (cin >> temp && temp > 0)
		data.push_back(temp);

	sort(data.begin(), data.end());

	int size = data.size();
	valarray numbers(size);
	int i;
	for (i = 0; i < size; i++)
		numbers[i] = data[i];

	valarray sq_rts(size);
	sq_rts = sqrt(numbers);

	valarray results(size);
	results = numbers+2.0* sq_rts;

	for (i = 0; i < size; i++)
		cout << results[i] << " ";

	cout << "Done!";
	return 0;
}

【String类和标准模板库】_第12张图片

 除了前边讨论的外,valarray还有很多其他特性。

1.valarray vbool = number(示例中number) > 9 (布尔数组);

2.扩展的下标指定版本,silce类,和matlab一样。

varint[slice(1,4,3)] = 10;  slice(起始值,找到几个值,间隔),这种特殊属性可以使用一个一维数组valarray对象来表示二维数据。

#include
#include
#include

typedef std::valarray vint;
void show(const vint& v, int cols);

int main()
{
	using std::slice;
	using std::cout;
	vint valint(12);  //想象成4行3列
	
	int i;
	for (i = 0; i < 12; i++)
		valint[i] = std::rand() % 10;
	cout << "original array:\n";
	show(valint, 3);

	vint vcol(valint[slice(1, 4, 3)]);
	cout << "Second column:\n";
	show(vcol,1);

	vint vrow(valint[slice(3, 3, 1)]);
	cout << "Second row:\n";
	show(vrow, 3);

	valint[slice(2, 4, 3)] = 10;
	cout << "set last column to 10:\n";
	show(valint, 3);

	valint[slice(2, 4, 3)] = 10;
	cout << "set first column to sum of next two:\n";
	valint[slice(0, 4, 3)] = vint(valint[slice(1, 4, 3)]) + vint(valint[slice(2, 4, 3)]);
	show(valint, 3);
	return 0;
}

void show(const vint& v, int cols)
{
	using std::cout;
	using std::endl;

	int lim = v.size();
	for (int i = 0; i < lim; ++i)
	{
		cout << v[i];
		if (i % cols == cols - 1)
			cout << endl;
		else
			cout << " ";
	}
	if (lim % cols != 0)
		cout << endl;
}

【String类和标准模板库】_第13张图片 

7.2模板initial_list(C++11)

initializer_list是C++11提供的新类型,定义在头文件中。用于表示某种特定类型的值的数组,和vector一样,initializer_list也是一种模板类型。

需要注意的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。并且,拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,其实只是引用而已,原始列表和副本共享元素。

模板 initializer_list使得我们可以使用初始化列表语法将STL容器初始化为一系列值。如果类也有接受initializer_list作为参数的构造函数,这将带来问题:

std::vector vi(10);  //10个未初始化元素

std::vector vi({10});  //一个元素为10

如有类也有接受initializer_list作为参数的构造函数,是第二种含义。所有initializer_list元素类型必须相同,编译器进行必要转换,但不能进行隐式的窄化转换。除非类要用预处理长度不同的列表,否则让他提供接受initializer_list作为参数的构造函数没有意义。就算用初始化列表篇也会默认调用构造函数是括号类型的。

7.3使用initial_list

要在代码中使用initializer_list对象,必须包含头文件initializer_list。这个模板类包含成员函数begin()和end(),可以使用这些函数来访问列表元素。他还包含成员函数size(),该函数返回元素数。

#include
#include

double sum(std::initializer_list il);
double average(const std::initializer_list& ril);

int main()
{
	using std::cout;
	cout << "List 1 : sum = "< di = { 1.1,2.2,3.3,4.4,5.5 };
	cout << "List 2 : sum = " << sum(di)
		<< ", ave = " << average(di) << "\n";
	di = { 16,25,36,40,64 };
	cout << "List 3 : sum = " << sum(di)
		<< ", ave = " << average(di) << "\n";

	return 0;
}
double sum(std::initializer_list il)
{
	double tot = 0;
	for (auto p = il.begin(); p != il.end(); p++)
		tot += *p;
	return tot;
}
double average(const std::initializer_list& ril)
{
	double tot = 0;
	for (auto p = ril.begin(); p != ril.end(); p++)
		tot += *p;
	return tot / ril.size();
}

【String类和标准模板库】_第14张图片

 可以将一个initializer_list赋给另一个initializer_list,提供initializer_list的初衷为了能够将一系列值传递给构造函数或其他函数。

有了initializer_list之后,对于STL的container的初始化就方便多了,比如以前初始化一个vector需要这样:

std::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);

而现在c++11添加了initializer_list后,我们可以这样初始化

std::vector v = { 1, 2, 3, 4 }。

你可能感兴趣的:(C++学习记录,1024程序员节)