C++中对STL相关内容的初步整理
- 泛型程序设计和STL概述
- 容器(vector)
- 迭代器(iterator)
- 算法
- string类型
泛型程序设计和STL概述
C++有两种特性来支持数据独立:模板和运算符重载。
泛型程序设计是以类型需求为中心的程序设计方法。其主要推动来自C++的标准模板库,STL是迄今最为成功和卓越的泛型程序设计范例。
STL的一个重要特点是数据结构和算法的分离。STL中包含的是堆栈、队列和其他许多标准数据结构的实现和许多重要泛化算法的实现。
STL的另一个重要特点是它不是面向对象的。STL中的类都是模板类。STL主要依赖于模板而不是封装、继承和多态(3要素)。在STL中找不到任何明显的类继承关系。
由于STL是基于模板、内联函数的使用,追求的是运行的效率,避免了虚函数的开销,这使得生成的代码短小高效。
STL提供了大量的模板类和函数,可以在面向对象程序设计和常规编程中使用。最有用的泛型组件就是迭代器、容器和算法。
为了避免和其他头文件冲突,确保可移植性,在包含头文件时缺省.h后缀。
容器(vector)
STL对最常用的数据结构提供支持,这些模板的参数允许容器中元素的数据类型,可以将许多重复而乏味的工作简化。
容器类库中包括7种基本容器:向量< vector >、列表< list >、双端队列< deque >、集合 < set >、映射< map >、栈< stack >和队列< queue >。
1、顺序容器
顺序容器将同类型的对象集合按严格的线性关系组成起来。STL提供了向量、列表和双端队列3个顺序容器。
std::vector向量容器,其数据安排和操作方式和数组非常相似,但向量容器是动态的,会根据需要自动扩充空间以容纳新的元素。
std::list列表容器,提供随机访问的可变长度表。可在任意位置插入删除元素,每次插入和删除元素就分配一个元素的空间。
std::deque双端队列,可在两端分别进行插入和删除元素的操作,长度可变。
2、顺序容器的接口
除了所有容器公用的运算符和方法外,顺序容器还拥有一个插入和删除方法集,插入方法是把元素添加到容器中,可以通过构造函数或使用插入成员函数来完成;删除方法是从容器中移走元素,但是移走元素调用的是容器中的构造函数而不是容器的析构函数,容器出作用域或进行删除操作才调用容器的析构函数。
每个顺序容器至少有3个插入方法和3个删除方法。
插入方法有:
(1)push_front()和push_back(),分别将一个元素加入到容器的首部或尾部;
(2)insert(),这个函数在不同的顺序容器中有不同的版本,不会使列表容器的迭代器失效,但可以让向量和双端队列的迭代器失效,故在插入和删除后面使用迭代器时要谨慎;
(3)通过使用赋值运算符向容器中添加元素,可以把一个容器赋值到另一个容器。
删除方法有:
(1)pop_front()和pop_back(),分别从容器的首部和尾部移走元素;
(2)erase(L),移走由L指定的元素,而erase(L1,L2)移走L1和L2之间的元素;
(3)clear(),移走容器中的所有元素,对每个删除方法在移走元素时都要调用析构函数。
向量容器
可以将向量(vector)认为是能在程序运行时改变长度的数组。向量的用途和数组相同,只是它的长度能在程序运行期间自由改变。
向量元素的索引编号从0开始,这和数组相同。可通过符号“[ ]”来读取和更改这些元素,但是将“[ ]”应用到向量时有一项限制:只能用v[i]更改一个已经赋值的元素,但不能使用v[i]来初始化编号为i的元素。当需要向向量中首次添加一个元素时,弹出要使用成员函数push_back().
向量容器具有自动存储管理功能,可以将程序员从乏味容易出错的分配和释放内存中解放出来。例如,vector可以用从键盘输入的数来作为元素的个数,即定义可变的容量。
例:
//容量可变的向量容器
#include <vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> sample(3,5); //向量sample的无素初始化为3个5
vector<int> intv;
int temp,i;
cout<<"Enter numbers and press 0 to end"<<endl;
cin>>temp;
while(temp!= 0)
{
intv.push_back(temp); //向向量intv中添加元素
cin>>temp;
}
cout<<"There are "<<intv.size()<<" elements in intv."<<endl;
for(i=0;i<intv.size();i++)
{
cout<<intv[i]<<" ";
}
cout<<endl;
cout<<"Sample element is :";
for(i=0;i<3;i++) //输出向量sample中的元素
{
cout<<sample[i]<<" ";
}
cout<<endl;
return 0;
}
运行结果:
Enter numbers and press 0 to end
1 2 3 4 5 6 0
There are 6 elements in intv.
1 2 3 4 5 6
Sample enlement is :5 5 5
分析:此例中向量intv没有预定大小,如果需要更大的容量来储存更多的元素,它的容量就会自动扩充。当向量中的一个元素位置获得了它的第一个元素之后(无论是通过push_back()还是构造函数来初始化),以后可以使用[ ] 来访问这个元素位置,就像普通的数组元素。
容器重分配(reserve)
如果必须考虑效率问题,可考虑由程序员自己来管理容量,而不是接受每次都是容量倍增的默认行为。可使用成员函数reserve()来显式地增加一个向量的容量。
reserve()保证容器预分配足够内存来存储至少n个元素。例如:
v.reserve(32); //将容器设置成至少32个元素;
v.reserve(v.size() + 10); //将容量设置成当前元素数目加上10
注意:虽然可以用v.reserve()来增加一个向量的容量,但如果参数值小于当前容量的前提下,它不一定能减小向量的容量。
capacity()和size()
向量的长度是向量中元素的个数,容量则是当前实际分配的内存的长度和个数。size()返回容器现在存储元素的数量。capacity()返回容器不需重分配就能存储的元素的总数量。
换句话说,capacity()-size()是在不重新分配内存的情况下容器中可用单位的总数。resize()分配n个对象的内存并初始化(可提供不同的初始值作为第二个可选参数)。reserve()分配内存而不初始化。另外,reserve()不改变size()返回的值,它只改变capacity()返回的值。resize()都改变。
例:容器重分配即容量测定举例
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector <int> vs;
vs.reserve(10); //得到至少10个整数的空间
vs.push_back(1); //插入元素1
vs.push_back(2);
cout<<"size: "<< vs.size()<<endl; //输出有效元素个数
cout<<"capacity: "<<vs.capacity()<<endl; //输出vs所占空间大小
cout<<"there's "<<vs.capacity()-vs.size()<<" empty elements before reallocation"<<endl;
vs.resize(20); //resize()后会给予其所有空间初始值,故此后size()和capacity()返回的值相同
cout<<"size: "<<vs.size()<<endl;
cout<<"capacity: "<<vs.capacity()<<endl;
return 0;
}
运行结果:
size:2
capacity:10
there’s 8empty elements before reallocation
size:20
capacity:20
成员函数 at()
at()会检查引用是否是有效的元素。它将执行范围检查,并且它将在试图访问范围以外的成员时抛出类型 std::out_of_range的异常。
#include<vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> vi(10,4); //使用默认构造函数申请10个元素并初始化为4
vi[0]=100; //重载运算符[]
try
{
cout<<vi.at(10)<<endl; //测试下标10是否越界,并抛出异常
}
catch(std::out_of_range)
{
cout<<"out of range"<<endl; //异常处理
}
for(int i=0;i<10;i++)
cout<<vi[i]<<" ";
cout<<endl;
return 0;
}
运行结果:
out of range //测试出10已越界
100 4 4 4 4 4 4 4 4 4
front 和 back 操作
成员函数front()和back()函数分别访问容器开始和结尾的一个元素。成员函数 push_back()在容器的结尾附加一个元素。当容器容量不足时函数重分配内存再附加元素。成员函数 pop_back()将最后一个元素移出容器。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<char> v_ch;
v_ch.push_back('A');
v_ch.push_back('B');
v_ch.push_back('C');
cout<<"front: "<<v_ch.front()<<endl;
cout<<"back: "<<v_ch.back()<<endl;
v_ch.pop_back(); //移走最后一个元素
cout<<"back: "<<v_ch.back() <<endl;
return 0;
}
运行结果:
front:A
back:C
back:B
另:STL容器重载赋值运算符,因此允许同类型的容器互相赋值。 每一个vector元素都必须是同样的大小。因为派生对象可能有额外的成员,其大小可能比基类要大。
迭代器(iterator)
迭代器相当于容器中的一种抽象的“指针”。迭代器本质上不是指针,但不妨把它当成指针来用。STL中每一个容器类模板都定义了一组对应的迭代器类。使用迭代器,算法函数可以访问容器中指定的元素,而无需关心元素的具体类型。
迭代器可以使用以下的重载操作符来操作迭代器,这些操作符将应用于迭代器对象。
(1)前置和后置“++”,用于将迭代器指向下一个数据项。
(2)前置和后置“–”, 用于将迭代器指向上一个数据项。
(3)等于操作符“==”和不等于操作符“!=”,用于测试两个迭代器是否指向同一个数据位置。
(4)一个间接访问运算符“ * ”,如果后面的变量P是一个迭代器,那么使用*P就能访问唯于P处的数据。
- using 声明
STL中的每种容器类型都有它自己的迭代器类型,不过所有的迭代器的基本使用方法都是相同的。讨论迭代器时,通常将::操作符应用于另一个级别。
using std::vector< int > ::iterator;
在这个例子中,标识符iterator命名了一种迭代器类型。所以以下语句是允许的:
iterator p;
如果习惯事先声明:
using namespace std;
则可以直接使用:
vector< int > ::iterator p;
例:
#include<iostream> //iterator 迭代器类型初应用
#include<vector>
using namespace std;
int main()
{
vector <char> v;
v.push_back('a'); //在容器尾部添加字母'a'
v.push_back('b');
v.push_back('c');
v.push_back('d');
for(int i=0;i<4;i++)
{
cout<<"v["<<i<<"]=="<<v[i]<<endl; //对容器的访问
}
vector <char>::iterator p=v.begin(); //定义迭代器p并初始化为指向容器的起始位置
cout<<"The third is "<<v[2]<<endl;
p++; //迭代器移向下一个数据项
cout<<*p<<endl; //输出第二个字母'b'
p++;
cout<<*p<<endl;
p--;
cout<<*p<<endl;
cout<<*(p+2)<<endl;
return 0;
}
运行结果:
v[0]==a
v[1]==b
v[2]==c
v[3]==d
The third is c
b
c
d
- 指针用作迭代器
一个指针也是一个迭代器。
例:
#include<iostream> //把指针作为迭代器用于STL的find()算法来搜索普通的数组
#include<algorithm>
using namespace std;
#define SIZE 10
int iarray[SIZE];
int main()
{
iarray[5]=50;
int* ip= find(iarray,iarray + SIZE,50);
if(ip==iarray + SIZE) //若不存在需要寻找的元素,则ip指向数组末尾之后的区域,不存在
{
cout<<"50 not found in array"<<endl;
}
else
{
cout<<*ip<<" found in array"<<endl;
}
return 0;
}
程序定义了尺寸为SIZE的全局数组。由于是全局变量,所以运行时数组自动初始化为0;
find()函数接受3个参数,头两个定了搜素的范围,第三个参数是待定位的值。find()函数返回和前两个参数相同类型的迭代器,这里是一个指向整数的指针ip。
为了判断find()是否成功,例子中测试ip和past-the-end的值是否相等:if(ip== iarray+ SIZE),如果表达式为真,则表示在搜索的范围内没有指定的值,否则就是指向一个合法对象的指针。
- 容器中的迭代器
尽管C++指针也是迭代器,但用的更多的是容器迭代器。两个典型的容器类方法是begin()和end()。他们在大多数容器中表示整个容器范围。begin()返回指向容器第一个元素的迭代器。end()返回一个指向容器最后一个元素之后(past)的迭代器。其他一些容器还使用rbegin()和rend()的方法提供反向迭代器,以按反方向指定对象范围。
例:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
vector<int> intVector(20,5);
intVector[5] = 50;
vector<int>::iterator intIter = find(intVector.begin(),intVector.end(),50);
if(intIter != intVector.end())
{
cout<<*intIter<<" found in vector "<<endl;
}
else
{
cout<<"50 not found in vector "<<endl;
}
return 0;
}
运行结果:
50 found in vector
注意:end()成员函数返回一个指向位于容器最后一个元素之后的迭代器。类似C语言风格字符串的表示方法。
算法
STL中定义了丰富的算法集合。算法是通用的,每个算法都适合于若干种不同的数据结构,而不是仅仅能用于一种数据结构。算法不是直接使用容器作为参数,而是使用迭代器类型。STL几乎所有的算法的头文件都是< algorithm >,根据算法的语义,将算法主要分为3种:不改变序列的算法、改变序列的算法和排序算法。
- 不改变序列的算法
不改变序列的算法是不直接改变序列的操作,包括查找、检查一致性和计数等。
find()函数返回指向第1个与要查找值相匹配元素的迭代器。如果不能找到要求的值,它返回最后一个元素之后一个位置的迭代器。
find()算法举例:
#include<iostream> //分别输出从键盘输入的字符串第一个'e'字符前后的字符串
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
vector<char> line;
cout<<"Enter a line of text: "<<endl;
char next;
cin.get(next);
while(next!='\n')
{
line.push_back(next);
cin.get(next);
}
vector<char> ::const_iterator where;
where =find(line.begin(),line.end(),'e');
vector<char>::const_iterator p;
cout<<"Before first e :\n";
for(p=line.begin();p!=where;p++)
cout<<*p;
cout<<endl;
cout<<"After first e:\n";
for(p=where;p!=line.end();p++)
cout<<*p;
cout<<endl;
cout<<"The end\n";
return 0;
}
运行结果:
Enter a line of text:
Good morning everyone!
Befor first e :
Good morning
After first e:
everyone!
The end.
不改变序列算法的应用举例:
#include<iostream>
#include<vector>
#include<algorithm>
#include<functional>
using namespace std;
int main()
{
int intarr_1[]={3,4,5,7,7,7,6,8,9,8};
//以数组intarr_1中的元素初始化向量ivect_1
vector <int> ivect_1(intarr_1,intarr_1 + sizeof(intarr_1)/sizeof(int));
int intarr_2[]={3,4,5,7,7,7,9,7};
vector <int> ivect_2(intarr_2,intarr_2 + sizeof(intarr_2)/sizeof(int));
//找出相邻两个元素值相等的第一个元素
vector <int>::iterator ip;
ip= adjacent_find( ivect_1.begin(),ivect_1.end());
cout<<*ip<<endl;
//统计ivect_1中元素值为7的元素个数
cout<<count(ivect_1.begin(),ivect_1.end(),7)<<endl;
//找出ivect_1中元素值小于7的元素个数
cout<<count_if(ivect_1.begin(),ivect_1.end(),bind2nd(less<int>(),7))<<endl;
//判断两个容器ivect_1和ivect_2是否相等(0为假,1为真)
cout<<equal(ivect_1.begin(),ivect_1.end(),ivect_2.begin())<<endl;
return 0;
}
运行结果:
7
3
4
0
注意:此例中使用大量泛型算法
- 改变序列的算法
copy()算法举例:
#include<algorithm>
#include<list>
#include<vector>
#include<iostream>
using namespace std;
int main()
{
list<char> line;
vector<char> word(3);
line.push_back('S');
line.push_back('T');
line.push_back('L');
list<char>::iterator q=line.begin();
//拷贝List的元素到vector,从vector开始
copy(line.begin(),line.end(),word.begin());
vector<char>::iterator p = word.begin();
while(p!= word.end())
{
cout<<*p++;
}
cout<<endl;
return 0;
}
运行结果:
STL
- 排序算法
这种类型包含了用于排序、合并序列的算法和操作已排序序列类集合算法,所有这些算法都是通过对序列元素进行比较操作来完成的。
4个排序算法:sort()、parital()、parital_copy()、和 stable_sort();
4个二分查找算法:binary_search()、lower_bound()、uper_bound()和equal_range();
…
举例:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
int intarr_1[]={3,14,5,17,27,7,6,18,9,8};
vector <int> ivect_1(intarr_1 , intarr_1 + sizeof(intarr_1)/sizeof(int));
sort(ivect_1.begin(),ivect_1.end());
vector <int>::iterator p = ivect_1.begin();
while (p!=ivect_1.end())
{
cout<<*p++<<" ";
}
cout<<endl;
return 0;
}
运行结果:
3 5 6 7 8 9 14 17 18 27
string 类型
- 标准类string
string类型在名称同样为< string >,需要预定义;
使用string类 ,可以使用“+”来连接两个字符串。
标准类string有一个默认构造函数和多个重载构造函数。默认构造函数将一个string对象初始化成空字符串。
string初始化应用举例:
#include<string>
#include<iostream>
using namespace std;
int main()
{
const char text[] = "hello world";
string s1 = text;
string s2(s1);
string s3(&text[0], &text[5]);
string s4(10, '0');
string s5(&*s2.begin(), s2.find(' '));
cout<<s1<<endl;
cout<<s2<<endl;
cout<<s3<<endl;
cout<<s4<<endl;
cout<<s5<<endl;
return 0;
}
运行结果:
hello world
hello world
hello
0000000000
hello
- string 类的 I/O
可以用流操作符输出string对象。但输入时使用>>和cin时,会忽略最初的空白符号,并在接受输入字符后遇到第一个空白字符就停止输入。
如果希望输入整行读入一个变量,可以使用getline函数。
#include<string>
#include<iostream>
using namespace std;
int main()
{
string subject("We love ");
string object;
getline( cin, object, '.'); //第3个参数为输入停止信号,读入该字符时停止读入;
cout<< subject << object <<endl; //如果想一次读入一个字符,可使用 cin.get 函数。用法:char x = cin.get();或cin.get(x);
return 0;
}
运行结果:
our homeland. //此行由键盘输入
We love our homeland.
如果想一次读入一个字符,可使用cin.get.