C++ Primer16 顺序容器

上一回说到标准IO库,下面是顺序容器。加油,加油!

顺序容器

顺序容器的顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。

C++的标准库提供了很多容器类型,但很多基本都用不到。我们只介绍常用的,一个是vector,一个是string,顶多还有一个deque。

下面来介绍一些操作:

一个概念:迭代器范围: 迭代器范围指的是 [begin,end)

使用这种范围,当begin和end相等的时候,范围为空。

几种迭代器

auto it1 = a.begin();
auto it2 = a.rbegin();  //反向迭代器
auto it3 = a.cbegin();
auto it4 = a.crbegin();

不以c开头的成员函数是被重载过的,当我们使用const的容器调用会返回const迭代器,否则返回非常量迭代器。

以c开头函数一定返回const类型迭代器。

常用的容器定义方式:

C c; //默认构造函数

C c1(c2);
C c1=c2; //拷贝构造函数

C c{a,b,c,d...};
C c={a,b,c,d...};

C c(b,e);

将一个容器初始化为另一个容器的拷贝

有两种方法:一种是调用拷贝构造函数,另一种是使用迭代器范围的构造函数。

对于第一种,两个容器的类型以及元素的类型必须匹配。
对于第二种,就不要求容器类型相同了,只要元素类型匹配或能相互转化就ok。

除了上面这些,还有一个容器构造函数:它接受一个大小的值和一个元素初始值

vector<int> vec(10,-1); //10为大小,-1为元素初始值
deque<string> de(10); //如果只提供大小的话,其值是默认初始化的。

赋值与swap

上面介绍了初始化和迭代器等,在初始化中用的是拷贝构造函数,下面来说一说赋值构造函数。

这些操作适合于所有的容器

c1 = c2; //将c1中的元素替换为c2中元素的拷贝  c1,c2具有相同的类型
c = {a,b,c...};
swap(c1,c2);   //交换c1和c2中的元素
c1.swap(c2);	//感觉有点像移动构造函数

swap操作交换两个相同类型容器的内容。

assign操作
对于顺序容器,为了支持不同类型容器之间的赋值操作,定义了assign操作。

seq.assign(b,e);//将seq的内容替换为b到e的内容
seq.assign(il);//将seq内容替换为初始化列表的内容
seq.assign(n,t);//将seq内容替换为n个t

例子如下

	list<char*> li = { "cuinan","lala" };
	vector<string> vec;
	vec.assign(li.cbegin(), li.cend());

	list<const char*> lis1(1);
	lis1.assign(10, "hi");

每个容器都支持三个操作:
size(),empty(),max_size()。

容器的关系运算符

容器也是可以使用关系运算符来进行比较的。但关系运算符左右两边的运算对象需要容器类型一致,元素类型一致。
其比较的方式与string的比较方式一样。

这个知识点挺重要: 容器的关系运算符使用元素的关系运算符完成比较。只有当元素定义了相应的运算符,才可以使用容器运算符。

容器的== 运算符使用的是元素的 == 运算符 。而其他关系运算符是使用元素的<运算符。

比如对于咱们前面定义的Sales_data类,没有给他定义比较运算符,因此将其放入容器中后,容器无法使用比较运算符。
.

顺序容器的操作

下面是顺序容器特性操作。
这里我们只看vector string和deque。

添加元素

c.push_back(t);
c.emplace_back(args);
//vector,string不支持push_front和emplace_front
c.push_front(t);
c.emplace_front(args);

c.insert(p,t);//在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向				   新添加元素的迭代器。
c.emplace(p,args);

c.insert(p,n,t)//在迭代器p之前插入n个t元素,返回插入的第一个元素的迭代器
c.insert(p,b,e)//在迭代器p之前插入迭代器b到e范围的元素,还是返回第一个
c.insert(p,li)//在p之前插入{}的元素,依然是返回第一个

有一个特别注意:
向vector、string、deque插入元素,会使得原来的指针、引用、迭代器都失效。

还有一句话:向vector或string中间位置插入元素,需要移动元素操作。
.
.
push_back
对string调用push_back可以向字符串末尾加字符

