您的位置:首页 > 编程语言 > C语言/C++

C++:顺序容器与迭代器

2015-05-13 11:08 288 查看

0.顺序容器与迭代器的概念

容器:一个容器实际上就是一组相同数据类型元素的集合。相当于是对C语言中的内置数组的一种泛化。顺序容器为程序员提供了控制元素存储和访问顺序的能力。

迭代器:迭代器是C++为了更好的切合容器的使用而引入的一种特殊的数据类型。迭代器在功能上和C语言的指针十分相像,可以快速方便的访问、查找、修改容器里边的元素。

1.常用顺序容器及其迭代器

容器类型数据结构支持的操作
vector可变长数组快速随机访问 在尾部插入删除元素很快
deque双端队列快速随机访问 在头、尾插入删除元素很快
list双向链表双向顺序访问 任意位置插入删除元素都很快
forward_list单向链表单向顺序访问 任意位置插入删除元素都快
array固定大小数组快速随机访问 不能添加或者删除元素
string可变长字符数组快速随机访问 尾部插入删除元素很快
根据上表可以得出一些结论:

除固定大小的array外,其他容器都可以添加删除元素,也就是说提供了高效、灵活的内存管理。

string和vector将元素存储在连续的内存空间,可以快速访问数据,但是删除与插入时会移动很多元素所以会很慢。

list和forward_list这两个容器的设计目的是令容器在任意位置插入和删除元素都很快,作为代价,在这两种容器中查找元素比较慢。

deque更为复杂。它支持如同string和vector的快速随机访问,在中间位置删除添加元素花费很高,但是在deque的头尾插入元素是很快的,几乎和list和forward_list花费相当。

一般来说,我们都选用vector作为常用容器,除非有更好的理由更换。

下面简单的介绍一下如何定义一个容器。

既然是容器,那么肯定能容纳不同的数据元素。我们希望vector既可以容纳int也能容纳double,而且这应当是两种不同的容器类型才可以。所以C++将所有容器都设定为模板类,通过下列方式定义容器:

vector<typename> vec;


这条语句的意思是:vec是一种vector的容器,其中容纳的元素类型是typename的

举个例子:

vector<int> intVec;
vector<double> doubleVec;


实际上,typename不必须是系统内置的类型,可以是用户自定义的类、结构体、范型

vector<myClass> vec;
vector< vector<int> > intVec;


该例子中的第二个,表示intVec是一个vector容器,容器中容纳的元素是一个int的vector(这里相当于是二维数组)

下面简单说一下迭代器。

因为迭代器可以对容器的元素访问,所以迭代器的类型必须与容器类型相匹配。

所以,迭代器的定义应该按照下述方式:

vector<int>::iterator it1;
vector<double>::iterator it2;


it1只能指向int型vector,it2只能指向double型vector。

通常,我们都是用一对迭代器来指示某一个范围。

两个迭代器分别指向同一个容器当前元素或者是尾元素的下一个位置

可以形象的表示为:

[begin,end)(值得一提的是,begin和end可以指向同一个元素)

表示范围从begin开始到end之前结束。

于是,给出定义,当且仅当两个迭代器begin和end满足下列所有条件时,我们称begin和end为一个迭代器对。

它们指向同一个容器的元素,或者是容器最后一个元素之后的位置

我们可以通过反复递增begin到达end,即end不在begin之前。

在这两个条件的约束下,我们可以利用迭代器循环处理一个范围内的元素

while(begin != end){//注意这里不可以写"<"或者">",指针迭代器不可以比较大小
*begin = val;//正确,范围非空,begin指向一个元素
//迭代器是一种特殊类型的指针,因此可以通过解引用符*来访问迭代器指向内存中的数据
++begin;
}


对于标准容器,可以直接使用成员函数begin和end获得容器的迭代器。

例如:

vector<int>::iterator it1 = intVec.begin();
vector<double>::iterator it2 = doubleVec.end();


2.容器的定义与初始化

每个容器类型都定义了一个默认构造函数。除array外,其他容器的默认构造函数都会创建一个指定类型的空容器

