第 16 章 string类和标准模板库
第 16 章 string类和标准模板库
16.1 string类
C语言在 string.h(C++中为cstring)提供了一系列的字符串函数。
16.1.1 构造字符串
string
实际上是模板具体化 basic_string<char>
的一个 typedef
,同时省略了与内存管理相关的参数。size_type
依赖于实现的整型,是在头文件 string
中定义的。
string类
将string::npos
定义为字符串的最大长度,通常为unsigned int
的最大值。- NBTS来表示以空字符结束的字符串----传统的C字符串
构造函数 | 描述 |
---|---|
string(const char *s) | 将string对象初始化为s指向的NBTS |
string(size_type n,char c) | 创建一个包含n个元素的string对象,其中每个元素都被初始化为字符c |
string(const string &str) | 将一个string对象初始化为string对象str(拷贝构造函数) |
string(const char *s,size_type n) | 将string对象初始化为s指向的NBTS的前n个字符,即使超过结尾 |
template< class Iter> string(Iter begin,Iter end) | 将string对象初始化为区间[begin,end)内的字符,begin与end的行为就像指针(常用迭代器),用于指定位置,范围包括begin在内,但不包括end |
stirng(const string &str,string size_type pos = 0,size_type n = npos) | 将一个string对象初始化为对象str中从位置pos开始到结尾的字符,或从位置pos开始的n个字符 |
string(string &&str) noexcept | 这是C++11新增的,它将一个string对象初始化为string对象str,并可能修改str |
string(initializer_list< char > il) | 它将一个string对象初始化为初始化列表il中的字符 |
阐述一下新增的两个函数的用法:
- string(string &&str) noexcept
构造函数string(string &&str)类似于拷贝构造函数,导致新创建的string为str的副本。但是它与普通的构造函数不同的是,它不保证将str视为const(这就意味着可以修改作为参数的str的原型)。
2、string(initializer_list< char > il)
string piano_man = {'L','i','s','z','t'};
string comp_lang{'L','i','s','z','t'};
下面的程序运用了大部分上述的构造函数:
#include <iostream>
#include <string>int main() {using namespace std;string one("Lottery Winner!");cout << "string one: " << one << endl;string two(20, '$');cout << "string two: " << two << endl;string three(one);cout << "string three: " << three << endl;one += " Oops!"; //overloaded +=cout << "string newOne: " << one << endl;two = "Sorry! That was ";three[0] = 'P';string four;four = two + three; //overloaded +,=cout << "string four: " << four << endl;char alls[] = "string five: All's well that ends well";string five(alls, 38);cout << five << "!\n";string six(alls + 6, alls + 10);cout << "string six: " << six << endl;string seven(&five[6], &five[10]);cout << "string seven: " << seven << "....\n";string eight(four, 7, 16);cout << "string eight: "<< eight << " in motion!" << endl;return 0;
}string one: Lottery Winner!
string two: $$$$$$$$$$$$$$$$$$$$
string three: Lottery Winner!
string newOne: Lottery Winner! Oops!
string four: Sorry! That was Pottery Winner!
string five: All's well that ends well!
string six: fiv
string seven: fiv....
string eight: That was Pottery in motion!
分析一下line24的用法:
它用的是这个构造函数:
template< class Iter>
string(Iter begin,Iter end)
由于对象名不同于数组名,不会被看作是对象的地址,因此five不是指针,所以five+6是不管用的。然而,five[6]是一个char值,所以&five[6]是一个地址,因此可被用作该构造函数的一个参数。
16.1.2 string类输入
在C-风格字符串,有3种方式:
char info[100];
cin >> info; // 读取字符
cin.getline(info,100); // 整行读取,舍弃 \n
cin.get(info,100); // 整行读取,保留 \n
对于string对象,有2种方式:
string stuff;
cin >> stuff; // 读取字符
getline(cin,stuff); // 读取整行内容,舍弃\n
两个版本的getline() 都有一个可选参数,用于指定使用某个字符来确定输入的边界。
// 读到 : 就舍弃 :
cin.getline(info,100,':');
getline(stuff,':');
对于string版本的getline() 函数从输入读取字符
,并将其存储到string对象
中,直到发生三种情况之一:
- 读到文件尾
- 输入流的
eofbit
将被设置,其对应的方法fail()
和eof()
都会返回true。
- 输入流的
- 遇到分界字符(默认是\n)
- 删除分界符,不存储
- 读取的字符数达到最大允许值(string::nops 和可供分配的内存字节数中较小的一个)
- 设置输入流中的 eofbit,让其方法 fail() 返回 true。
输入流对象有一个统计系统
,用于跟踪流的错误状态。
- 系统中如果检测到
文件尾
后将设置eofbit寄存器
- 检测到
输入错误
时将设置failbit寄存器
- 出现
无法识别的故障
时将设置badbit寄存器
。 一切顺利
则设置goodbit寄存器
。
下面程序是一个从文件中读取字符串的简短示例,它假设文件中包含冒号字符分隔的字符串,并使用指定分界符的getline()方法,然后,显示字符串并给它们编号,每个字符串占一行
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
int main(){using namespace std;ifstream fin;fin.open("/Users/mac/CLionProjects/test/tobuy,txt");if(fin.is_open() == false){cerr << "Can't open file. Bye.\n";exit(EXIT_FAILURE);}string item;int count = 0;getline(fin,item,':');while (fin) //while input is good{++count;cout << count << ": " << item << endl;getline(fin,item,':');}cout << "Done\n";fin.close();return 0;
}1: sardines
2: chocolate ice cream
3: pop corn
4: leeks
5:
cottage cheese
6: olive oil
7: butter
8: tofu
Done
补充tobuy.txt的内容:
sardines:chocolate ice cream:pop corn:leeks:
cottage cheese:olive oil:butter:tofu:
因为指定 :作为分界字符,换行符将被视为常规字符。所以文件内的第一行末尾的换行符将成为包含“cottage cheese”的字符串的第一个字符,所以看line32,先换行再输出字符串。
16.1.3 使用字符串
string类对象对全部 6个关系运算符都进行了重载。
- 如果在机器序列中,一个对象位于另一个对象的前面,则前者小于后者
- 如果机器序列为ASCII码,则小写数字大于大写字符。
通过确定字符串的长度
来比较大小,使用 size()
和 length()
成员函数返回字符串中的字符数
。
if (snake1.length() == snake2.size())cout << "both strings have the same length \n";
在字符串中搜索给定的子字符串或字符。
方法原型 | 方法描述 |
---|---|
size_type find(const string &str, size_type pos = 0) const | 从字符串的pos位置开始,查找子字符串str,如果找到,则返回子字符串首次出现时其首字符的索引,否则返回 string::npos |
size_type find(const char * s,size_type pos = 0)const | 从字符串的pos位置开始,查找子字符串s,如果找到,则返回子字符串首次出现时其首字符的索引,否则返回 string::npos |
size_type find(const char *s,size_type pos = 0,size_type n) | 从字符串的pos位置开始,查找s的前n个字符组成的子字符串,如果找到,则返回子字符串首次出现时其首字符的索引,否则返回 string::npos |
size_type find(char ch, size_type pos = 0)const |
从字符串的pos位置开始,查找字符串ch,如果找到,则返回字符出现的位置 ,否则返回 string::npos
|
string库提供了相关的方法(其重载函数特征标都与find()
方法相同。):
rfind()
:查找子字符串或字符最后一次出现的位置。find_first_of()
:在字符串中查找参数中任何一个字符首次出现
的位置。string snake1 = "cobra"; int where = snake1.find_first_of("hark");
where的值为3,因为r属于“hark”的字符,并在snake1中首次出现。
find_last_of()
:在字符串中查找参数中任何一个字符最后一次出现
的位置。string snake1 = "cobra"; int where = snake1.last_first_of("hark");
where的值为4,因为a属于"hark"中的字符,并在snake1中最后出现
find_first_not_of()
:在字符串中查找不在参数
中的字符第一次出现
的位置。string snake1 = "cobra"; int where = snake.find_first_not_of("hark");
where的值为0,因为c是第一个不包含在参数中的字符。
find_last_not_of()
:在字符串中查找不在参数
中的字符最后一次出现
的位置。string snake1 = "cobra"; int where = snake.find_first_not_of("hark");
where的值为2,因为b不在参数“hark”中,并且是最后出现的。
+=运算符
的某个重载版本使得能够将一个字符
附加到字符串
中。
下面设置一个游戏规则:创建一个非图形版本的Hangman拼字游戏。该游戏将一系列的单词存储在一个string对象数组中,然后随机选择一个单词,让人猜测单词的字母。如果猜错6次,玩家就输了。该程序使用find()函数来检查玩家的猜测,使用+=运算符创建一个string对象来记录玩家的错误猜测。为记录玩家猜对的情况,程序创建一个单词,其长度与被猜的单词相同,但包含的是连字符。玩家猜对字符时,将用该字符替换相应的连字符。
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cctype>using std::string;
const int NUM = 26;
const string wordlist[NUM] = {"apiary", "beetle", "cereal", "danger", "ensign","florid", "garage", "health", "insult", "jackal", "keeper","loaner", "manage", "nonce", "onset", "plaid", "quilt", "remote","stolid", "train", "useful", "valid", "whence", "xenon", "yearn", "zippy"};int main() {using std::cout;using std::cin;using std::tolower;using std::endl;std::srand(std::time(0));char play;cout << "Will you play a word game?<y/n>";cin >> play;play = tolower(play);while (play == 'y') {string target = wordlist[std::rand() % NUM];int length = target.size();string attemp(length, '-');string badchars;int guesses = 6;cout << "Guess my scret word.It has " << length<< " letters, and you guess\n"<< "one letter at a time.You get " << guesses<< " wrong guesses.\n";cout << "Your word: " << attemp << endl;while (guesses > 0 && attemp != target) {char letter;cout << "Guess a letter: ";cin >> letter;if (badchars.find(letter) != string::npos || attemp.find(letter) != string::npos) {cout << "You already guessed that.Try again.\n";continue;}int loc = target.find(letter);if (loc == string::npos) {cout << "Oh,bad guess!\n";--guesses;badchars += letter; //add to string} else {cout << "Good guess!\n";attemp[loc] = letter;//check if letter appears again//将猜对的字母所对应的位置上的'-'均替换为所猜对的字母loc = target.find(letter, loc + 1);while (loc != string::npos) {attemp[loc] = letter;loc = target.find(letter, loc + 1);}}cout << "Your words: " << attemp << endl;if (attemp != target) {if (badchars.size() > 0) {cout << "Bad choices: " << badchars << endl;}cout << guesses << " bad guesses left\n";}}if (guesses > 0) {cout << "That's right!\n";} else {cout << "Sorry,the word is " << target << ".\n";cout << "Will you play another?<y/n>";cin >> play;play = tolower(play);}}cout << "Bye\n";return 0;
}输出:
Will you play a word game?<y/n>y
Guess my scret word.It has 6 letters, and you guess
one letter at a time.You get 6 wrong guesses.
Your word: ------
Guess a letter: q
Oh,bad guess!
Your words: ------
Bad choices: q
5 bad guesses left
Guess a letter: t
Oh,bad guess!
Your words: ------
Bad choices: qt
4 bad guesses left
Guess a letter: g
Oh,bad guess!
Your words: ------
Bad choices: qtg
3 bad guesses left
Guess a letter: s
Good guess!
Your words: -s----
Bad choices: qtg
3 bad guesses left
Guess a letter: b
Oh,bad guess!
Your words: -s----
Bad choices: qtgb
2 bad guesses left
Guess a letter: q
You already guessed that.Try again.
Guess a letter: z
Oh,bad guess!
Your words: -s----
Bad choices: qtgbz
1 bad guesses left
Guess a letter: h
Oh,bad guess!
Your words: -s----
Bad choices: qtgbzh
0 bad guesses left
Sorry,the word is useful.
Will you play another?<y/n>n
Bye
这个还挺好玩的!
16.1.4 字符串的额外函数方法
c++通常会实现分配一个比实际字符串大的内存快,为字符串提供了增大空间。当字符串不断增大的同时,超过了内存块的大小,程序将分配一个大小为原来两倍的内存块的大小。
capacity()
:返回当前分配给字符串的内存块的大小。reserve()
:能够请求内存块的最小长度。(注意⚠️:不是最终长度)
下面的示例演示上面的这两种方法:
#include <iostream>
#include <string>int main(){using namespace std;string empty;string small = "bit";string larger = "Elephants are a girl's best friend";cout << "Sizes:\n";cout << "\tempty: " << empty.size() << endl;cout << "\tsmall: " << small.size() << endl;cout << "\tlarger: " << larger.size() << endl;cout << "Capacities:\n";cout << "\tempty: " << empty.capacity() << endl;cout << "\tsmall: " << small.capacity() << endl;cout << "\tlarger: " << larger.capacity() << endl;empty.reserve(50);cout << "Capacity after empty.reserve(50): "<< empty.capacity() << endl;return 0;
}输出:
Sizes:empty: 0small: 3larger: 34
Capacities:empty: 22small: 22larger: 47
Capacity after empty.reserve(50): 63
1.5 字符串种类
string类看作是基于char类型模板的具体化
// Allocator 是管理内存分配的类,对于各种字符类型,都有预定义的allocator模板具体化,都是默认的,使用new和delete。
template <class charT, class traits = char _traits<charT>,class Allocator = allocator<charT> >
basic_string {...};
模板 basic_string 有4个具体化,每一具体化都有一个 typedef名称:
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string; // C++11
typedef basic_string<char32_t> u32string; // C++11
16.2 智能指针模板类型
智能指针是行为类似于指针的类对象
。
目的:将开发者将申请未释放的内存回收
,从而避免内存泄漏
。
16.2.1 使用智能指针
智能指针类一共有3种:
auto_ptr
unique_ptr
shared_ptr
通过new
获得(直接或间接)的地址
赋给对象
。当智能指针过期时,其析构函数
将使用delete
来释放内存
。
如果将new
返回的地址赋给对象后,无需关注释放内存。当智能指针过期时,内存将自动释放
。
auto_ptr
和常规指针
在行为方面的差别:share_ptr
和 unique_ptr
的行为 和 auto_ptr
相同。
使用常规指针实现:
void remodel(std::string &str)
{std::string * ps = new std::string(str);...if(weird_thing())throw exception();str = *ps;// 如果出现异常,delete不会被执行,会导致内存泄漏delete ps;return;
}
使用智能指针的3个步骤:
- 包含
头文件memory
- 将指向
string的指针
替换为指向string的智能指针对象
- 删除
delete
语句
所以将remodel()函数使用智能指针重写为:
#include <memory>
void remodel(std::string & str)
{std::auto_ptr <std::string> ps (new std::string(str));...if (weird_thing())throw exception();str = *ps;// 不需要delete ps;语句return;
}
下面程序演示了如何使用三种智能指针。
#include <iostream>
#include <string>
#include <memory>class Report{
private:std::string str;
public:Report(const std::string s) : str(s){std::cout << "Object created!\n";}~Report(){std::cout << "Object deleted!\n";}void comment()const{std::cout << str << "\n";}
};int main(){{std::auto_ptr<Report> ps (new Report("using auto_ptr"));ps->comment();}std::cout << std::endl;{std::shared_ptr<Report> ps(new Report("using shared_ptr"));ps->comment();}std::cout << std::endl;{std::unique_ptr<Report> ps (new Report("using unique_ptr"));ps->comment();}return 0;
}输出:
Object created!
using auto_ptr
Object deleted!Object created!
using shared_ptr
Object deleted!Object created!
using unique_ptr
Object deleted!
⚠️:所有智能指针类都是一个explicit构造函数,该构造函数将指针作为参数。因此不需要自动转换为智能指针对象。
shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; //not allowed
pd = shared_ptr<double>(p_reg); //allowed(explicit conversion)
shared_ptr<double> pshared = p_reg; //allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); //allowed (explicit conversion)
避雷⚠️
string vacation("I wandered lonely as a child.");
shared_ptr<string> pvac(&vacation);
pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。(因为string并不是动态分配的,固然是放在栈上,而不是放在堆上)
16.2.2 智能指针的注意事项
首先来看一个程序段
auto_ptr<stirng>ps (new string("I reigned lonely as a cloud"));
auto_prt<string> vocation;
vocation = ps;
如果ps和vocation是常规指针,则两个指针将会指向同一个string对象,程序会试图删除一个对象两次,我们要尽可能地避免这种错误的发生,通常方法有三:
- 定义赋值运算符,使之执行深拷贝(在其中再次动态分配内存,将指向移到别处),这样两个指针将指向不同的对象。
- 建立所有权概念,对于特定的对象,只能有一个智能指针可拥有它。
- 创建智能更高的指针,跟踪引用特定对象的智能指针数,这称为引用计数。(‼️仅当最后的指针过期时,才调用delete)
‼️注意‼️:
auto_ptr
和unique_ptr
都是采用所有权模型。unique_ptr
时,程序不会等到运行阶段崩溃,而是在编译器阶段就出现错误。- 使用引用计数的shared_ptr是绝对稳定安全。
// auto_ptr 会在运行阶段出现 segmentation fault (core dumped)
auto_ptr<string> p1 (new string("auto"));
auto_ptr<string p2>;
p2 = p1; // p2会接管string对象的所有权后,p1的所有权会被剥夺// unique_ptr 会在编译阶段出现 segmentation fault (core dumped)
unique_ptr<string> p3 (new string("unique"));
unique_ptr<string p4>;
p4 = p3;
所以 unique_ptr相对比auto_ptr更加安全。可以直接就在编译阶段解决在运行阶段会出现的潜在问题。
下面程序是一个不适用使用auto_ptr的示例:
#include <iostream>
#include <string>
#include <memory>int main() {using namespace std;auto_ptr<string> films[5] = {auto_ptr<string>(new string("Fowl Balls")),auto_ptr<string>(new string("Duck Walks")),auto_ptr<string>(new string("Chicken Runs")),auto_ptr<string>(new string("Turkey Errors")),auto_ptr<string>(new string("Goose Eggs"))};auto_ptr<string> pwin;pwin = films[2]; //films[2] loses ownership;//最佳鸟类棒球电影的提名是cout << "The nominees for best avian baseball film are\n";for (int i = 0; i < 5; ++i) {cout << *films[i] << endl;}cout << "The winner is " << *pwin << "!\n";cin.get();return 0;
}输出:
The nominees for best avian baseball film are
Fowl Balls
Duck Walks进程已结束,退出代码为 139 (interrupted by signal 11: SIGSEGV)
程序无法按照预期执行的缘由为:
pwin = films[2]; //films[2] loses ownership;
这导致了files[2]不再引用该字符串,在auto_ptr放弃对象的所有权后,便可能使用它来访问该对象。当程序打印films[2]指向的字符串时,却发现这是一个空指针。
使用share_ptr可以有效地解决这个问题。
share_ptr<string> pwin;
pwin = files[2];
pwin和films[2]指向同一个对象,但不会出现像auto_ptr那样对对象进行争夺的情形。引用计数从1加到2。在程序末尾,后声明的pwin首先调用其析构函数,该析构函数将引用计数降低到1。然后,shared_ptr数组成员被释放,对films[2]调用析构函数,引用计数降低到0,调用delete来释放之前动态分配的内存。
16.2.3 unique_ptr维和由于auto_ptr
看看下面的语句:
auto_ptr<string> p1(new string("auto"));
auto_ptr<string> p2;
p2 = p1;
⚠️:p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是件好事,可防止p1和p2的析构函数试图删除同一个对象;但如果程序随后试图使用p1,这将是一件坏事,因为p1不再指向有效的数组。
下面看一下unique_ptr:
unique_ptr<string> p3(new string("auto"));
unique_ptr<string> p4;
p4 = p3;
✅编译器会认为line3是非法的,这样避免了auto_ptr所出现的问题,更为安全。
有时候可以将一个智能指针赋值给另一个并不会留下危险的悬挂指针。
unique_ptr<string> demo(const char*s){unique_ptr<string> temp(new string(s));return temp;
}uniuqe_ptr<string> ps;
ps = demo("Uniquely special");
这里demo()返回了一个临时的unique_ptr,然后ps接管了原本归所返回的unique_ptr所有的对象,而返回的unique_ptr被销毁,这样没有机会使用它来访问无效的数据。(故以上的赋值是正确的)
但是不可以将一个智能指针赋值给另一个悬挂指针。
using namespace std;
unique_ptr<string> pu1(new string("Hi,ho!"));
unique_ptr<string> pu2;
pu2 = pu1;
line4是不被允许的,因为其会留下悬挂的unique_ptr(pu1);
如果想要把line4盘活也不是没有办法,需要使用标准库函数std::move(),让一个unique_ptr赋给另一个。
ps1 = demo("Uniquely special");
ps2 = move(ps1);
⚠️ 重要的注意点
:
unique_ptr有一个可用于数组的变体。
使用
new分配内存
时,才能使用auto_ptr
和shared_ptr
,使用new[ ]分配内存
时,不能使用
它们;不使用new
分配内存时,不能使用auto_ptr
或shared_ptr
;不使用new
或new[ ]
分配内存时,不能使用unique_ptr
。
16.2.4 如何选择智能指针?
shared_ptr
:程序需要使用多个指向同一个对象的指针。- 指针数组中使用辅助指针来标识特定的元素。
- 两个对象包含都指向第三个对象的指针
- STL容器包含指针
- 如果编译中没有
shared_ptr
,可使用Boost库
提供的shared_ptr
。 - 可将作为右值的unique_ptr转换为shared_ptr,然后shared_ptr将接管原来归unique_ptr所有的对象。
auto_ptr
:- 在满足
unique_ptr
要求的条件时,也可使用auto_ptr
,但unique_ptr
是更好的选择。
- 在满足
unique_ptr
:程序不需要多个指向同一个对象的指针。如果编译器没有
unique_ptr
,可以使用Boost库
中的scoped_ptr
。可将unique_ptr存储到STL容器中,只要不调用需要复制unique_ptr或者将unique_ptr赋值给另一个的方法或者算法(如sort)就没问题
unique_ptr<int> make_int(int n){return unique_ptr<int>(new int(n)); }void show(unique_ptr<int> &pi){cout << *a << endl; }int main(){...vector<unique<int>> vp(size);for(int i = 0;i < vp.size();i++){vp[i] = make_int(rand() % 1000); //copy temporary unique_ptr}vp.push_back(make_int(rand() % 1000)); //ok because arg is temporaryfor_each(vp.begin(),vp.end(),show); //use for_each() }
如果是按值传递而不是按引用传递给show()传递对象,for_each()语句将非法。
16.3 标准模板库
STL提供的模板
- 容器:一个与数组类似的单元,可以存储若干个值。STL容器是同质,即
存储的值的类型相同
。 - 迭代器:用来
遍历容器的对象
,与可遍历数组的指针类似,是广义指针
。 - 函数对象:类似
函数对象
,可以是类对象
或者函数指针
。 - 算法:完成
特定任务
处方。
16.3.1 模版类vector
STL通过在头文件vector(vector.h)中定义了vector模板,使其类成为通用的。
创建vector模板对象,使用通常的表示法来指出要使用的类型。vector模板使用动态内存分配,所以可以用初始化参数来指出多少矢量。
#include <vector>
vector<int> rating(5);
int n;
cin >> n;
vector<double> scores(n);
使用[]运算符
来访问 vector元素,可以使用通常的数组表示法来访问各个元素:
ratings[0] = 9;
for(int i = 0;i < n ; i++){cout << scores[i] << endl;
}
下面搞一个要求不高的应用程序,它使用了这个类,并创建了两个vector对象—一个是int规范,另一个是string规范
#include <iostream>
#include <vector>const int NUM = 5;int main(){using std::vector;using std::string;using std::cin;using std::endl;using std::cout;vector<int> ratings(NUM);vector<string> titles(NUM);cout << "You will do excatly as told.You will enter\n"<< NUM << " book titles and your ratings(0-10).\n";int i;for (i = 0; i < NUM; ++i) {cout << "Enter title #" << i + 1 << ": ";getline(cin,titles[i]);cout << "Enter your rating(0-10):";cin >> ratings[i];cin.get();}cout << "Thank you.You entered the following:\n"<< "Rating\tBook\n";for (i = 0; i < NUM; ++i) {cout << ratings[i] << "\t" << titles[i] << endl;}return 0;
}输出:
You will do excatly as told.You will enter
5 book titles and your ratings(0-10).
Enter title #1: The Cat Who Knew C++
Enter your rating(0-10):6
Enter title #2: Felonious Felines
Enter your rating(0-10):4
Enter title #3: Warlords of Wonk
Enter your rating(0-10):3
Enter title #4: Don't Touch That Metaphor
Enter your rating(0-10):5
Enter title #5: Panic Oriented Programming
Enter your rating(0-10):8
Thank you.You entered the following:
Rating Book
6 The Cat Who Knew C++
4 Felonious Felines
3 Warlords of Wonk
5 Don't Touch That Metaphor
8 Panic Oriented Programming
16.3.2 可对vector执行的操作
size()
:返回容器中元素数目swap()
:交换两个容器的内容begin()
:返回一个指向容器中第一个元素的迭代器end()
:返回一个表示超过容器尾的迭代器。
迭代器:一个广义指针。每个容器类都定义了一个合适的迭代器,迭代器的类型为:iterator 的 typedef,其作用域为整个类。
声明一个迭代器变量的语法如下:
vector<double>::iterator pd; // pd 是迭代器
vector<double> scores;
// 迭代器的可进行的两个操作
pd = scores.begin();
*pd = 22.3; // 使用 * 来解引用
++pd; // 使用 ++ 使得指针指向下一个元素
push_back()
:将元素添加到vector结尾。erase():删除vector中给定区间的元素。
接受两个参数:
- 起始位置
- 终点位置
区间为左闭右开。
//删除第一个和第二个元素 vector.erase(scores.begin(),scores.begin() + 2);
insert():插入元素
接受3个参数
指定新元素的插入位置
第二个和第三个为被插入区间。
//将适量new_v中除第一个元素外的所有元素插入到old_v矢量的第一个元素前面 vector<int> old_v; vector<int> new_v; ... old_v.insert(old.begin(),new_v.begin() + 1,new_v.end());
下面程序演示size(),begin(),end(),push_back(),erase()和insert的用法。为简化数据处理,将程序清单中的rating和title组合成了一个Review结构,并使用FillReview() 和 ShowReview()函数来输入和输出Review对象
#include <iostream>
#include <string>
#include <vector>struct Review {std::string title;int rating;
};bool FillReview(Review &rr);void ShowReview(const Review &rr);int main() {using std::cout;using std::endl;using std::vector;vector<Review> books;Review temp;while (FillReview(temp)) {books.push_back(temp);}int num = books.size();if (num > 0) {cout << "Thank you. You entered the following:\n"<< "Rating\tBook\n";for (int i = 0; i < num; ++i) {ShowReview(books[i]);}cout << "Reprising:\n"<< "Rating\tBook\n";vector<Review>::iterator pr;for (pr = books.begin(); pr != books.end(); pr++) {ShowReview(*pr);}vector<Review> oldList(books); //copy constructor usedif (num > 3) {//remove 2 itemsbooks.erase(books.begin() + 1, books.begin() + 3);cout << "After erasure:\n";for (pr = books.begin(); pr != books.end(); pr++) {ShowReview(*pr);}//在books的最前面插入1个元素books.insert(books.begin(), oldList.begin() + 1, oldList.begin() + 2);cout << "After insertion:\n";for (pr = books.begin(); pr != books.end(); pr++) {ShowReview(*pr);}books.swap(oldList);cout << "After swap:\n";for (pr = books.begin(); pr != books.end(); pr++) {ShowReview(*pr);}} else {cout << "Nothing enter,nothing gained.\n";}return 0;}
}bool FillReview(Review &rr) {std::cout << "Enter book title (\"quit\" to quit): ";std::getline(std::cin, rr.title);if (rr.title == "quit") {return false;}std::cout << "Enter book rating: ";std::cin >> rr.rating;if (!std::cin) {return false;}//get rid of rest of input linewhile (std::cin.get() != '\n') {continue;}return true;
}void ShowReview(const Review &rr) {std::cout << rr.rating << "\t" << rr.title << std::endl;
}输出:
Enter book title ("quit" to quit): The Cat Who Knew Vectors
Enter book rating: 5
Enter book title ("quit" to quit): Candid Canines
Enter book rating: 7
Enter book title ("quit" to quit): Warriors of Wonk
Enter book rating: 4
Enter book title ("quit" to quit): Quantum Manners
Enter book rating: 8
Enter book title ("quit" to quit): quit
Thank you. You entered the following:
Rating Book
5 The Cat Who Knew Vectors
7 Candid Canines
4 Warriors of Wonk
8 Quantum Manners
Reprising:
Rating Book
5 The Cat Who Knew Vectors
7 Candid Canines
4 Warriors of Wonk
8 Quantum Manners
After erasure:
5 The Cat Who Knew Vectors
8 Quantum Manners
After insertion:
7 Candid Canines
5 The Cat Who Knew Vectors
8 Quantum Manners
After swap:
5 The Cat Who Knew Vectors
7 Candid Canines
4 Warriors of Wonk
8 Quantum Manners
16.3.3 STL对vector的其它操作
STL从更广泛的角度定义了非成员函数
来执行这些操作,即不是为了每个容器而定义一个find()成员函数
,而是定义了一个适用于所有容器类
的非成员函数find()
。这种设计理念省去了大量重复的工作。
3个具有代表性的STL函数:
for_each()
- 可用于很多个容器类,接受3个参数:
- 前两个是定义容器中
区间的迭代器
- 最后一个
指向函数的指针
,普遍一般说最后一个参数是函数对象
。
- 前两个是定义容器中
for_each()
函数将被指向的函数用于容器区间中的各个元素
- 被指向的函数
不能修改容器元素的值
- 可以使用
for_each()
函数来代替for循环
。
#include<vector> struct Review{std::string title;int rating; }; vector<Review> book; vector<Review>::iterator pr; vector<Review>::iterator pr; for (pr = books.begin(); pr != books.end();pr++)ShowReview(*pr);// 使用for_each修改为: for_each(books.begin(),books.end(),ShowReview); // 可以避免显式地使用迭代器变量
- 可用于很多个容器类,接受3个参数:
random_shuffle()
接受两个指定区间的迭代器参数,并随机排列区间中的元素。
random_shuffle(books.begin(),books.end());
要求容器类允许随机访问,例如vector类。
sort()
也要求容器支持随机访问。
函数的两个版本:
版本1:接受2个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,对区间中元素进行操作。(全排序)
// 使用内置的<运算符 vector<int> coolstuff; ... sort(coolstuff.begin(),coolstuff.end()); // 如果容器元素是用户定义的对象,则必须定义能够处理该类型对象的 operator<() 函数 bool operator<(const Review &r1 , const Review & r2) {if (r1.title < r2.tile)return true;else if (r1.title == r2.title && r1.rating < r2.rating)return true;elsereturn true; }
版本2:接受3个参数(完整弱排序)
- 前两个参数:指定区间的迭代器
- 最后一个参数:指向要使用的函数的指针(函数对象)。
bool WorseThan(const Review &r1,const Review &r2){if(r1.rating < r2.rating){return true;}else{return false;} }//有了这个函数,就可以使用下面的语句包含Review对象的books矢量按rating升序排列 sort(books.begin(),books.end(),WorseThan);
下面程序演示上述的STL函数的用法:
#include <iostream> #include <string> #include <vector> #include <algorithm>struct Review {std::string title;int rating; };bool operator<(const Review &r1,const Review &r2); bool worseThan(const Review &r1,const Review &r2); bool FillReview(Review &rr); void ShowReview(const Review &rr);int main() {using std::cout;using std::endl;using std::vector;vector<Review> books;Review temp;while (FillReview(temp)) {books.push_back(temp);}int num = books.size();if (num > 0) {cout << "Thank you. You entered the following:\n"<< "Rating\tBook\n";for_each(books.begin(),books.end(),ShowReview);sort(books.begin(),books.end(),worseThan);cout << "Sorted ny rating:\nRating\tBook\n";for_each(books.begin(),books.end(),ShowReview);random_shuffle(books.begin(),books.end());cout << "After shuffling:\nRating\tBook\n";for_each(books.begin(),books.end(),ShowReview);}else{cout << "No entries. ";cout << "Bye.\n";return 0;}return 0; }bool FillReview(Review &rr) {std::cout << "Enter book title (\"quit\" to quit): ";std::getline(std::cin, rr.title);if (rr.title == "quit") {return false;}std::cout << "Enter book rating: ";std::cin >> rr.rating;if (!std::cin) {return false;}//get rid of rest of input linewhile (std::cin.get() != '\n') {continue;}return true; }void ShowReview(const Review &rr) {std::cout << rr.rating << "\t" << rr.title << std::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 true;} }bool worseThan(const Review &r1,const Review &r2){if(r1.rating < r2.rating){return true;} else{return false;} }输出: Enter book title ("quit" to quit): The Cat Who Can Teach You Enter book rating: 8 Enter book title ("quit" to quit): The Dogs of Dharma Enter book rating: 6 Enter book title ("quit" to quit): The Wimps of Wonk Enter book rating: 3 Enter book title ("quit" to quit): Farewell and Delete Enter book rating: 7 Enter book title ("quit" to quit): quit Thank you. You entered the following: Rating Book 8 The Cat Who Can Teach You 6 The Dogs of Dharma 3 The Wimps of Wonk 7 Farewell and Delete Sorted ny rating: Rating Book 3 The Wimps of Wonk 6 The Dogs of Dharma 7 Farewell and Delete 8 The Cat Who Can Teach You After shuffling: Rating Book 3 The Wimps of Wonk 8 The Cat Who Can Teach You 7 Farewell and Delete 6 The Dogs of Dharma
16.3.4 基于范围的for循环
for_each(books.begin(),books.end(),ShowReview);
//可改为
for(auto x :books) ShowReview(x);
基于范围的for循环可以修改容器的内容,诀窍是制定一个引用参数。例如:假设有如下函数:
void InflateReview(Review &r){r.rating++;}
//则可以使用如下循环
for(auto &x:books) InflateReview(x);
16.4 泛型编程
STL是一种 泛型编程(generic programming)
。
面向对象编程
关注编程的数据方面
。泛型编程
关注算法
。共同点:抽象和创建可重用代码。
泛型编程旨在编写独立于数据类型的代码。
在C++中,完成通用程序的工具是模板
。
16.4.1 为何使用迭代器?
模板提供了存储在容器中的数据类型的通用表示
,因此还需要遍历容器中的值的通用表示
,迭代器
正是这样的通用表示,它算法独立于使用的容器类型。
- 迭代器应具备的特征:
- 能进行
解除引用
的操作,以便能够访问
它引用的值。 - 迭代器之间能
赋值
。 - 迭代器之间能
比较
,判断是否相等。 - 能
遍历
容器中的所有元素,以便于实现++
需求。
- 能进行
struct Node{double item;Node *p_next;
};class iterator
{Node *pt;public:iterator() : pt(0) {};iterator (Node *pn) : pt(pn) {}double operator*() {return pt->item;}iterator & operator++() // for ++it(先变化再操作){pt = pt->p_next;return *this;}iterator operator++(int) // for it++(先操作再变化){iterator tmp = *this;//创建一个临时迭代器存储当前的值pt = pt->p_next; //进行+1操作return tmp; //返回操作前的临时迭代器}...
};
由于c++将operator++作为前缀版本,operator++(int)作为后缀版本;其中的参数永远不会被用到,所以不必指定其名称。
有了上述的类,我们可以编写一个find函数找到所要找的结点(或者值)
//假设
typedef Node* iterator;
iterator find(iterator head,const double &val){iterator start;for(start = head;start != 0;++start){if(*start == val){return start;}}return 0;
}
作为一种编程风格,最好避免直接使用迭代器,应尽可能使用STL函数(如for_each())来处理细节。也可以使用C++11新增的基于范围的for循环。
for (auto x : scores) cout << x << std::endl;
16.4.2 迭代器的类型
不同的算法对迭代器的要求也不同。
- 查找算法
- 需要定义
++运算符
,以便迭代器能遍历
整个容器。 - 要求能
读取
数据,但不要求能写
数据。
- 需要定义
- 排序算法
- 能够
随机访问
,便于交换
两个不相邻的元素。 - 如果
iter
是迭代器
,则可以通过定义+运算符
来实现随机访问。
- 能够
STL定义了5种迭代器
输入迭代器
- 被程序用来读取容器中的信息,换句话说就是:容器中的信息输入到程序当中
- 能访问容器中所有的值,通过支持
++运算符
来实现。 单向
通行,只读算法
输出迭代器
- 将信息从程序传输给容器的迭代器,因此
程序的输出
就是容器的输入
。 单向
通行,只写算法
- 将信息从程序传输给容器的迭代器,因此
正向迭代器
- 只使用
++运算符
来遍历容器,每次沿容器向前移动一个元素。 可读可写
,如果要使得只读
,可使用const关键字
。
int *pr; //read-write iterator const int *pr; //read-only iterator
- 只使用
双向迭代器
- 典型函数:
reverse()
翻转操作 - 具备
正向迭代器
的所有特性
。 - 支持两种(前缀和后缀)递减运算符
- 典型函数:
随机访问迭代器
- 有些算法(如标准排序和二分检索)要求能够直接跳到容器中的任何一个元素,这叫随机访问,需要随机访问迭代器。
- 具备双向迭代器的所有特性。
5种迭代器可执行解除引用操作
(已定义*运算符
),可进行比较
(看是否相等,使用了==运算符
)
16.4.3 迭代器的层次结构
由于迭代器之间都是后者支持了前者的全部功能,也有自己的功能,所以对应的层次结构可表示为:
每个容器类都定义一个类级 typedef名称
— iterator
,因此 vector<int>类
的迭代器类型为 vector<int>::interator
。
16.4.4 概念、改进和模型
概念可以具有类似继承的关系。概念的具体实现叫做 模型(model)
。指向int的常规指针是一个随机访问迭代器模型
,也是一个正向迭代器模型
,因为它满足该概念的所有要求。
16.4.4.1 将指针用作迭代器
- 迭代器是广义指针,而指针满足所有的迭代器要求。
- 迭代器是STL算法的接口,而指针是迭代器,因此,STL算法可以使用指针来对基于指针的非STL容器进行操作。
STL sort() 函数接受指向容器第一个元素的迭代器和指向超尾的迭代器作为参数。
const int SIZE = 100;
double Receipts[SIZE];
sort(Receipts, Receipts + SIZE); // 等价于 sort(&Receipts[0],&Receipts[SIZE])
copy()
: 将数据从一个容器复制到另一个容器中,通过迭代器的方式实现。
- 前两个参数是要赋值的范围(输入迭代器),最后一个参数是要复制到的位置(输出迭代器)。
- 不能使用
copy
将数据放到空vector
中。
int casts[10] = {6,7,2,9,4,11,8,7,10,5};
vector<int> dice[10];
copy(casts,casts + 10,dice.begin()); //copy array to vector
STL为模板提供了 ostream_iterator模板。
适配器(adapter)
---- 一个类或函数
。适配器
的作用:将一些接口转换为STL使用的接口:可以通过包含头文件<iterator>
来实现。
ostream_iterator 适配器
#include <iterator> ... // 第一个参数:int 指出被发送给输出流的数据类型 // 第二个参数:char 指出了输出流使用的字符类型。 // out_iter中的第一个参数:要使用的输出流 // out_iter中的第二个参数:分隔符 ostream_iterator<int, char> out_iter(cout," "); // out_iter 是一个接口// 使用解引用的方式对迭代器进行操作 //意味着将15和空格组成的字符串发送到cout管理的输出流中 *out_iter++ = 15; // 相当于 cout << 15 << " ";
如果有一个表示
输出流
的迭代器
,可以用copy()
。copy(dice.begin(),dice.end(),out_iter); //copy vector to output stream //或者直接构建一个匿名迭代器 copy(dce.begin(),dce.end(),ostream_iterator<int, char> out_iter(cout," "));
istream_iterator
// 使用两个 istream_iterator 对象来定义 copy() 的输入范围 copy(istream_iterator<int,char>(cin),istream_iterator<int,char>(),dice.degin()); // 第一个参数:指出要读取的数据类型 // 第二个参数:指出输入流使用的字符类型
16.4.4.2 其它迭代器
除了 istream_iterator
、ostream_iterator
之外,还有一些专用的预定义迭代器类型。
reverse_iterator
:执行递增操作将导致它被递减。- 只能允许用于尾部快速插入的容器
- ‼️rbegin()和end()返回相同的值(超尾),但类型不同(reverse_iterator和iterator)。同样,rend()和begin()也返回相同的值(制定第一个元素的迭代器),但是类型不同。
- ✅反向指针通过先递减,再解除引用解决了这两个问题~~(rbegin()返回超尾,不能对该地址进行解引用;rend()不在区间内,无法访问第一个元素的迭代器)~~。即rp将在rp的当前值之前对迭代器执行解除引用。直白的说就是如果rp指向的位置是6,那么*rp将是5的值。
下面例子演示了如何使用copy()、istream迭代器和反向迭代器:
#include <iostream>
#include <iterator>
#include <vector>int main(){using namespace std;int casts[10] = {6,7,2,9,4,11,8,7,10,5};vector<int> dice(10);// copy from array to vectorcopy(casts,casts + 10,dice.begin());cout << "Let the dice be cast!\n";//create an ostream iteratorostream_iterator<int,char> out_iter(cout," ");//copy from vector to outputcopy(dice.begin(),dice.end(),out_iter);cout << endl;cout << "Implicit use of reverse iterator.\n";copy(dice.rbegin(),dice.rend(),out_iter);cout << endl;cout << "Explicit use of reverse iterator.\n";vector<int>::reverse_iterator it;for(it = dice.rbegin();it != dice.rend();it++){cout << *it << " ";}cout << endl;return 0;
}输出:
Let the dice be cast!
6 7 2 9 4 11 8 7 10 5
Implicit use of reverse iterator.
5 10 7 8 11 4 9 2 7 6
Explicit use of reverse iterator.
5 10 7 8 11 4 9 2 7 6
‼️如果可以在显式声明迭代器和使用STL函数来处理内部问题之间选择,请采用后者。后一种方法要做的工作较少,人为出错的机会也较少。
back_insert_iterator
:将元素插入到容器尾部front_insert_iterator
:将元素插入到容器的前端- 只能用于允许在起始位置做时间固定插入的容器类型。
insert_iterator
:将元素插入到 insert_iterator 构造函数的参数指定的位置前面。- 无限制
后三种(back_insert_iterator
、front_insert_iterator
、insert_iterator
)通过将复制转换为插入来解决不知道长度或者不覆盖已有内容的问题。原因:只做插入新元素的操作,所以不会覆盖已有数据,并使用自动内存分配来确保可以容纳新的数据信息。
back_insert_iterator<vector<int> > back_iter(dice);
insert_iterator<vector<int> > insert_iter(dice,dice.begin() );
下面程序演示了两种迭代器的用法,还是用for_each()而不是ostream迭代器进行输出:
#include <iostream>
#include <string>
#include <iterator>
#include <vector>
#include <algorithm>void output(const std::string & a) {std::cout << s << " ";}int main()
{using namespace std;string s1[4] = {"fine","fish","fashion","fate"};string s2[2] = {"busy","bate"};string s3[2] = {"silly","singer"};vector <string> words(4);copy(s1,s1 + 4,words.begin()); // 将s1中的4个字符串复制到words中for_each(words.begin(),words.end(),output);cout << endl;// 构造匿名对象 back_insert_iterator// 将s2的所有元素插入到末尾,而且words的长度扩增到6个元素copy(s2,s2+2,back_insert_iterator<vector<string>> (words));for_each(word.begin(),words.end(),output);cout << endl;// 构造匿名对象 insert_iterator// 将s3中所有的元素插入到words第一个元素的前面,words的长度被增加到8个元素copy(s3,s3+2,insert_iterator<vector<string>>(words,words.begin()));for_each(words.begin(),words.end(),output);cout << endl;return 0;
}
16.4.5 容器种类
STL具有容器概念和容器类型。
概念是具有名称(如容器、序列容器、关联容器等)的通用类别。
16.4.5.1 容器类型
可用于创建具体容器对象的模板。
C++11 之前有11个
deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset和bitset
C++11中新增5个
forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset
16.4.5.2 容器概念
容器概念指定了所有STL容器类都必须满足的一系列要求。
- 容器是存储其他对象的对象。被存储的对象必须是
同一种类型
的,可以是OOP意义上的对象
,也可以是内置类型值
。 - 存储在容器中的基本类型必须满足:是
可复制构造
和可赋值
的。 - 只要类定义没有将可复制构造函数和赋值运算符声明为私有或保护的。
稍微补充:拷贝构造函数和使用重载"="的时候复杂度是线性的,因为它需要用到vector对象中的所有元素。
“复杂度”:描述了执行操作所需的时间。
- 编译时间:在编译时执行,执行时间为0
- 固定时间:发生在运行阶段,但独立于对象中的数目
- 线性时间:时间与元素数目成正比。例子:如果a和b都是容器,则a == b具有线性时间,因为==操作必须用于容器中的所有元素。但假如a与b容器的长度不相同,那么就直接得出对应的布尔值(false)。
16.4.5.3 序列(sequence)容 器
7种STL容器类型:deque
、C++11新增的forward_list
、list
、queue
、priority_queue
、stack
和vector
都是序列。
队列
可以在 队尾添加
元素,在 队首删除
元素。
deque
表示的 双端队列 允许在 两端添加和删除 元素。
序列要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素、除第一个元素和最后一个元素外,每个元素前后都有一个元素。
数组和链表是序列,但是分支结构不是。
对于序列,都支持以下的操作:
在情况允许下,复杂都是固定时间。下表还列出了一些STL容器的其他操作:
解释上表的一部分操作:
- a[n]与a.at(t)都返回一个指向容器中第n个元素的引用。它们的区别是a.at(t)会检查边界,若超出边界就会报out_of_range的错
- vector容器不能使用push_front():假如使用push_front()的话那么vector容器中的每个元素都要往后移动一位,它是移动单个数字所需时间的vector.size()倍。
vector
- 模板头文件
vector
- 是数组的一种类表示
- 提供
自动内存管理
概念,可以动态改变vector对象的长
度,随着元素的添加和删除而增大和减小。 - 在
尾部
复杂度是:固定时间
- 在
头部
或中间
复杂度:线性时间
- 支持可反转容器(reversible container)的两个类方法
rbegin()
:返回一个指向反转序列的第一个元素的迭代器rend()
:返回反转序列的超尾迭代器- 强调随机快速访问
- 模板头文件
deque
- 模板头文件
deque
- 双端队列(double-ended queue)
- 在STL中,类似vector,支持随机访问。
- 一般发生在起始和结尾处的操作,考虑使用deque
- 模板头文件
list
- 模板头文件
list
双向链表
- list在链表中任一位置进行插入和删除的复杂度都固定
- 元素的
快速插入
和删除
- ⚠️list不支持数组表示法和随机访问
- 包含链表专用的成员函数
- 模板头文件
下面程序演示这五种方法还有insert的用法:
#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>void outInt(int n) {std::cout << n << " ";
}int main(){using namespace std;list<int>one(5,2);int stuff[5] = {1,2,4,8,6};list<int> two;two.insert(two.begin(),stuff,stuff + 5);int more[6] = {6,4,2,4,6,5};list<int> three(two);three.insert(three.end(),more,more + 6);cout << "List one: ";for_each(one.begin(),one.end(),outInt);//2 2 2 2 2cout << endl << "List two: ";for_each(two.begin(),two.end(),outInt);//1 2 4 8 6cout << endl << "List three: ";for_each(three.begin(),three.end(),outInt);//1 2 4 8 6 6 4 2 4 6 5three.remove(2);//1 4 8 6 6 4 4 6 5cout << endl << "List three minus 2s: ";for_each(three.begin(),three.end(),outInt);three.splice(three.begin(),one);//2 2 2 2 2 1 4 8 6 6 4 4 6 5cout << endl << "List three after splice: ";for_each(three.begin(),three.end(),outInt);cout << endl << "List one: ";for_each(one.begin(),one.end(),outInt);three.unique();cout << endl << "List three after unique: ";for_each(three.begin(),three.end(),outInt);three.sort();three.unique();cout << endl << "List three after sort & unique: ";for_each(three.begin(),three.end(),outInt);two.sort();three.merge(two);cout << endl << "Sorted two merged into three: ";for_each(three.begin(),three.end(),outInt);cout << endl;return 0;
}输出:
List one: 2 2 2 2 2
List two: 1 2 4 8 6
List three: 1 2 4 8 6 6 4 2 4 6 5
List three minus 2s: 1 4 8 6 6 4 4 6 5
List three after splice: 2 2 2 2 2 1 4 8 6 6 4 4 6 5
List one:
List three after unique: 2 1 4 8 6 4 6 5
List three after sort & unique: 1 2 4 5 6 8
Sorted two merged into three: 1 1 2 2 4 4 5 6 6 8 8
insert与splice的区别:
insert将原始区间的副本插入到目标地址,而splice()则将原始区间移动到目标地址。因此,在one与three合并后,one就变为空了。
unique只能将相邻的重复值进行去重,如果需要完全去重,可以先排序然后再进行去重。
forward_list(C++11)
- 实现了
单链表
- 单链表的特点:每个节点都只链接到下一个节点,而没有链接到前一个节点。
- 是一个正向迭代器
- 实现了
queue
- 模板头文件
queue
- 是一个
适配器类
- 不允许随机访问队列元素,不允许遍历队列
- 使用限制在定义队列的基本操作上,可以将元素进行如下操作:
- 添加到队尾
- 从队首删除元素
- 查看队首和队尾的值
- 检查元素数目
- 测试队列是否为空
- 模板头文件
priority_queue
模板头文件
queue
是一个
适配器类
与queue主要区别: 在priority_queue中,最大元素被移到队首,内部区别在于,默认的底层是 vector
可修改用于确定哪个元素放到队首的比较方法
priority_queue<int> pq1; // 使用priority_queue默认方式 排序 priority_queue<int> pq2(greater<int>) // 使用greater<int> 排序
stack
- 头文件
stack
- 是一个适配器类
- 不允许随机访问栈元素,也不能遍历栈。
- 基本操作和queue类似。
- 头文件
16.4.6 关联容器(associative container)
关联容器是对容器概念的另一个改进。
关联容器将值与键关联在一起,并使用键来查找值
。
优点:提供对元素的快速访问。允许插入新元素,但不能指定插入的位置
。关联容器通常是用于确定数据放置的算法。
基于树(数据结构)
来实现。
树的概念:其根节点链接到一个或两个节点,节点再次分支。
STL提供4种关联容器
头文件 set
set
set的类型与键相同,且键唯一。不会有相同的元素,对于set来说,值就是键。
set的方法
- set_union()(“并集”)可以接受5个迭代器:前两个迭代器定义为第一个集合的区间,接下来的两个迭代器定义为第二个集合的区间,最后一个迭代器是输出迭代器。set_intersection()与set_difference()分别查找交集和获得两个集合的差
- lower_bound()将键作为参数,并返回一个迭代器,该迭代器指向集合中第一个不小于键参数的成员。
- upper_bound()将键作为参数,并返回一个迭代器,该迭代器指向集合中大于键参数的成员。
下面演示一下set的这些方法或函数的用法:
#include <iostream> #include <string> #include <set> #include <algorithm> #include <iterator>int main(){using namespace std;const int N = 6;string s1[N] = {"buffoon" , "thinkers", "for" , "heavy" , "can" , "for"};string s2[N] = {"metal","any","food","elegant","deliver","for"};set<string>A(s1,s1 + N);set<string>B(s2,s2 + N);ostream_iterator<string,char> 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(B.begin(),B.end(),A.begin(),A.end(),out);cout << endl;set<string> C;cout << "Set C:\n";set_union(A.begin(),A.end(),B.begin(),B.end(),insert_iterator<set<string>>(C,C.begin()));copy(C.begin(),C.end(),out);cout << endl;string s3("grungy");C.insert(s3);cout << "Set C after insertion:\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; }输出: Set A: buffoon can for heavy thinkers Set B: any deliver elegant food for metal Union of A and B: any buffoon can deliver elegant food for heavy metal thinkers Intersection of A and B: for Difference of A and B: any deliver elegant food metal Set C: any buffoon can deliver elegant food for heavy metal thinkers Set C after insertion: any buffoon can deliver elegant food for grungy heavy metal thinkers Showing a range: grungy heavy metal 输出: Set A: buffoon can for heavy thinkers Set B: any deliver elegant food for metal Union of A and B: any buffoon can deliver elegant food for heavy metal thinkers Intersection of A and B: for Difference of A and B:// 差集 any deliver elegant food metal Set C: any buffoon can deliver elegant food for heavy metal thinkers Set C after insertion: any buffoon can deliver elegant food for grungy heavy metal thinkers Showing a range: grungy heavy metal
multiset
- 得到最终的对象是经过排序的。
- 类似set,不同的是一个键可能有多个值。为将信息结合起来,实际的值类型将键类型和数据类型结合为一对,为此STL使用模版类pair<const keyType,dataType>实现。
- count()接受键作为参数,并返回具有该键的元素数目。
- lower_bound()、upper_bound()的用法与set类似。
- equal_range()用键做参数,且返回两个迭代器,它们表示的区间与该键匹配。可以将这两个迭代器封装到pair之中。
下面演示一下上面所说的方法:
#include <iostream> #include <map> #include <algorithm>typedef int KeyType; typedef std::pair<const KeyType,std::string> Pair; typedef std::multimap<KeyType,std::string> MapCode;int main(){using namespace std;MapCode codes;codes.insert(Pair(415,"San Francisco"));codes.insert(Pair(510,"Oakland"));codes.insert(Pair(718,"Brooklyn"));codes.insert(Pair(718,"Staten Island"));codes.insert(Pair(415,"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<MapCode::iterator,MapCode::iterator> range = codes.equal_range(718);cout << "Cities with area code 718:\n";for(auto it = range.first;it != range.second;it++){cout << (*it).second << endl;}return 0; }输出: Number of cities with area code 415: 2 Number of cities with area code 718: 2 Number of cities with area code 510: 2 Area Code City415 San Francisco415 San Rafael510 Oakland510 Berkeley718 Brooklyn718 Staten Island Cities with area code 718: Brooklyn Staten Island
头文件 map
- map
- 值与键的类型不同,键是唯一的
- 每一个键只对应于一个值。
- multimap
- 类似map,一个键可与多个值相关联。
- map
16.4.7 无序关联容器(C++11)
无序关联容器是对容器概念的另一种改进。
关联容器基于树结构实现,而无序关联容器是基于数据结构哈希表来实现。旨在提高添加和删除元素的速度以及提高查找算法的效率。
4种无序关联容器:
- unordered_set
- unordered_multiset
- unordered_map
- unordered_multimap
16.5. 函数对象
很多STL算法都使用函数对象 ---- 也叫 函数符(functor)
。
函数符是可以以函数方式与()
结合使用任意对象:函数名
、指向函数的指针
和重载了()运算符的类对象
(即定义了函数operator()()的类
),假定有一个这样的类
class Linear{
private:double slope;double y0;
public:Linear(double sl_ = 1,double y_ = 0):slope(sl_),y0(y){}double operator()(double x){return y0 + slope * x};
}//重载()之后,就可以像使用函数一样使用类对象了,用法如下:
Linear L1;
Linear L2(2.5,10.0);
double y1 = L1(12.5); // 0 + 12.5 * 1 = 12.5
double y2 = L1(0.4); // 10 + 2.5 * 0.4 = 11
在for_each方法中,第三个参数不能声明函数指针,所以for_each 模板原型为:
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);
由于ShowReview()的原型如下:
void ShowReview(const Review &);
这样我们可以知道,根据原型,我们可以知道标识符ShowReview的类型将为void(*)(const Review &),显然标识符ShowReview是指向函数的指针,用ShowReview()调用该函数。如果for_each()参数是一个对象,则Linear()将是调用其重载的()运算符的对象。
16.5.1 函数符概念
STL定义了容器和迭代器的概念,也定义了函数符的概念
生成器(generator)是不用参数即可调用的函数符。
一元函数(unary function)是用一个参数可以调用的函数符
- for_each()函数符应当是一元函数,因为它每次只用于一个容器元素
二元函数(binary function)是用两个参数可以调用的函数符
返回bool值的一元函数是谓词(predicate)
list模版有一个将谓词作为参数的remove_if()成员,下面稍稍演示
bool tooBig(int n) { return n > 100; } list<int> scores; ... scores.remove_if(tooBig);
返回bool值的二元函数是二元谓词(binary predicate)。
将二元谓词作为其第3个参数
bool WorseThan(const Review &r1,const Review &r2); ... sort(books.begin(),books.end(),WorseThan);
下面的程序演示类函数符适用的地方:
#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>template <class T>
class tooBig{
private:T cutOff;
public:tooBig(const T &v):cutOff(v){}bool operator()(const T &v){ return v > cutOff;}
};void displayInt(int n) {std::cout << n << " ";
}int main(){using std::list;using std::cout;using std::endl;tooBig<int> f100(100);// limit = 100;int vals[10] = {50,100,90,180,60,210,415,88,188,201};list<int> yadayada(vals,vals + 10);list<int> etcetera(vals,vals + 10);cout << "Original lists:\n";for_each(yadayada.begin(),yadayada.end(),displayInt);cout << endl;for_each(etcetera.begin(),etcetera.end(),displayInt);cout << endl;yadayada.remove_if(f100); //use a named function objectetcetera.remove_if(tooBig<int>(200)); //construct a function objectcout << "Trimmed lists:\n";for_each(yadayada.begin(),yadayada.end(),displayInt);cout << endl;for_each(etcetera.begin(),etcetera.end(),displayInt);cout << endl;return 0;
}输出:
Original lists:
50 100 90 180 60 210 415 88 188 201
50 100 90 180 60 210 415 88 188 201
Trimmed lists:
50 100 90 60 88
50 100 90 180 60 88 188
一个值v作为函数参数传递,而第二个参数cutOff是由类构造函数设置的,这种技术将不同的tooBig对象初始化为不同的取舍值,上面的程序正诠释着这种操作。
16.5.2 预定义的函数符
STL定义了多个基本函数,它们执行诸如两个值相加、比较两个值是否相等的操作。提供这些函数对象是为了支持将函数作为参数的STL函数。
举transform()的例子,transform()有两个版本:
1⃣️接收四个参数,头两个参数指定容器区间的迭代器,第三个参数是指定结果复制到哪里的迭代器,最后一个参数就是函数符,它应用于所有元素。
const int Num = 5;
double arr1[Num] = {36,47,55,69,91};
vector<double> vec(arr1.begin(),arr1.end());
ostream_iterator<double,char> out(cout," ");
transform(vec.begin(),vec.end(),out,sqrt);
2⃣️使用一个接受两个参数的函数,并将该函数用于两个区间的元素;第三个参数标识第二个区间的起始位置,add计算(两区间对应位置的和,若没有对应,则把区间1的元素输出即可)的结果输出到out里面
#include <iostream>
#include <vector>
using namespace std;
double add(double a,double b){return a + b;
}
int main(){vector<double> ref1 = {23,45,67,89,100};vector<double> ref2 = {20,44,55,66,77};ostream_iterator<double,char> out (cout," ");transform(ref1.begin(),ref1.end(),ref2.begin(),out,add);return 0;
}输出:
43 89 122 155 177
‼️头文件 functional
定义了多个模板类函数对象,其中包括了 plus<>()
。
#include <functional>
...
plus<double> add; // 创建一个 plus<double> 对象
double y = add(2.2,3.4); //使用 plus<double>::operator()()transform(ref1.begin(),ref1.end(),ref2.begin(),out,plus<>());
‼️这里,没有创建命名的对象,而是用plus< double >构造函数构造了一个函数符,以完成相加运算(括号表示调用默认构造函数,传递给transfrom()的第4个参数为类对象)
16.5.3 自适应函数符号和函数适配器
函数符成为自适应的原因:携带标识参数类型和返回类型的typedef成员
。这些成员是:
- result_type
- first_argument_type
- second_argument_type
(看着晕,查了一下源码感受一下
第 16 章 string类和标准模板库相关推荐
- 《C++ Primer Plus》读书笔记 第16章 string类和标准模板库
第16章 string类和标准模板库 1.string类 表16.1列出了string类的几个构造函数.其中NBTS(null-terminated string)表示以空字符结束的传统C-风格字符串 ...
- C++Primer Plus笔记——第十六章 string类和标准模板库总结及程序清单
目录 本章小结 程序清单 string类 16.1 str1.cpp 16.2 strfile.cpp 16.3 hangman.cpp ...
- 《C++ Primer Plus》学习笔记-string类和标准模板库
第16章 string类和标准模板库(本书附录G有详细介绍) 16.1 string类 string类是由头文件string支持的. string类位于命名空间std中. ctor标识是传统C++中构 ...
- C++ Primer Plus 笔记(16章:string类和标准模板库)
16 string类和标准模板库 16.1 string类 16.1.1 构造字符串 常见的字符串书中给了7个,另外还有两个在C++11里新增的(NTBS)表示以空字符结束的传统字符串 构造函数 描述 ...
- C++ Primer plus学习笔记-第十六章:string类和标准模板库
第十六章:string类和标准模板库 前言:这一章已经相当靠近全书的后面部分了:这一章我们会深入探讨一些技术上的细节,比如string的具体构造函数,比如适用于string类的几个函数,比如我们还会介 ...
- 【String类和标准模板库】
1.string类 2.智能指针模板类 3.标准模板库 4.泛型编程 5.函数对象 6.算法 7.其他库 1.string类 string类是由头文件string支持的,要使用类,关键要知道它的公有接 ...
- 蓝桥杯算法竞赛系列第0章——蓝桥必考点及标准模板库STL(上)(万字博文,建议抱走)
欢迎来到:遇见蓝桥遇见你,不负代码不负卿! 目录 一.蓝桥必考点剖析 二.什么是STL 三.vector的常见用法详解 1.vector的定义 2.vector容器内元素的访问 (1).通过下标访 ...
- Visual C++ 2010 第10章 标准模板库
10.1 标准模板库的定义 STL 是为本地C++编译器提供的一个类与函数模板的大集合. STL 包含6种组件:容器.容器适配器.迭代器.算法.函数对象和函数适配器. 因为STL组件时标准库的一部分, ...
- C++ 笔记(19)— 标准模板库(STL容器、STL迭代器、STL算法、STL容器特点、STL字符串类)
C++ 标准库可以分为两部分: 标准函数库: 这个库是由通用的.独立的.不属于任何类的函数组成的.函数库继承自 C 语言. 面向对象类库: 这个库是类及其相关函数的集合. C++ 标准库包含了所有的 ...
最新文章
- 【Qt】Qt多屏编程,在指定显示屏上显示指定对话框
- WCF技术剖析之三十:一个很有用的WCF调用编程技巧[下篇]
- Perl正则表达式--练习1
- Object类的用法(一)
- 漫步最优化三十三——牛顿法
- 一加Z配置细节曝光:处理器大变 为了更好的进入欧洲市场?
- PHP的几个常用加密函数
- java流程图表示输入 输出_流程图 - 迷途行者 - 博客园
- 思科交换机命令大全 一
- JDBC - new Date插入mysql数据库,数据库时间多一秒问题
- 【转载】log4j日志
- linux 安装minio并设置开机自启动
- 使用VLC把视频转换为一帧一帧的图片
- 【QT】将指定ip添加到凭据管理器
- 信号满格怎么显示无法连接服务器,Win7 32系统网络信号满格却无法连接上网怎么处理...
- 自媒体必备工具:免费的音文对齐生成SRT字幕,快速打轴匹配声音及文字的在线工具
- 原创:Eclipse 上网代理设置(亲测有效)
- 我总结了70篇论文的方法,帮你透彻理解神经网络的剪枝算法
- python 音频数据归一化
- 让QQ群聊天记录自动保存到群空间中(转)
热门文章
- 服务器信号有杂音怎么回事,电脑麦克风有杂音滋滋怎么解决
- 【资源】这款工具让SpringBoot不再需要Controller、Service、DAO、Mapper!
- 中职计算机说课教法,2015教师资格证面试高中美术中职公共艺术美术篇说课稿 平面构成中的形象—活泼的点...
- 最长公共子序列 【DP】+【最长公共子序列】
- 关闭vscode链接检查,去掉VSCODE 编辑器的链接下划线
- MySQL UPDATE 更新
- 单精度浮点型(float)和双精度浮点型(double)的区别
- 原理解析!腾讯3轮面试都问了Android事件分发,已整理成文档
- pyinstaller打包exe文件的详细步骤及过程中遇到的问题
- TCP---拥塞控制