word.push_back('s');

重要
当用一个对象初始化容器,或者将一个对象插入容器,实际放入容器的是对象的拷贝,而不是对象本身。容器中的元素与提供值的对象没有任何关联。
.
.
push_front
vector和string都是不支持push_front()的。原因很简单,开销太大。
deque支持push_front

deque<int> de
for(size_t i=0;i<4;i++)
{
	de.push_front(i);//最后的结果是3210
}

.
.
insert

insert提供给我们更一般的插入操作。

insert的第一个参数可以指向容器中的任何位置,包括尾后迭代器的位置。

使用insert的返回值:
下面这个例子挺经典的

	int i;
	vector<int> temp = { 1,2,3,4,5,6 };
	auto iter = temp.begin();
	while (cin >> i)
	{
		iter = temp.insert(iter, i);
	}

这个例子的结果是不断的在前面插入元素。
.
.
emplace操作

C++11新标准中引入了三个新的向容器中插入元素的操作

  1. c.emplace_back()
  2. c.emplace()
  3. c.emplace_front()

它们分别对应的是push_back,insert,push_front。但有所不同的是,emplace系列是直接根据括号中的参数构造元素然后放入容器中,push系列是将括号中元素拷贝到容器中。

比如说下面这个:

	vector<Sales_data> sd;
	sd.emplace_back("c++", 100, 10);
	sd.push_back(Sales_data("c++", 100, 10));

记住这句话:emplace函数在容器中直接构造元素,其括号中的参数必须与元素的构造函数相匹配。

一个小练习,顺便复习一下sstream和fstream
从文件中读取string序列,存入deque,并通过迭代器打印。

int main()
{
	ifstream ifstr("test_fstream.txt");
	deque<string> de;
	istringstream iss;
	if (ifstr)
	{
		string str;
		while (getline(ifstr, str))
		{
			iss.str(str);
			string temp;
			while (iss >> temp)
			{
				de.push_back(temp);
			}
			iss.clear();//这里需要特别注意
		}
		for (auto iter = de.begin(); iter != de.end(); iter++)
		{
			cout << *iter << " ";
		}
		cout << endl;
		cout << "加载完成" << endl;
	}
	else
	{
		cout << "加载失败" << endl;
	}
}

在这里插入图片描述
在这里插入图片描述

访问元素

//这些操作对vector string deque都适用
c.front();//返回c中首元素的引用,若c为空,操作未定义
c.back();//返回c中尾元素的引用,若c为空,操作未定义
c[n];//返回第n个元素的引用
c.at(n)

注意,这些访问操作,它们返回的都是引用。因此,我们可以用这一特点来改变其值。

	deque<int> c;
	if (!c.empty())
	{
		c.front() = 42;//可以改变
		auto& v1 = c.back();
		v1 = 1024;//可以改变
		auto v2 = c.back();
		v2 = 0;//不可改变
	}

这一段程序注意,v1才能改变其值,v2并不能改变c.back()的值。

对于c[0]和c.at(0),它们都是通过下标来进行随机访问,不同的是: c[0]没有安全检查机制,而c.at(0)有安全检查机制,一旦下标越界,会抛出out_of_range异常。
在这里插入图片描述
这样我们就能准确的知道,我们访问越界了。
.
.
删除元素

c.pop_back();//删除最后一个元素 返回void
c.pop_front();//删除第一个元素 返回void vector和string不支持

c.erase(p);//删除迭代器p指向的元素,返回p之后的第一个元素
c.erase(b,e);//删除b到e范围的元素,返回e指向的元素

c.clear();//删除所有,返回void

这两句话挺重要:

删除deque中除首尾位置之外的任何元素都会使所有迭代器,引用和指针失效。
指向vector string删除点之后的迭代器,引用和指针都会失效。

使用erase的例子

	vector<int> vec = { 0,1,2,3,4,5,6 };
	auto iter = vec.begin();
	while (iter != vec.end())
	{
		if (*iter % 2)
		{
			iter = vec.erase(iter);
		}
		else
		{
			iter++;
		}
	}