构造方式解释
Container c调用默认构造函数。如果c是array,c中元素按照值初始化方式,否则c为空
Container c1(c2)c1初始化为c2的拷贝。c1和c2必须同种容器、容器元素类型相同,对于array类型,c1和c2必须等长
Container c{a,b,..}列表初始化。要求元素类型相容(即可以存在隐式转换)。对array类型,列表中元素数目必须小于等于c的长度,遗漏元素全部默认初始化(int类型就会赋值为0,因为是列表初始化)
Container c1(c2.beg,c2.end)c初始化为[beg,end)范围内的拷贝,元素类型必须相容,c1和c2的容器类型不必相同,array不适用
Container seq(n)seq是顺序容器,包含n个元素,对元素进行值初始化,如果元素是某类的对象,调用默认构造函数,string不适用
Container seq(n,t)将n个元素初始化为值t
举几个例子:

vector<int> intVec1;//默认构造函数,空的vector;

vector<int> intVec2 = {1,2,3,4,5};//列表初始化

vector<int> intVec3(intVec2);//拷贝初始化

vector<int> intVec4(10);//seq(n)

vector<string> strVec1(10,"i am ok");//seq(n,t)

list<string> strLis1 = { "Milton", "Shakespare", "Austen" };

vector<string> strVec2(strLis1.begin(),strLis1.end());//strVec2与strLis1虽然容器类型不同,但是容器内元素类型相同

vector<double> doubleVec1 = {1.0,2.0};

list<int> intLis1(doubleVec1.begin(),doubleVec1.end());//这里doubleVec1和intLis1容器类型不同,元素类型不同,但是元素类型是相容(double可以隐式转换为int)的因此编译成功


3.容器常用函数

assign 赋值函数

常规的赋值是通过赋值符号“=”完成的。但是“=”两边容器类型不匹配时无法使用“=”符号,为此有了更为便利的内置函数assign,通过迭代器操作。

举个例子:

list<string> lis = { "Milton", "Shakespare", "Austen" };
vector<string> vec;
vec.assign(lis.begin(),lis.end());


这样就完成了对vec的赋值。

需要注意的是,赋值运算会使得原本容器(vec)的迭代器、引用、指针失效

也就是说:

list<string> lis = { "Milton", "Shakespare", "Austen" };
vector<string> vec;
vector<string>::iterator iter3 = vec.begin();
vec.assign(lis.begin(),lis.end());//这里是用了assign赋值,赋值运算会使迭代器、引用、指针失效
//cout << *iter3 << endl;//这里如果不注释掉,运行会崩坏,原因是assgin后使得iter3失效,不在指向vec.begin()
//经测试iter3的地址没有发生改变的,但是对iter3解引用不是我们想看到的Milton而是系统崩溃,实际上vec.begin()在assign后发生了改变
//以前的iter3所指向的内存不可以被访问,也就是我们这里所说的迭代器失效


swap交换函数

swap函数交换两个类型相同的容器的内容,调用该swap两个容器的元素会发生互换。swap保证交换很快,因为实际上并不是元素发生了交换,而是交换了两个容器的内部数据结构(指针)。所以swap操作后容器的迭代器并不会发生改变。

看一个例子:

vector<string>::iterator it1 = vec.begin();
vector<string>::iterator it2 = vec2.begin();
cout << "before swap  vec[0] at : " << &vec[0] << " and  vec.begin() is " << *vec.begin() << endl;
cout << "before swap vec2[0] at : " << &vec2[0] << " and vec2.begin() is " << *vec2.begin() << endl;
swap(vec, vec2);
//vec.swap(vec2);
cout << "after  swap vec2[0] at : " << &vec2[0] << " and  vec2.begin() is : " << *vec2.begin() << endl;
cout << "after  swap  vec[0] at : " << &vec[0] << " and vec.begin() is : " << *vec.begin() << endl;


输出结果如下:

before swap vec[0] at : 002F8190 and  vec.begin() is Milton
before swap vec2[0] at : 002FBC60 and vec2.begin() is i
after  swap vec2[0] at : 002F8190 and  vec2.begin() is : Milton
after  swap  vec[0] at : 002FBC60 and vec.begin() is : i


由此我们可以看出,swap交换了两个容器的内部指针。

这样指向原来指针的迭代器自然不会失效。

详细说来:

假定iter在swap前指向vec[3],swap后iter指向vec2[3]

(因为vec[3]和vec2[3]的地址发生了交换)



swap之后:



可以看出来iter并没有失效

insert插入函数

insert函数可以对容器插入一个或者几个元素。

基本的语法格式如下:

vec.insert(vec.begin(), "hello");//在迭代器之前的位置插入hello


