[1] string基础
[1.1] string 的构造
1 #include <iostream> 2 #include <string> 3 4 int main() 5 { 6 using namespace std; 7 8 cout << "1 --- string(const char* s):将string对象初始化为s指向的C风格字符串" << endl; 9 string one("benxintuzi_1"); 10 cout << "one = " << one << endl; // 重载了 << 11 12 cout << "2 --- string(size_type n, char c):以字符c初始化包含n个字符的string对象" << endl; 13 string two(20, ‘$‘); 14 cout << "two = " << two << endl; 15 16 cout << "3 --- string(const string& str):复制构造函数初始化" << endl; 17 string three(one); 18 cout << "three = " << three << endl; 19 20 cout << "4 --- string():默认构造函数" << endl; 21 string four; 22 four += one; // 重载了 += 23 four = two + three; // 重载了 + 24 cout << "four = " << four << endl; 25 26 cout << "5 --- string(const char* s, size_type n):用s指向的C风格字符串的前n个字符初始化string对象" << endl; 27 char s[] = "benxintuzi"; 28 string five(s, 5); 29 cout << "five = " << five << endl; 30 31 cout << "6 --- string(Iter begin, Iter end):用[begin, end)区间内的字符初始化string对象" << endl; 32 string six(s + 2, s + 5); 33 cout << "six = " << six << endl; 34 35 cout << "7 --- string(const string& str, string size_type pos = 0, size_type n = npos):将string对象初始化为str中从pos开始的n个字符" << endl; 36 string seven(four, 5, 2); 37 cout << "seven = " << seven << endl; 38 39 /* 40 cout << "8 --- string(string&& str) noexcept:将string对象初始化为str,并可能修改str,移动构造函数[C++11新特性]" << endl; 41 42 cout << "9 --- string(initializer_list<char>il:将string对象初始化为初始化列表il中的字符[C++11新特性]" << endl; 43 string nine = {‘t‘, ‘u‘, ‘z‘, ‘i‘}; // or string nine{‘t‘, ‘u‘, ‘z‘, ‘i‘}; 44 cout << "nine = " << nine << endl; 45 */ 46 }
[1.2] string 的输入
对于 C 风格字符串,有3种输入方式 |
char info[100]; cin >> info; // 从流中读一个单词存放到info中 cin.getline(info, 100); // 从流中读入一行存放到info,删除流中的\n cin.get(info, 100); // 从流中读入一行存放到info,保留流中的\n |
对于 string 对象,有2种输入方式 |
string stuff; cin >> stuff; // 从流中读取一个单词 getline(cin, stuff); // 从流中读取一行,删除流中的\n |
getline可以指定用于分割单词的分隔符,将其放到第三个参数中,如:
cin.getline(info, 100, ‘:’); // C 风格字符串使用这种形式
getline(stuff, ‘:’); // string 类使用这种形式
string与传统 C 风格字符串的比较:
使用string类输入时,不需要具体指定输入的字符个数,其可以自动匹配字符串大小,使用C风格字符串必须显示指定,因为C风格字符串使用的是istream类的方法,而string版本使用的是独立的函数,因此形式上有所区别,这种区别在其他运算符上也有体现,如>>操作符:
C 风格字符串使用:cin.operator>>(fname),而string风格为:operator(cin, fname);
说明:
string版本的getline()在如下三种情况下结束读取:
1 到达文件末尾:此时输入流中的eofbit置位;
2 遇到分割符:默认为\n,此时,删除流中的\n;
3 读取的字符个数达到最大值[min(string::npos, 空闲内存)],此时将输入流的failbit置位。
在输入流中有一个统计系统,用于跟踪流的状态:
1 检测到文件末尾,设置eofbit寄存器;
2 检测到错误,设置failbit寄存器;
3 检测到无法识别的故障,设置badbit寄存器;
4 检测到一切顺利,设置goodbit寄存器。
string 类对全部 6 个关系运算符进行了重载,对于每个关系运算符,又进行了3种重载,分别是:
string 对象与 string 对象;
string 对象 C 风格字符串;
C 风格字符串与 string 对象。
[2] 智能指针(smart pointer)
智能指针是行为类似于指针,但却是类。其可以帮助管理动态分配的内存,有三种可选:分别为auto_ptr\unique_ptr\shared_ptr。auto_ptr是C++ 98提供的,C++ 11已经抛弃了(虽然已经被抛弃,但是仍然被大量使用)。
小知识: 为何新标准要放弃auto_ptr?理由如下: 假设如下赋值: auto_ptr<string> ps1(new string(“benxintuzi”)); auto_ptr<string> ps2; ps2 = ps1; 如果ps2和ps1是普通指针,那么他们将同时指向一块堆内存,那么auto_ptr类型的指针若同时指向一块堆内存,那么就会调用两次delete,这个太可怕了,因此,auto_ptr采用的策略是如果发生赋值,那么就会令ps1为0,同样unique_ptr也是采用这种策略,但是更加严格。 如下,当ps2 = ps1,即ps1置空时,如果发生调用*ps1,那么相当于调用了一个空指针指向的对象,在auto_ptr情况下,可以编译通过,在运行时会出错;但在unique_ptr情况下,不会让你通过编译的。 结论就是unique_ptr比auto_ptr更安全一些。
shared_ptr对于每个堆对象,其维护一个指向同一对象的引用计数,只有当引用计数为0时才调用delete,因此可以很好地支持智能指针赋值操作。 |
要创建智能指针,必须包含头文件memory,然后使用模板语法实例化所需类型的指针,例如auto_ptr包含如下构造函数:
template<class X> class auto_ptr
{
public:
// throw()意味着不引发异常【C++ 11也将throw()抛弃了】
explicit auto_ptr(X* p = 0) throw();
...
}
因此,智能指针的使用非常简单,只需要用特定类型的指针初始化即可,如:
auto_ptr<double> pd(new double);
说明:
智能指针都放在名称空间std中(注意shared_ptr和unique_ptr都是C++ 11新增的,旧时的编译器可能不支持)。
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 class Report 6 { 7 public: 8 Report(const std::string s) : str(s) 9 { 10 std::cout << "Object created!" << std::endl; 11 } 12 ~Report() 13 { 14 std::cout << "Object deleted!" << std::endl; 15 } 16 void comment() const 17 { 18 std::cout << "str = " << str << std::endl; 19 } 20 21 private: 22 std::string str; 23 }; 24 25 int main() 26 { 27 std::auto_ptr<Report> pa(new Report("using auto_ptr")); 28 pa->comment(); 29 30 std::shared_ptr<Report> ps(new Report("using shared_ptr")); 31 ps->comment(); 32 33 std::unique_ptr<Report> pu(new Report("using unique_ptr")); 34 pu->comment(); 35 36 return 0; 37 } 38 39 /** output */ 40 Object created! 41 str = using auto_ptr 42 Object created! 43 str = using shared_ptr 44 Object created! 45 str = using unique_ptr 46 Object deleted! 47 Object deleted! 48 Object deleted!
注意:
绝对不要将非堆内存指针赋予智能指针,否则虽然可以通过编译,但是情况并非总是乐观的,这就相当于delete了非堆内存,容易引发难以发现的问题。
关于如何选择合适的智能指针,如下给出参考建议:
如果需要多个指向同一堆对象的有效指针,那么选择使用shared_ptr。这种情况包括:
1 有一个指针数组,并且使用一些辅助指针标识特殊元素,如标识最值元素;
2 两个对象中都包含指向第三个对象的指针;
3 STL容器中的指针等,很多STL算法都支持复制和赋值操作。
如果程序不需要多个有效指针同时指向同一堆对象,那么使用unique_ptr。如果需要将unique_ptr作为右值时,可将其赋值给shared_ptr;在满足unique_ptr条件时,也可使用auto_ptr,但unique_ptr似乎是更好的选择(如果编译器不提供unique_ptr,可以使用boost库提供的scoped_ptr,其功能与unique_ptr类似)。
[3] 迭代器
[3.1] 迭代器基础
迭代器就是广义的指针,可对其进行递增操作和解引用操作等的对象。每个容器类都定义了一个与之相关的迭代器,该迭代器是一个名为iterator的typedef定义,作用域为整个类。模板使算法独立于数据类型,而迭代器使算法独立于容器类型。
迭代器主要用于遍历容器中的元素,其应该具有如下基本功能:
1 支持解引用操作
2 赋值操作
3 比较操作,如 ==、!=
4 自增操作
具备以上能力就差不多了。其实STL按照迭代器功能强弱定义了多种级别(5种)的迭代器,说明如下:
迭代器类型 |
说明 |
输入迭代器 |
【读容器】可来读取容器中的元素,但可能不会修改容器中的元素。输入迭代器必须可以访问容器中的所有元素,因此,支持++操作。 |
输出迭代器 |
【写容器】程序可以修改容器中的元素值,但不能读取容器中的元素。 |
正向迭代器 |
【读或者写容器】单向读写。 |
双向迭代器 |
【双向读或者写容器】双向读写。支持自减运算符。 |
随机访问迭代器 |
【双向读或者写容器】提供了附加的“跳步”访问能力。 |
迭代器支持的操作含义:
表达式 |
说明 |
a + n、n + a |
指向a所指元素后的第n个元素 |
a - n |
指向a所指元素前的第n个元素 |
r += n、r -= n |
r = r + n、r = r – n |
a[n] |
*(a + n) |
b - a |
a ~ b区间的元素个数 |
a < b、a > b、a >= b、a <= b |
逻辑判断 |
STL迭代器功能汇总:
总结:
迭代器支持的通常操作为解引用读或者写;所有类型的迭代器都支持自增操作;自减操作只有双向迭代器和随机访问迭代器支持;随机访问迭代器为“跳步”访问提供了可能。因此迭代器能力大小可以表示如下:
随机访问迭代器 > 双向迭代器 > 正向迭代器 > 输入迭代器 == 输出迭代器
[3.2] 迭代器进阶
迭代器是广义的指针,而指针满足所有迭代器的要求。因此STL算法可以使用指针来对基于指针的非STL容器进行操作。例如,可将STL算法用于数组:
如果要将一个double Receipts[100]进行排序,可以使用STL的算法sort,但传入的确是指针,如:
sort(Receipts, Receipts + 100);
STL提供了一些预定义的迭代器:copy()、ostream_iterator、istream_iterator。
假设有如下定义:
int casts[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> dice(10);
copy(casts, casts + 10, dice.begin()); // 将[casts, casts + 10)范围内的数据复制到dice.begin()开始的目标空间中
输出流迭代器:
假设现在需要将数据复制到显示器上,那么需要一个表示输出流的迭代器,STL为我们提供了ostream_iterator模板。该迭代器是一个适配器,可将所有的接口转换为STL使用的接口。我们使用时必须包含头文件iterator,并且做出如下声明来创建该迭代器:
#include <iterator>
...
ostream_iterator<int, char> out_iter(cout, “ ”);
说明:
int: 表示发送到输出流中的数据类型;
char: 表示输出流使用的字符类型;
构造函数中,cout: 表示要使用的输出流;“ ”:表示所发送数据的分隔符;
可以这样使用迭代器:
*out_iter++ = 15; // 等价于cout << 15 << “ ”;
这条语句表明将”15 ”赋予指针指向的位置,然后指针向前移动一个单位。
实现如上的copy动作为:
copy(dice.begin(), dice.end(), out_iter)即可将dice容器的整个区间复制到显示器中显示。
当然也可以创建匿名的迭代器,如下:
copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, “ ”));
输入流迭代器:
对于的输入流迭代器为istream_iter模板:
copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), ostream_iterator<int, char>(cout, “ ”));
说明:
int:要读取的数据类型;
char:输入流使用的数据类型;
构造函数中,第一个参数cin表示读取由cin管理的输入流;省略构造函数意味着输入失败,因此上述代码从输入流中读取int型数据,直到文件末尾、类型不匹配或者出现其他输入故障为止才输出到屏幕上。
1 #include <iostream> 2 #include <iterator> 3 using namespace std; 4 5 int main() 6 { 7 copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), 8 ostream_iterator<int, char>(cout, " ")); 9 return 0; 10 } 11 12 /** output */ 13 1 2 3 4 5 14 1 2 3 4 5 __
除了istream_iterator和ostream_iterator外,头文件iterator中还提供了一些专用的迭代器,如:reverse_iterator、back_insert_iterator、front_insert_iterator和insert_iterator。
reverse_iterator执行递增时,指针实际上发生递减操作。例如:vector类中的rbegin()和rend()分别返回指向最后一个元素下一位置的指针、指向第一个元素的指针。
强调:
虽然rbegin()和end()返回的指针虽然指向同一个位置,但是类型不同,前者类型是reverse_iterator,后者类型是iterator。
同样要注意的是:对于反向迭代的解引用的具体实现实则是先递减,再解引用。否则会出错。试想一下,如果一个rp是rbegin()返回的,直接解引用将导致操作一个空指针。
1 #include <iostream> 2 #include <iterator> 3 #include <vector> 4 using namespace std; 5 6 /* 7 int main() 8 { 9 copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), 10 ostream_iterator<int, char>(cout, " ")); 11 return 0; 12 } 13 */ 14 15 int main() 16 { 17 int casts[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 18 vector<int> dice(10); 19 copy(casts, casts + 5, dice.begin()); // 将casts的前5个数据复制到dice中 20 21 ostream_iterator<int, char> out_iter(cout, " "); // 创建输出流迭代器 22 copy(dice.begin(), dice.end(), out_iter); // 将dice中的全部元素复制到屏幕上输出 23 24 cout << endl; 25 26 /* 隐式使用reverse_iterator */ 27 copy(dice.rbegin(), dice.rend(), out_iter); // 将dice中的全部元素反向输出到屏幕上 28 29 cout << endl; 30 31 /* 显式使用reverse_iterator */ 32 vector<int>::reverse_iterator ri; 33 for(ri = dice.rbegin(); ri != dice.rend(); ++ri) 34 cout << *ri << " : "; 35 cout << endl; 36 37 return 0; 38 } 39 40 /** output */ 41 1 2 3 4 5 0 0 0 0 0 42 0 0 0 0 0 5 4 3 2 1 43 0 : 0 : 0 : 0 : 0 : 5 : 4 : 3 : 2 : 1 :
在形式上,STL中的很多算法都和copy函数类似,如下为三种插入迭代器操作:
back_insert_iterator将元素插入容器尾部;
front_insert_iterator将元素插入容器头部;
insert_iterator将元素插入指定位置。
插入操作相比于copy操作而言,可以自动增加空间,而copy确是静态分配好的空间,即使不够了也不会向操作系统去申请,默认情况下会截断后边的数据,只复制容量允许的数据。使用插入迭代器时,使用容器类型作为模板参数,使用容器变量作为构造参数,如下所述:
back_insert_iterator<vector<int>> back_iter(dice);
对于insert_iterator,还需要一个指定插入位置的构造参数:
insert_iterator<vector<int>> insert_iter(dice, dice.begin());
[4]容器类
容器用于存储对象,其要求所存储的对象必须具有相同的类型,而且该类型必须是可复制构造和可赋值的。一般基本类型和类类型都满足要求(当然除非你把这两种函数声明为私有或保护类型的)。C++ 11中又添加了可复制插入和可移动插入要求。
容器类型是用于创建具体容器的模板。之前共有11个容器类型:deque/list/queue/priority_queue/stack/vector/map/multimap/set/multiset/biset,C++ 11新增了5个:forward_list/unordered_map/unordered_multimap/unordered_set/unordered_multiset。
何为序列?
序列保证了元素将按特定的顺序排列,不会在两次迭代之间发生变化。序列还要求其元素严格按线性顺序排列,即存在第一个/第二个/.../等。比如数组和链表都是序列,但分支结构就不是序列。
由于序列中的元素具有特定的顺序,因此可以执行像插入元素到特定位置、删除特定区间等操作。如下容器都为序列容器:
vector |
vector是数组的一种类的表示,它提供了自动管理内存的功能,可以动态改变vector的长度,增大或减小。vector是可以反转的,主要是通过rbegin()和rend()实现的,并且其返回的迭代器类型都是reverse_iterator。 |
deque |
双端队列。其实现类似于vector,支持随机访问。主要区别在于可以从deque中开头和结尾处插入和删除元素,而且其时间复杂度固定。 |
list |
双向链表。与vector类似,list也可以反转。主要区别是:list不支持随机访问。除此之外,list模板类还包括了链表专用的成员函数,如下所示: void merge(list<T, Alloc>& x): 将链表x与调用链表合并,并且已然有序。合并后的链表保存在调用链表中,x为空。 void remove(const T& val): 从链表中删除val的所有实例。 void sort(): 使用<运算符对链表进行排序,时间复杂度为nlgn。 void splice(iterator pos, list<T, Alloc>x): 将链表x的内容插入到pos前边,插入后x为空。 void unique(): 将链表中连续的相同元素压缩为单个元素。 说明: insert()和splice()之间的主要区别在于:insert是插入原始区间的副本到目标地址,而splice是将原始区间转移到目标地址。 C++ 11新增了容器类forward_list,实现了单链表,与之关联的迭代器是正向迭代器而非双向迭代器。 |
queue |
queue模板类是一个适配器类,如前所述,ostream_iterator模板也是一个适配器,可以让输出流使用迭代器接口,同样,queue模板类让底层类(默认为deque)展示出队列接口。 queue模板的限制比deque更多。他不仅不允许随机访问,而且不允许遍历操作。其把使用限制在队列基本操作上,如入队、出队、取队首、判队空等,具体如下: bool empty() const: 如果队列为空则返回true;否则返回false。 size_type size() const: 返回队列中元素的数目。 T& front(): 返回指向队首元素的引用。 T& back(): 返回指向队尾元素的引用。 void push(const T& x): 在队尾插入x。 void pop(): 删除队首元素。 说明: priority_queue与queue的主要区别在于:最大元素被移到队首。其内部实现为vector,可以指定内部元素排序规则。 |
stack |
适配器类,使用vector实现,支持操作如下: bool empty() const: 如果栈为空则返回true;否则返回false。 size_type size() const: 返回栈中元素的数目。 T& top(): 返回指向栈顶元素的引用。 void push(const T& x): 在栈顶插入x。 void pop(): 删除栈顶元素。 |
array(C++ 11 新增) |
模板类array并非STL容器,因为其长度是固定的。因此不能使用动态调整容器大小的函数如push_back()和insert()等,但是其定义了很有用的成员函数,如operator[]()和at(),正如之前所述,许多标准的STL算法也可用于array对象,如:copy()和for_each()。 |
操作示例:
1 #include <iostream> 2 #include <list> 3 #include <iterator> 4 #include <algorithm> 5 using namespace std; 6 7 void PrintList(int n){ cout << n << " "; } 8 9 int main() 10 { 11 list<int> one(5, 2); // 5个2 12 cout << "list one: "; 13 for_each(one.begin(), one.end(), PrintList); 14 cout << endl; 15 16 list<int> two; 17 int stuff[3] = {2, 5, 9}; 18 two.insert(two.begin(), stuff, stuff + 3); 19 cout << "list two: "; 20 for_each(two.begin(), two.end(), PrintList); 21 cout << endl; 22 23 list<int> three(two); 24 three.insert(three.end(), one.begin(), one.end()); 25 cout << "list three: "; 26 for_each(three.begin(), three.end(), PrintList); 27 cout << endl; 28 29 cout << "merge one and three: "; 30 three.splice(three.begin(), one); 31 for_each(three.begin(), three.end(), PrintList); 32 cout << endl; 33 34 cout << "delete the duplicate elements: "; 35 three.unique(); 36 for_each(three.begin(), three.end(), PrintList); 37 cout << endl; 38 39 cout << "merge with another: four = "; 40 list<int> four(3, 8); 41 four.merge(three); 42 for_each(four.begin(), four.end(), PrintList); 43 cout << endl; 44 45 cout << "remove the value 8 in four: "; 46 four.remove(8); 47 for_each(four.begin(), four.end(), PrintList); 48 cout << endl; 49 50 return 0; 51 } 52 53 /** output */ 54 list one: 2 2 2 2 2 55 list two: 2 5 9 56 list three: 2 5 9 2 2 2 2 2 57 merge one and three: 2 2 2 2 2 2 5 9 2 2 2 2 2 58 delete the duplicate elements: 2 5 9 2 59 merge with another: four = 2 5 8 8 8 9 2 60 remove the value 8 in four: 2 5 9 2