elem1 = vec.erase(elem1,elem2);//调用后 elem1 == elem2
//删除elem1到elem2之前的元素

迭代器失效问题

向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效,如果我们使用这些失效的指针、引用或迭代器是一种严重的错误。下面分两种情况讨论:

1.添加元素

对于vector和string来说:如果重新分配存储空间,则所有… … …都会失效。如果不重新分配存储空间,则指向插入位置之前的… … …都有效,其他的无效。

对于deque来说:插入到除首尾位置之外的位置都会使… … …失效。插入到首尾位置,迭代器会失效,指针引用不会。

2.删除元素

对于vector和string来说:被删元素之前的… … …都有效,其他的失效。

对于deque来说:如果删除除首尾之外的其他元素,那么所有… … …都会失效,如果删除首尾元素,尾后迭代器会失效,其他… … …不受影响。

必须保证每次改变容器的操作之后都正确的重新定位迭代器。
添加或删除vector、string或deque的程序必须考虑迭代器失效问题。程序必须保证每个循环都更新迭代器,引用或指针。

下面是一个小例子:
删除偶数元素,复制奇数元素。

int main()
{
	vector<int> vec = { 0,1,2,3,4,5,6,7 };
	auto iter = vec.begin();
	while (iter != vec.end())
	{
		if (*iter % 2)
		{
			iter = vec.insert(iter, *iter);//更新了迭代器
			iter += 2;
		}
		else
		{
			iter = vec.erase(iter);//更新了迭代器
		}
	}
	for (auto s : vec)
	{
		cout << s << " ";
	}
}

在这里插入图片描述

不要保存end返回的迭代器

当我们添加或删除vector或string元素时,原来的保存end()的迭代器总会失效。因此,添加或删除元素的程序必须反复调用end() 否则会导致程序是未定义的。
.
.

vector对象是如何增长的

当向vector对象添加元素时,若vector对象没有存储空间来存储,则会分配一个比现在所需要空间更大的存储空间,然后将已有元素移动到新的存储空间并将新元素添加进去,这样就不需要每次添加元素都重新分配存储空间了。

vector有一些管理容量的成员函数这里暂且先不讲…
.
.

额外的string操作

.
string另外三种初始化操作

string s(cp,n); //cp指向的数组的前n个
string s(s2,pos2); //string s2从下标pos2开始
string s(s2,pos2,len2);//string s2从下标pos2开始长为len2

substr操作

string s1 = s.substr(pos,n)//从下标pos开始的n个元素
s.substr()//代表整个字符串
string s2 = "hello world";
string s3 = s2.substr(0,5);//s3 = hello

从一个vector《char》构造string

	vector<char> svec(6, 'a');
	string s(svec.begin(),svec.end());

改变string的其他方法

string的insert和erase除了支持迭代器版本还支持下标版本。 下标表示要删除的开始位置或者要向某一个位置之前插入元素。

	string s,s1;
	const char* cp = "Stately, plump Buck";
	s1 = cp;
	s.assign(cp, 7);
	s.insert(s.size(), cp + 7);
	cout << s <<" "<< s1 << endl;

在这里插入图片描述
从c风格的字符串转化为string直接赋值就好。

还可以将其他string插入到string中

string s = "hello",s1 = "world";
s.insert(0,s1);//在下标0之前插入s1

append和replace函数

string定义了两个额外的成员函数 append和replace函数。
append函数:就是在末尾增加字符串。

	string s1 = "C++ primer";
	s1.append(" 4th");
	s1.insert(s1.size(), " 4th");//两者是等价的

replace函数:对字符串内容进行替换。

	s1.erase(11, 3);
	s1.insert(11, "5th");
	//上下等价
	s1.replace(11, 3, "5th");
	s1.replace(11,3,"Fifth")

这些函数重载接口很多,详见ipad电子版350页,书的324页。

做个小练习:
练习9.43

void trans(string s, string oldVal, string newVal)
{
	size_t sz = oldVal.size();
	for (auto iter = s.begin(); iter + (sz - 1) < s.end(); iter++)
	{
		string temp(iter, iter + sz);
		if (temp == oldVal)
		{
			iter = s.erase(iter, iter + sz);
			iter = s.insert(iter, newVal.begin(), newVal.end());
			iter += newVal.size();
			iter--;
		}
	}
	cout << s << endl;
}

