第 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中的字符

阐述一下新增的两个函数的用法:

  1. 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_ptrunique_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_ptrunique_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_ptrshared_ptr,使用new[ ]分配内存时,不能使用它们;

  • 不使用new分配内存时,不能使用auto_ptrshared_ptr

  • 不使用newnew[ ]分配内存时,不能使用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); // 可以避免显式地使用迭代器变量
    
  • 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_iteratorostream_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_iteratorfront_insert_iteratorinsert_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容器类型:dequeC++11新增的forward_listlistqueuepriority_queuestackvector 都是序列。

队列 可以在 队尾添加 元素,在 队首删除 元素。

deque 表示的 双端队列 允许在 两端添加和删除 元素。

序列要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素、除第一个元素和最后一个元素外,每个元素前后都有一个元素。

数组和链表是序列,但是分支结构不是。

对于序列,都支持以下的操作:

在情况允许下,复杂都是固定时间。下表还列出了一些STL容器的其他操作:

解释上表的一部分操作:

  1. a[n]与a.at(t)都返回一个指向容器中第n个元素的引用。它们的区别是a.at(t)会检查边界,若超出边界就会报out_of_range的错
  2. 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
  1. insert与splice的区别:

    insert将原始区间的副本插入到目标地址,而splice()则将原始区间移动到目标地址。因此,在one与three合并后,one就变为空了。

  2. 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,一个键可与多个值相关联。

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类和标准模板库相关推荐

  1. 《C++ Primer Plus》读书笔记 第16章 string类和标准模板库

    第16章 string类和标准模板库 1.string类 表16.1列出了string类的几个构造函数.其中NBTS(null-terminated string)表示以空字符结束的传统C-风格字符串 ...

  2. C++Primer Plus笔记——第十六章 string类和标准模板库总结及程序清单

    目录 本章小结 程序清单 string类 16.1 str1.cpp                16.2 strfile.cpp            16.3 hangman.cpp      ...

  3. 《C++ Primer Plus》学习笔记-string类和标准模板库

    第16章 string类和标准模板库(本书附录G有详细介绍) 16.1 string类 string类是由头文件string支持的. string类位于命名空间std中. ctor标识是传统C++中构 ...

  4. C++ Primer Plus 笔记(16章:string类和标准模板库)

    16 string类和标准模板库 16.1 string类 16.1.1 构造字符串 常见的字符串书中给了7个,另外还有两个在C++11里新增的(NTBS)表示以空字符结束的传统字符串 构造函数 描述 ...

  5. C++ Primer plus学习笔记-第十六章:string类和标准模板库

    第十六章:string类和标准模板库 前言:这一章已经相当靠近全书的后面部分了:这一章我们会深入探讨一些技术上的细节,比如string的具体构造函数,比如适用于string类的几个函数,比如我们还会介 ...

  6. 【String类和标准模板库】

    1.string类 2.智能指针模板类 3.标准模板库 4.泛型编程 5.函数对象 6.算法 7.其他库 1.string类 string类是由头文件string支持的,要使用类,关键要知道它的公有接 ...

  7. 蓝桥杯算法竞赛系列第0章——蓝桥必考点及标准模板库STL(上)(万字博文,建议抱走)

    欢迎来到:遇见蓝桥遇见你,不负代码不负卿! 目录 ​ 一.蓝桥必考点剖析 二.什么是STL 三.vector的常见用法详解 1.vector的定义 2.vector容器内元素的访问 (1).通过下标访 ...

  8. Visual C++ 2010 第10章 标准模板库

    10.1 标准模板库的定义 STL 是为本地C++编译器提供的一个类与函数模板的大集合. STL 包含6种组件:容器.容器适配器.迭代器.算法.函数对象和函数适配器. 因为STL组件时标准库的一部分, ...

  9. C++ 笔记(19)— 标准模板库(STL容器、STL迭代器、STL算法、STL容器特点、STL字符串类)

    C++ 标准库可以分为两部分: 标准函数库: 这个库是由通用的.独立的.不属于任何类的函数组成的.函数库继承自 C 语言. 面向对象类库: 这个库是类及其相关函数的集合. C++ 标准库包含了所有的 ...

最新文章

  1. 【Qt】Qt多屏编程,在指定显示屏上显示指定对话框
  2. WCF技术剖析之三十:一个很有用的WCF调用编程技巧[下篇]
  3. Perl正则表达式--练习1
  4. Object类的用法(一)
  5. 漫步最优化三十三——牛顿法
  6. 一加Z配置细节曝光:处理器大变 为了更好的进入欧洲市场?
  7. PHP的几个常用加密函数
  8. java流程图表示输入 输出_流程图 - 迷途行者 - 博客园
  9. 思科交换机命令大全 一
  10. JDBC - new Date插入mysql数据库,数据库时间多一秒问题
  11. 【转载】log4j日志
  12. linux 安装minio并设置开机自启动
  13. 使用VLC把视频转换为一帧一帧的图片
  14. 【QT】将指定ip添加到凭据管理器
  15. 信号满格怎么显示无法连接服务器,Win7 32系统网络信号满格却无法连接上网怎么处理...
  16. 自媒体必备工具:免费的音文对齐生成SRT字幕,快速打轴匹配声音及文字的在线工具
  17. 原创:Eclipse 上网代理设置(亲测有效)
  18. 我总结了70篇论文的方法,帮你透彻理解神经网络的剪枝算法
  19. python 音频数据归一化
  20. 让QQ群聊天记录自动保存到群空间中(转)

热门文章

  1. 服务器信号有杂音怎么回事,电脑麦克风有杂音滋滋怎么解决
  2. 【资源】这款工具让SpringBoot不再需要Controller、Service、DAO、Mapper!
  3. 中职计算机说课教法,2015教师资格证面试高中美术中职公共艺术美术篇说课稿 平面构成中的形象—活泼的点...
  4. 最长公共子序列 【DP】+【最长公共子序列】
  5. 关闭vscode链接检查,去掉VSCODE 编辑器的链接下划线
  6. MySQL UPDATE 更新
  7. 单精度浮点型(float)和双精度浮点型(double)的区别
  8. 原理解析!腾讯3轮面试都问了Android事件分发,已整理成文档
  9. pyinstaller打包exe文件的详细步骤及过程中遇到的问题
  10. TCP---拥塞控制