《C++ Primer Plus》第16章:string类和标准模板库(1)

标准 C++ string 类
模板 auto_ptr、unique_ptr、shared_ptr
标准模板库(STL)
容器类
迭代器
函数对象 ( functor)
STL 算法
模板 initializer_list

至此,您熟悉了 C++ 可重用代码的目标,这样做的一个很大的回报是可以重用别人编写的代码,这正是类库的用户之地。有很多商业C++类库,也有一些库是C++程序包自带的。例如,曾使用过的头文件 iostream 支持的输入/输出类。本章介绍一些其他可重用代码,它们将给编程工作带来快乐。

本书前面介绍过 string 类,本章将更深入地讨论它;然后介绍“智能指针”模板类,它们让管理动态内存更容易;接下来介绍标准模板库(STL),它是一组用于处理各种容器对象的模板。STL演示了一种编程模式——泛型编程;最后,本章将介绍 C++ 新增的模板 initializer_list,它让您能够将初始化列表语法用于 STL 对象。

string 类

很多应用程序都需要处理字符串。C 语言在 string.h(在 C++ 中为 cstring)中提供了一系列的字符串函数,很多早期的 C++ 实现为处理字符串提供了自己的类。第4章介绍了 ANSI/ISO C++ string 类,而第12章创建了一个不大的 String 类,以说明设计表示字符串的类的某些方面。

string 类是由头文件 string 支持的(注意,头文件 string.h 和 cstring 支持对 C-风格字符串进行操纵的 C 库字符串函数,但不支持 string 类)。要使用类,关键在于知道它的公有接口,而 string 类包含大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。简而言之,string 类包含的内容很多。

构造字符串

先来看 string 的构造函数。毕竟,对于类而言,最重要的内容之一是,有哪些方法可用于创建其对象。下面的程序使用了 string 的 7 个构造函数(用 ctor 标识,这是传统 C++ 中构造函数的缩写)。下表简要地描述了这些构造函数,它首先使用顺序简要描述了下面的程序使用的7个构造函数,然后列出了C++11新增的两个构造函数。使用构造函数时都进行了简化,即隐藏了这样的一个事实:string实际上时模板具体化 basic_string 的一个 typedef,同时省略了与内存管理相关的参数(这将在本章后面和附录F中讨论)。size_type 是一个依赖于实现的整型,是在头文件 string 中定义的。 string 类将 string::npos 定义为字符串的最大长度,通常为 unsigned int 的最大值。另外,表格中使用缩写 NBTS(null-terminated string) 来表示以空字符结束的字符串——传统的 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() 创建一个默认的 string 对象,长度为 0(默认构造函数)
string(const char * s, size type n) 将 string 对象初始化为 s 指向的 NBTS 的前 n 个字符,即使超过了 NBTS 的结尾
template string(Iter begin, Iter end) 将string对象初始化为区间 [begin,end) 内的字符,其中 begin 和 end 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括 end
string(const string & str, 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 il 这是 C++11 新增的,它将一个 string 对象初始化为初始化列表 il 中的字符
// str1.cpp -- introducing the string class

#include
#include

// using string constructors

int main(){
    using namespace std;
    string one("Lottery Winner!");      // ctor #1
    cout << one << endl;                // overloaded <<
    string two(20, '$');                // ctor #2
    cout << two << endl;
    string three(one);                  // ctor #3
    cout << three << endl;
    one += "Oops!";                     // overloaded +=
    cout << one << endl;
    two = "Sorry! That was ";
    three[0] = 'P';
    string four;                        // ctor #4
    four = two + three;                 // overloaded +, =
    cout << four << endl;
    char alls[] = "All's well that ends well";
    string five(alls, 20);              // ctor #5
    cout << five << "!\n";
    string six(alls+6, alls+10);        // ctor #6
    cout << six << ", ";
    string seven(&five[6], &five[10]);  // ctor #6 again
    cout << seven << "...\n";
    string eight(four, 7, 16);          // ctor #7
    cout << eight << " in motion!" << endl;
    return 0;
}

上面的程序还使用了重载+=运算符,它将一个字符串附加到另一个字符串的后面;重载的=运算符用于将一个字符串赋给另一个字符串;重载的<<运算符用于显式string对象;重载的[]运算符用于访问字符串中的各个字符。

下面是该程序的输出:

Lottery Winner!
$$$$$$$$$$$$$$$$$$$$
Lottery Winner!
Lottery Winner!Oops!
Sorry! That was Pottery Winner!
All's well that ends!
well, well...
That was Pottery in motion!
  1. 程序说明
    上面的程序首先演示了可以将string对象初始化为常规的 C-风格字符串,然后使用重载的<<运算符来显示它:

    string one("Lottery Winner!");		// ctor #1
    cout << one << endl;				// overloaded <<
    

    接下来的构造函数将 string 对象 two 初始化为由 20 个$字符组成的字符串:

    string two(20, '$');				// ctor #2
    

    复制构造函数将 string 对象 three 初始化为 string 对象 one:

    string three(one);					// ctor #3
    

    重载的+=运算符将字符串 “Oops!” 附加到字符串 one 的后面:

    one += "Oops!";						// overloaded +=
    

    这里是将一个 C-风格字符串附加到一个 string 对象的后面。但 += 运算符被多次重载,以便能够附加 string 对象和单个字符:

    one += two;			// append a string object (not in program)
    one += '!';			// append a type char value (not in program)
    

    同样,= 运算符也被重载,以便可以将 string 对象、C-风格字符串或 char 值赋给 string 对象:

    two = "Sorry! That was ";		// assign a C-style string
    two = one;						// assign a string object (not in program)
    two = '?';						// assign a char value (not in grogram)
    

    重载[] 运算符(就像第12章的 String 示例那样)使得可以使用数组表示法来访问 string 对象中的各个字符:

    three[0] = 'P';
    

    默认构造函数创建一个以后可对其进行赋值的空字符串:

    string four;			// ctor #4
    four = two + three;		// overloaded +, =
    

    第 2 行使用重载的 + 运算符创建了一个临时 string 对象,然后使用重载的=运算符将它赋给对象four。正如所预料的,+ 运算符将其两个操作数组合成一个 string 对象。该运算符被多次重载,以便第二个操作数可以是 string 对象,C-风格字符串或 char 值。

    第 5 个构造函数将一个 C-风格字符串和一个整数作为参数,其中的整数参数表示要复制多少个字符:

    char alls[] = "All's well that ends well";
    string five(alls, 20);		// ctor #5
    

    从输出可知,这里只使用了前20个字符(“All’s well that ends”)来初始化 five 对象。正如上表指出的,如果字符数超过了 C-风格字符串的长度,仍将复制请求数目的字符。所以在上面的例子中,如果用 40 代替 20,将导致 15 个无用字符被复制到 five 的结尾处(即构造函数将内存中位于字符串“All’s well that ends well" 后面的内容作为字符。
    第6个构造函数有一个模板参数:

    template string(Iter begin, Iter end);
    

    begin 和 end 将像指针那样,指向内存中两个位置(通常,begin 和 end 可以是迭代器——广泛用于 STL 中的广义化指针)。构造函数将使用begin和end指向的位置之间的值,对string对象进行初始化。[begin,end)来自数学中,意味着包括 begin,但不包括 end 在内的区间。也就是说,end 指向被使用的最后一个值后面的一个位置。请看下面的语句:

    string six(alls+6, alls+10);		// ctor #6
    

    由于数组名相当于指针,所以 alls + 6 和 alls + 10 的类型都是 char*,因此使用模板时,将用类型 char * 替换 Iter。第一个参数指向数组 alls 中的第一个 w,第二个参数指向第一个 well 后面的空格。因此,six 将被初始化为字符串 “well”。

    现在假设要用这个构造函数将对象初始化为另一个 string 对象(假设为five)的一部分内容,则下面的语句不管用:

    string seven(five+6, five +10);
    

    原因在于,对象名(不同于数组名)不会被看作是对象的地址,因此 five 不是指针,所以 five+6 是没有意义的。然而,five[6] 是一个 char 值,所以 &five[6] 是一个地址,因此可被用作该构造函数的一个参数。

    string seven(&five[6], &five[10]);	// ctor #6 again
    

    第7个构造函数将一个 string 对象的部分内容复制到构造的对象中:

    string eight(four, 7, 16);		// ctor #7
    

    上述语句从 four 的第8个字符(位置7)开始,将16个字符复制到 eight 中。

  2. C++11 新增的构造函数
    构造函数 string(string && str)类似于复制构造函数,导致新创建的 string 为 str 的副本。但与复制构造函数不同的是,它不保证将 str 视为 const。这种构造函数被称为移动构造函数(move constructor)。在有些情况下,编译器可使用它而不是复制构造函数,以优化性能。第18章的“移动语义和右值引用”一节将讨论这个主题。
    构造函数 string (initializer_listil)让您能够将列表初始化语法用于 string 类。也就是说,它使得下面这样的声明是合法的:

    string piano_man = { 'L', 'i', 's', 'z', 't' };
    string comp_lang { 'L', 'i', 's', 'p' };
    

    就 string 类而言,这可能用处不大,因为使用 C-风格字符串更容易,但确实实现了让列表初始化语法普遍实用的意图。本章后面将更深入的讨论模板 initializer_list。

string 类输入

对于类,很有帮助的另一点是,知道有哪些输入方式可用。对于 C-风格字符串,有3种方式:

char info[100];
cin >> info;		// read a word
cin.getline(info, 100);		// read a line, discard \n
cin.get(info,100);		// read a line, leave \n in queue

对于 string 对象,有两种方式:

string stuff;
cin >> stuff;		// read a word
getline(cin, stuff);		// read a line, discard \n

两个版本的 getline() 都有一个可选参数,用于指定使用哪个字符来确定输入的边界:

cin.getline(info, 100, ':' )	// read up to :, discard :
getline(stuff, ':' );		// read up to:, discard :

在功能上,它们之间的主要区别在于,string 版本的 getline() 将自动调整目标string对象的大小,使之刚好能够存储输入的字符:

char fname[10];
string lname;
cin >> fname; 	// could be a problem if input size > 9 characters
cin >> lname;	// can read a very, very long word
cin.getline(fname, 10);		// may truncate input
getline(cin, fname);			// no truncation

自动调整大小的功能让 string 版本的 getline() 不需要指定读取多少个字符的数值参数。
在设计方面的一个区别是,读取 C-风格字符串的函数是 istream 类的方法,而 string 版本是独立的函数。这就是对于 C-风格字符串输入,cin 是调用对象;而对于 string 对象输入,cin 是一个函数参数的原因。这种规则也适用于 >> 形式,如果使用函数形式来编写代码,这一点将显而易见:

cin.operator>>(fname);		// ostream class method
operator>>(cin, lname);		// regular function

下面更深入地探讨一下 string 输入函数。正如前面指出的,这两个函数都自动调整目标 string 的大小,使之与输入匹配。但也存在一些限制。第一个限制因素是 string 对象的最大允许长度,由常量 string::npos 指定。这通常是最大的 unsigned int 值,因此对于普通的交互式输入,这不会带来实际的限制;但是如果您试图将整个文件的内容读取到单个string对象中,这可能成为限制因素。第二个限制因素是程序可以使用的内存量。

string 版本的 getline() 函数从输入中读取字符,并将其存储到目标 string 中,直到发生下列三种情况之一:

  • 到达文件尾,在这种情况下,输入流的 eofbit 将被设置,这意味着方法 fail() 和 eof() 都将返回 true;
  • 遇到分界字符(默认为\n),在这种情况下,将把分界字符从输入流中删除,但不存储它;
  • 读取的字符数达到最大允许值(string::nops 和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的 failbit,这意味着方法 fail() 将返回 true。

输入流对象有一个统计系统,用于跟踪流的错误状态。在这个系统中,检测到文件尾后将设置 eofbit 寄存器,检测到输入错误时将设置 failbit 寄存器,出现无法识别的故障(如硬盘故障)时将设置 badbit 寄存器,一切顺利时将设置 goodbit 寄存器。第17章将更深入地讨论这一点。

string 版本的 operator>>() 函数的行为与此类似,只是它不断读取,直到遇到空白字符并将其留在输入队列中,而不是不断读取,直到遇到分界字符并将其丢弃。空白字符指的是空格、换行符和制表符,更普遍地说,是任何将其作为参数来调用 isspace() 时,该函数返回 true 的字符。

本书前面有多个控制台 string 输入示例。由于用于 string 对象的输入函数使用输入流,能够识别文件尾,因此也可以使用它们来从文件中读取输入。下面的程序是一个从文件中读取字符串的简短示例,它假设文件中包含用冒号字符分隔的字符串,并使用指定分界符的 getline() 方法。然后,显示字符串并给它们编号,每个字符串占一行。

// strfile.cpp -- read strings from a file

#include
#include
#include
#include

int main(){
    using namespace std;

    ifstream fin;
    fin.open("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){
        ++count;
        cout << count << ": "<< item << endl;
        getline(fin, item, ':');
    }
    cout << "Done\n";
    fin.close();
    return 0;
}

下面是文件 tobuy.txt 的内容:

sardines:chocolate ice cream:pop corn:leeks:
cottage cheese:olive oil:butter:tofu:

通常,对于程序要查找的文本文件,应将其放在可执行程序或项目文件所在的目录中;否则必须提供完整的路径名。在 Windows 系统中,C-风格字符串中的转义序列\表示一个斜杠:

fin.open("C:\\CPP\\Progs\\tobuy.txt");		// file = C:\CPP\Progs\\tobuy.txt

下面是程序的输出:

1: sardines
2: chocolate ice cream
3: pop corn
4: leeks
5:
cottage cheese
6: olive oil
7: butter
8: tofu
9:

Done

注意,将:指定为分界字符后,换行符将被视为常规字符。因此文件 tobuy.txt 中第一行末尾的换行符将成为包含“cottage cheese” 的字符串中的第一个字符。同样第二行末尾的换行符是第9个输入字符串中唯一的内容。

你可能感兴趣的:(C++,c++,开发语言)