int main()
{
	string s = "i tho na";
	trans(s, "tho", "thought");
}

在这里插入图片描述
练习9.44

void trans_replace(string s, string oldVal, string newVal)
{
	size_t sz = oldVal.size();
	for (size_t i = 0; i + (sz - 1) < s.size(); i++)
	{
		string str = s.substr(i, sz);
		if (str == oldVal)
		{
			s.replace(i, sz, newVal);
			i = i + newVal.size() - 1;
		}
	}
	cout << s << endl;
}

int main()
{
	string s = "i tho na";
	trans_replace(s, "tho", "thought");
}

结果一样。

练习9.45

void addString(string name,string head,string tail)
{
	auto iter = name.begin();
	name.insert(iter, head.begin(), head.end());
	name.append(tail);
	cout << name << endl;
}
int main()
{
	//string s = "i tho na";
	//trans_replace(s, "tho", "thought");
	addString("cuinan", "Mr.", ".lll");
}

在这里插入图片描述
练习9.46

void addString_pos(string name, string head, string tail)
{
	name.insert(0, head);
	name.insert(name.size(), tail);
	cout << name << endl;
}

int main()
{
	addString_pos("cuinan", "Mr.", ".lll");
}

结果一样。
.
.
string搜索操作

string提供了一些搜索函数,这些搜索函数的返回值是size_t,表示匹配发生位置的下标。如果查找失败,返回string::nops

find函数

string name("AnnaBelle");
auto pos1 = name.find("Anna");
//得到的结果为pos1=0; find函数是大小写敏感的。

find_first_of函数
其用来查找与给定字符串中任何一个字符匹配的位置。

string numbers("01234567"),name("r2d2");
auto pos = name.find_first_of(numbers);
//pos的结果为1

find_first_not_of函数
如果是要搜索第一个不在的

string dept("03714p3");
auto pos = dept.find_first_not_of(numbers);
//pos的结果为5
s.find(args);
s.rfind(args);
s.find_first_of(args);
s.find_last_of(args);
s.find_first_not_of(args);
s.find_last_not_of(args);
//它们分别一个是正着找,一个是倒着找。
args可以为
c,pos 从s的位置pos开始查找字符c       pos都默认为0
s2,pos 从s的位置pos开始查找字符串s2
cp,pos 从s的位置pos开始查找c风格字符串
cp,pos,n 从s的位置pos开始查找c风格字符串前n个字符   n默认为0

find系列函数一种常见的编程模式:循环搜索

int main()
{
	string name("cui2nan2"), num("0123456789");
	size_t pos = 0;
	while ((pos = name.find_first_of(num, pos)) != string::npos)
	{
		cout << "find element :" << name[pos] << endl;
		pos++;
	}
}

在这里插入图片描述

compare函数

对string进行比较不仅可以使用关系运算符,而且可以使用compare函数。

s.compare(s1)
若s==s1 返回0
若s>s1 返回正数
若s<s1 返回负数

compare的参数详见电子版353页,书327页

数值转换

新标准引入多个函数,可以实现数值数据与标准库string之间的转换。

int i=42;
string s = to_string(i);
//to_string可以将数值转换为字符串
double d = stod(s);
//stod()将字符串转换为浮点型,这个函数要求第一个非空白符是数值字符
//其他的都一样
//stoi(s,p,b) b表示转换所用的基数默认为10 p用来保存第一个非数值下标,默认不保存

终于终于。。到最后一个了。

容器适配器

栈:
#include《stack》

stack<int> s;
s.pop()//删除栈顶元素,无返回值
s.push(item)//将元素压栈
s.emplace(args)//压栈 由args构造
s.top()//返回栈顶元素,但不出栈

队列:
#include《queue》

queue<int> q;
q.pop()//元素出队 返回void
q.front()//返回首元素 不删除元素
q.back()//返回尾元素 不删除元素
q.push(item);//进队
q.emplace(args);

顺序容器结束!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值