除了第一个迭代器参数以外,insert函数还可以接受更多的参数

vec.insert(vec.end(), 10, "hello");
//在vec的末尾插入10个“hello”元素
vec.insert(vec.end(), { "xx", "yy", "zz", "kk" })
//在vec后边插入四个string,分别为"xx","yy","zz","kk"
vec.insert(it1,it2,it3)//将[it2,it3)范围内的元素插入到it1之前


看最后一个例子:

c++标准要求要拷贝的范围不能指向与目的位置相同的容器

vec.insert(vec.begin(),vec.begin(),vec.end());


也就是说这种方式是不允许的。亲测有些编译器可以通过,那是编译器的实现问题,不要采用这种方式。

insert()的返回值将是新插入元素的迭代器,我们可以利用这个方法,不断的在某一个位置(一般来说是begin位置)之前插入元素:

auto it = vec.begin();
while (cin >> word){
it = vec.insert(it, word);//相当于push_front
}


这里需要强调的是:

insert()操作不一定会引起迭代器失效!

如果插入新的元素后需要重新分配内存,那么所有迭代器失效。

如果插入新的元素后不会重新分配内存,那么插入位置之前的迭代器不会失效,插入位置之后的迭代器失效。

因此,下述代码是严重错误:

void insertDoubleValue(vector<int> &iv, int some_val)
{
vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2 ;
while (iter != mid){
if (*iter== some_val)
iter = iv.insert(iter, 2 * some_val);
else ++iter;
}
}


插入后,iter之后的迭代器将发生改变,意味着mid的值会发生改变,iter永远不等于mid,因此,应当改成:

void insertDoubleValue(vector<int> &iv, int some_val)
{
vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2 ;
while (iter != mid){
if (*mid == some_val)
mid = iv.insert(mid, 2 * some_val);
else --mid;
}
}


emplace 构造操作

emplace函数直接构造而非拷贝元素。

调用insert或者push函数时,将对象当做参数传递,这些对象被拷贝到容器中。

调用emplace函数时,将对象传递给对应元素类型的构造函数,直接在内存中构造对象。

举个例子:

假如说有如下类:

class Sales_item {
public:
Sales_item(): units_sold(0), revenue(0.0) { }
Sales_item(const std::string &book):
bookNo(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is) { is >> *this; }
private:
std::string bookNo;      // implicitly initialized to the empty string
unsigned units_sold;
double revenue;
};
vector<Sales_item> vec3;

vec3.emplace_back(Sales_item());//调用默认构造函数
auto it = vec3.end();
vec3.emplace(it, Sales_item("999-99999"));//调用Sales_item(const & string)


empalce会在容器管理的内存空间中直接创建对象,调用push则会创建一个临时对象,然后将其压入容器。

显然这种方式更适合将类插入到某容器中,而且会调用构造函数,这就需要那个类有合适的构造函数,参数必须能匹配构造函数。

vec3.emplace(it, Sales_item("999-99999",0,0));
//error Sales_item不存在接受三个参数的构造函数


显然emplace会使迭代器失效

erase删除函数

erase函数和insert函数很像,可以在容器中删除一个或者多个元素。

例如,下面一个循环删除vec中的所有奇数:

vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto it = v.begin();
while (it != v.end()){
if (*it % 2 == 0)
it++;
else
it = v.erase(it);//返回删除元素 之后 的迭代器
}


删除多个元素:

v.erase(it1,it2)//删除[it1,it2)范围内元素,并且返回被删除的最后一个元素的后一个位置
it1 = v.erase(it1,it2);//实际上删除后,it1 == it2
v.erase(v.begin(),v.end() - 3);//保留最后三个元素删除其余元素


erase操作使得被删除元素之前的迭代器、引用、指针有效,之后的元素迭代器等会失效。

并且,erase操作总会使尾后迭代器失效,因此删除操作时保存尾后迭代器将引起灾难性错误。

auto it = v.end();
auto x = v.begin();
while (x != it){
if (*x == 5)
x = v.erase(x);
else
x++;
}


这段代码将会引起灾难性错误,大多数都会进入死循环。因为一旦完成一次删除操作,尾后迭代器it失效了,x永远不会 == it,将会死循环,正确的方法应当是:

auto x = v.begin();
while (x != v.end()){
if (*x == 5)
x = v.erase(x);
else
x++;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: