Skip to content

Latest commit

 

History

History
1215 lines (903 loc) · 73.3 KB

06.md

File metadata and controls

1215 lines (903 loc) · 73.3 KB

六、使用字符串

在某些时候,您的应用需要与人交流,这意味着使用文本;例如输出文本,将数据作为文本接收,然后将该数据转换为适当的类型。C++ 标准库拥有丰富的类集合,用于操作字符串、在字符串和数字之间进行转换,以及获取针对指定语言和区域性的本地化字符串值。

使用字符串类作为容器

C++ 字符串基于basic_string模板类。这个类是一个容器,所以它使用迭代器访问和方法来获取信息,并且有模板参数,这些参数包含关于它所保存的字符类型的信息。特定的字符类型有不同的typedef:

    typedef basic_string<char,
       char_traits<char>, allocator<char> > string; 
    typedef basic_string<wchar_t,
       char_traits<wchar_t>, allocator<wchar_t> > wstring; 
    typedef basic_string<char16_t,
       char_traits<char16_t>, allocator<char16_t> > u16string; 
    typedef basic_string<char32_t,
       char_traits<char32_t>, allocator<char32_t> > u32string;

string类基于charwstring基于wchar_t宽字符,16stringu32string类分别基于 16 位和 32 位字符。对于本章的其余部分,我们将只关注string类,但它同样适用于其他类。

比较、复制和访问字符串中的字符将需要不同大小字符的不同代码,而 traits 模板参数提供了实现。对于string来说,这是char_traits班。例如,当这个类复制字符时,它会将这个动作委托给char_traits类及其copy方法。流类也使用特性类,因此它们也定义了适合文件流的文件结束值。

字符串本质上是零个或多个字符的数组,当需要时分配内存,当string对象被破坏时释放内存。在某些方面,它非常类似于vector<char>对象。作为一个容器,string类通过beginend方法提供迭代器访问:

    string s = "hellon"; 
    copy(s.begin(), s.end(), ostream_iterator<char>(cout));

这里调用beginend方法从string,中的项目获取迭代器,迭代器从<algorithm>传递到copy函数,通过ostream_iterator临时对象将每个字符复制到控制台。在这方面,string对象类似于一个vector,所以我们使用之前定义的s对象:

vector<char> v(s.begin(), s.end()); 
copy(v.begin(), v.end(), ostream_iterator<char>(cout));

这将使用在string对象上使用beginend方法提供的字符范围填充vector对象,然后使用copy功能将这些字符打印到控制台,方式与我们之前使用的完全相同。

获取关于字符串的信息

max_size方法将给出计算机体系结构上指定字符类型的字符串的最大大小,这可能会非常大。例如,在具有 2 GB 内存的 64 位 Windows 计算机上,string对象的max_size将返回 40 亿个字符,而对于wstring对象,该方法将返回 20 亿个字符。这显然超过了机器中的内存!其他大小方法返回更有意义的值。length方法返回与size方法相同的值,即字符串中有多少项(字符)。capacity方法根据字符数指示已经为字符串分配了多少内存。

您可以通过调用其compare方法来比较一个string和另一个。这将返回一个int而不是一个bool(但是注意一个int可以无声地转换成一个bool,其中0的返回值意味着两个字符串是相同的。如果它们不相同,如果参数字符串大于操作数字符串,则此方法返回负值;如果参数小于操作数字符串,则返回正值。在这方面大于小于将测试字符串的字母顺序。此外,还有为<<===>=>定义的全局运算符来比较字符串对象。

一个string对象可以通过c_str方法像 C 弦一样使用。返回的指针是const;您应该知道,如果string对象发生变化,指针可能会失效,因此您不应该存储该指针。您不应该使用&str[0]为 C++ 字符串str获取 C 字符串指针,因为字符串类使用的内部缓冲区不能保证NUL被终止。提供c_str方法是为了返回一个可以作为 C 字符串的指针,因此NUL终止。

如果你想把数据从 C++ 字符串复制到 C 缓冲区,你可以调用copy方法。您将目标指针和要复制的字符数作为参数(以及可选的偏移量)传递,并且该方法将尝试最多将指定数量的字符复制到目标缓冲区:,但没有空终止字符。此方法假设目标缓冲区足够大,可以容纳复制的字符(您应该采取措施来确保这一点)。如果您想传递缓冲区的大小,以便该方法为您执行该检查,请调用_Copy_s方法。

改变字符串

字符串类有标准的容器访问方法,因此您可以使用at方法和[]运算符通过引用(读写访问)访问单个字符。您可以使用assign方法替换整个字符串,或者使用swap方法交换两个字符串对象的内容。此外,您可以使用insert方法在指定位置插入字符,使用erase方法删除指定字符,使用clear方法删除所有字符。该类还允许您使用push_backpop_back方法将字符推到字符串的末尾(并删除最后一个字符):

    string str = "hello"; 
    cout << str << "n"; // hello 
    str.push_back('!'); 
    cout << str << "n"; // hello! 
    str.erase(0, 1); 
    cout << str << "n"; // ello!

您可以使用append方法或+=运算符在字符串末尾添加一个或多个字符:

    string str = "hello"; 
    cout << str << "n";  // hello 
    str.append(4, '!'); 
    cout << str << "n";  // hello!!!! 
    str += " there"; 
    cout << str << "n";  // hello!!!! there

<string>库还定义了一个全局+运算符,该运算符将两个字符串连接成第三个字符串。

如果要更改字符串中的字符,可以使用[]运算符通过索引访问该字符,使用引用覆盖该字符。您也可以使用replace方法将指定位置的一个或多个字符替换为来自 C 字符串、C++ 字符串或通过迭代器访问的其他容器的字符:

    string str = "hello"; 
    cout << str << "n";    // hello 
    str.replace(1, 1, "a"); 
    cout << str << "n";    // hallo

最后,您可以提取字符串的一部分作为新字符串。substr方法采用偏移和可选计数。如果省略字符数,则子字符串将从指定位置开始,直到字符串结束。这意味着您可以通过传递偏移量 0 和小于字符串大小的计数来复制字符串的左边部分,或者您可以通过仅传递第一个字符的索引来复制字符串的右边部分。

    string str = "one two three"; 
    string str1 = str.substr(0, 3);  
    cout << str1 << "n";          // one 
    string str2 = str.substr(8); 
    cout << str2 << "n";          // three

在这段代码中,第一个示例将前三个字符复制到一个新字符串中。在第二个示例中,复制从第八个字符开始,一直持续到结尾。

搜索字符串

使用字符、C 字符串或 C++ 字符串传递find方法,您可以提供一个初始搜索位置来开始搜索。find方法返回搜索文本所在的位置(而不是迭代器),如果找不到文本,则返回npos值。offset 参数和来自find方法的成功返回值使您能够重复解析字符串以查找特定的项目。find方法向前搜索指定的文本,还有一个rfind方法反向搜索。

注意rfind并不是find方法的完全相反。find方法在字符串中向前移动搜索点,并在每一点将搜索字符串与来自搜索点的字符进行向前比较(因此第一个搜索文本字符,然后第二个字符,依此类推)。rfind方法将搜索点向后移动,但仍进行向前的比较。因此,假设rfind方法没有给定偏移量,第一次比较将在距离字符串末尾的偏移量处进行搜索文本的大小。然后,通过将搜索文本中的第一个字符与搜索字符串中搜索点处的字符进行比较来进行比较,如果成功,则将搜索文本中的第二个字符与搜索点之后的字符进行比较。因此,比较是在与搜索点移动方向相反的方向上进行的。

这变得很重要,因为如果您想使用来自find方法的返回值作为偏移量来解析字符串,那么在每次搜索之后,您应该将搜索偏移量向前移动**,对于rfind您应该将其向后移动。**

**例如,要搜索以下字符串中the的所有位置,可以调用:

    string str = "012the678the234the890"; 
    string::size_type pos = 0; 
    while(true) 
    { 
        pos++ ; 
        pos = str.find("the",pos); 
        if (pos == string::npos) break; 
        cout << pos << " " << str.substr(pos) << "n"; 
    } 
    // 3 the678the234the890 
    // 9 the234the890 
    // 15 the890

这将在字符位置 3、9 和 15 找到搜索文本。要向后搜索字符串,可以调用:

    string str = "012the678the234the890"; 
    string::size_type pos = string::npos; 
    while(true) 
    { 
        pos--; pos = str.rfind("the",pos); 
        if (pos == string::npos) break; 
        cout << pos << " " << str.substr(pos) << "n"; 
    } 
    // 15 the890 
    // 9 the234the890 
    // 3 the678the234the890

突出显示的代码显示了应该进行的更改,显示您需要从头开始搜索并使用rfind方法。当你有一个成功的结果时,你需要在下一次搜索前减少位置。与find方法一样,rfind方法如果找不到搜索文本,则返回npos

有四种方法可以让您搜索几个单独的字符之一。例如:

    string str = "012the678the234the890"; 
    string::size_type pos = str.find_first_of("eh"); 
    if (pos != string::npos) 
    { 
        cout << "found " << str[pos] << " at position "; 
        cout << pos << " " << str.substr(pos) << "n"; 
    } 
    // found h at position 4 he678the234the890

搜索字符串为eh,当在字符串中找到字符eh时,find_first_of将返回。在本例中,字符h首先出现在位置 4。您可以提供一个偏移参数来开始搜索,因此您可以使用来自find_first_of的返回值来解析字符串。find_last_of方法与此类似,但它会以相反的方向在字符串中搜索搜索文本中的一个字符。

除了搜索文本中提供的字符外,还有两种搜索方法可以查找字符*:find_first_not_offind_last_not_of。例如:*

    string str = "012the678the234the890"; 
    string::size_type pos = str.find_first_not_of("0123456789"); 
    cout << "found " << str[pos] << " at position "; 
    cout << pos << " " << str.substr(pos) << "n"; 
    // found t at position 3 the678the234the890

该代码寻找数字以外的字符,因此它在位置 3(第四个字符)找到t

没有库函数可以修剪string中的空白,但是您可以通过使用 find 函数查找非空白来修剪字符串左侧和右侧的空白,然后将其用作substr方法的适当索引。

    string str = "  hello  "; 
    cout << "|" << str << "|n";  // |  hello  | 
    string str1 = str.substr(str.find_first_not_of(" trn")); 
    cout << "|" << str1 << "|n"; // |hello  | 
    string str2 = str.substr(0, str.find_last_not_of(" trn") + 1); 
    cout << "|" << str2 << "|n"; // |  hello|

在前面的代码中,创建了两个新的字符串:一个左对齐空格,另一个右对齐空格。第一次向前搜索第一个非空白字符,并将其用作子字符串的起始索引(不提供计数,因为所有剩余的字符串都被复制)。在第二种情况下,对字符串进行反向搜索,寻找非空白字符,但返回的位置将是hello的最后一个字符;因为我们需要从第一个字符开始的子字符串,所以我们增加这个索引来获得要复制的字符数。

国际化

<locale>头包含用于本地化时间、日期和货币格式的类,还为字符串比较和排序提供本地化规则。

The C Runtime Library also has global functions to carry out localization. However, it is important in the following discussion that we distinguish between C functions and the C locale. The C locale is the default locale, including the rules for localization, used in C and C++ programs and it can be replaced with a locale for a country or culture. The C Runtime Library provides functions to change the locale, as does the C++ Standard Library.

由于 C++ 标准库提供了本地化类,这意味着您可以创建多个代表一个区域设置的对象。区域设置对象可以在函数中创建,并且只能在函数中使用,或者可以全局应用于线程,并且只能由该线程上运行的代码使用。这与 C 本地化函数形成对比,在 C 本地化函数中,更改区域设置是全局的,因此所有代码(以及所有执行线程)都会受到影响。

locale类的实例要么通过类构造函数创建,要么通过类的静态成员创建。C++ 流类将使用区域设置(如后面所解释的),如果您想要更改区域设置,您可以在流对象上调用imbue方法。在某些情况下,您可能希望直接访问这些规则中的一个,并且您可以通过 locale 对象访问它们。

使用刻面

国际化规则被称为方面。区域设置对象是一个方面的容器,您可以使用has_facet函数测试区域设置是否有特定的方面;如果是这样的话,你可以通过调用use_facet函数得到一个对方面的const引用。下表中有六种类型的方面,由七个类别概括。方面类是locale::facet嵌套类的子类。

| 刻面类型 | 描述 | | codecvtctype | 在一种编码方案和另一种编码方案之间进行转换,用于对字符进行分类并将其转换为大写或小写 | | collate | 控制字符串中字符的排序和分组,包括字符串的比较和散列 | | messages | 从目录中检索本地化邮件 | | money | 将表示货币的数字转换成字符串或从字符串转换成数字 | | num | 将数字转换为字符串或从字符串转换 | | time | 音乐会时间和日期以数字形式往返于字符串 |

facet 类用于将数据转换为字符串,因此它们都有一个用于所用字符类型的模板参数。moneynum,time面分别由三个类表示。带有_get后缀的类处理字符串解析,而带有_put后缀的类处理字符串格式。对于moneynum方面,有一个带有punct后缀的类,它包含标点符号的规则和符号。

由于_get方面用于将字符序列转换为数字类型,因此类有一个模板参数,您可以使用它来指示get方法将用来表示一系列字符的输入迭代器类型。类似地,_put方面类有一个模板参数,您可以使用它来提供输出迭代器类型put方法将把转换后的字符串写入其中。两种迭代器类型都有默认类型。

messages方面用于与 POSIX 代码兼容。该类旨在允许您为应用提供本地化字符串。其思想是用户界面中的字符串被索引,在运行时,您通过messages方面使用索引访问本地化的字符串。然而,Windows 应用通常使用使用消息编译器编译的消息资源文件。也许正是因为这个原因,作为标准库的一部分提供的messages方面没有做任何事情,但是基础设施在那里,并且您可以派生自己的messages方面类。

has_facetuse_facet函数是为您想要的特定类型的方面模板化的。所有方面类都是locale::facet类的子类,但是通过这个模板参数,编译器将实例化一个返回您请求的特定类型的函数。例如,如果您想为法语区域设置设置时间和日期字符串的格式,可以调用以下代码:

    locale loc("french"); 
    const time_put<char>& fac = use_facet<time_put<char>>(loc);

这里french字符串标识了区域设置,这是 C 运行时库setlocale函数使用的语言字符串。第二行获取将数字时间转换为字符串的方面,因此函数模板参数为time_put<char>。这个类有一个名为put的方法,您可以调用它来执行转换:

    time_t t = time(nullptr); 
    tm *td = gmtime(&t); 
    ostreambuf_iterator<char> it(cout); 
    fac.put(it, cout, ' ', td, 'x', '#'); 
    cout << "n";

time函数(通过<ctime>)返回一个带有当前时间和日期的整数,并使用gmtime函数将其转换为tm结构。tm结构包含年、月、日、小时、分钟和秒的单个成员。gmtime函数将地址返回到一个在函数中静态分配的结构中,因此您不必删除它所占用的内存。

刻面将通过作为第一个参数传递的输出迭代器将tm结构中的数据格式化为字符串。在这种情况下,输出流迭代器是由cout对象构造的,因此 facet 将格式流写入控制台(第二个参数没有使用,但是因为它是一个引用,所以您必须传递一些东西,所以cout对象也在那里使用)。第三个参数是分隔符(同样,这是不使用的)。第五个和第六个参数(可选)表示您需要的格式。这些格式字符与 C 运行时库函数strftime中使用的格式字符相同,是两个单一字符,而不是 C 函数使用的格式字符串。在本例中,x用于获取日期,#用作修饰符以获取字符串的长版本。

代码将给出以下输出:

    samedi 28 janvier 2017

注意单词没有大写,也没有标点符号,还要注意顺序:工作日名称,日数,月,然后年。

如果locale对象构造器参数变为german,那么输出将是:

    Samstag, 28\. January 2017

项目的顺序与法语相同,但单词大写,使用标点符号。如果使用turkish,那么结果是:

    28 Ocak 2017 Cumartesi

在这种情况下,星期几在字符串的末尾。

被共同语言划分的两个国家会给出两个不同的字符串,以下是americanenglish-uk的结果:

    Saturday, January 28, 2017
28 January 2017

这里以时间为例,因为没有流,对tm结构使用了插入运算符,这是一种不常见的情况。对于其他类型,有插入操作符将它们放入流中,因此流可以使用区域设置来国际化它显示类型的方式。例如,您可以在cout对象中插入一个double,该值将被打印到控制台。默认区域设置美国英语使用句点来分隔整数和小数,但在其他文化中使用逗号。

imbue函数将改变定位,直到随后调用该方法:

    cout.imbue(locale("american")); 
    cout << 1.1 << "n"; 
    cout.imbue(locale("french")); 
    cout << 1.1 << "n"; 
    cout.imbue(locale::classic());

这里,流对象被本地化为美国英语,然后浮点数1.1被打印在控制台上。接下来本地化改为法语,这次控制台会显示1,1。在法语中,小数点是逗号。最后一行通过传递从static classic方法返回的区域设置来重置流对象。这将返回所谓的 C 语言环境,这是 C 和 C++ 中的默认值,是美式英语。

static方法global可用于设置每个流对象默认使用的语言环境。当一个对象从一个流类创建时,它调用locale::global方法来获取默认区域设置。流克隆这个对象,这样它就有自己的副本,独立于随后通过调用global方法设置的任何本地副本。请注意,cincout流对象是在调用main函数之前创建的,这些对象将使用默认的 C 语言环境,直到您输入另一个语言环境。但是,需要指出的是,一旦创建了流,global方法对流没有影响,imbue是更改流所使用的区域设置的唯一方法。

global方法还将调用 C setlocale函数来更改 C 运行时库函数使用的区域设置。这很重要,因为一些 C++ 函数(例如to_stringstod,如下文所述)将使用 C 运行时库函数来转换值。然而,C 运行时库对 C++ 标准库一无所知,因此调用 C setlocale函数来更改默认区域设置不会影响随后创建的流对象。

值得指出的是,basic_string类使用模板参数指示的字符特征类来比较字符串。string类使用char_traits类及其版本的compare方法对两个字符串中的相应字符进行直接比较。这种比较没有考虑比较人物的文化规则。如果你想做一个使用文化规则的比较,你可以通过collate方面来做:

    int compare( 
       const string& lhs, const string& rhs, const locale& loc) 
    { 
        const collate<char>& fac = use_facet<collate<char>>(loc); 
        return fac.compare( 
            &lhs[0], &lhs[0] + lhs.size(), &rhs[0], &rhs[0] + rhs.size()); 
    }

字符串和数字

标准库包含在 C++ 字符串和数值之间转换的各种函数和类。

将字符串转换为数字

C++ 标准库包含名称类似stodstoi的函数,用于将 C++ string对象转换为数值(stod转换为doublestoi转换为integer)。例如:

    double d = stod("10.5"); 
    d *= 4; 
    cout << d << "n"; // 42

这将使用值10.5初始化浮点变量d,然后在计算中使用该值,并将结果打印在控制台上。输入字符串可能包含无法转换的字符。如果是这种情况,那么字符串的解析就在这一点上结束。您可以提供一个指向size_t变量的指针,该变量将被初始化为第一个不能转换的字符的位置:

    string str = "49.5 red balloons"; 
    size_t idx = 0; 
    double d = stod(str, &idx); 
    d *= 2; 
    string rest = str.substr(idx); 
    cout << d << rest << "n"; // 99 red balloons

在前面的代码中,idx变量将被初始化为值4,表示5r之间的空格是第一个不能转换为double的字符。

将数字转换为字符串

<string>库提供了to_string函数的各种重载,将整数类型和浮点类型转换成一个string对象。此函数不允许您提供任何格式细节,因此对于整数,您无法指示字符串表示的基数(例如十六进制),对于浮点转换,您无法控制有效数字的数量等选项。to_string功能是设施有限的简单功能。更好的选择是使用流类,如下节所述。

使用流类

您可以使用cout对象(一个ostream类的实例)将浮点数和整数打印到控制台,或者打印到一个带有ofstream实例的文件。这两个类都将使用成员方法和操纵器将数字转换为字符串,以影响输出字符串的格式。类似地,cin对象(T4】类的一个实例)和ifstream类可以从格式化的流中读取数据。

操纵器是引用流对象并返回该引用的函数。标准库有各种全局插入操作符,其参数是对流对象和函数指针的引用。适当的插入操作符将以流对象作为参数调用函数指针。这意味着操纵器可以访问并操纵它所插入的流。对于输入流,还有提取操作符,它们有一个函数参数,可以用流对象调用函数。

C++ 流的体系结构意味着在代码中调用的流接口和获取数据的底层基础设施之间有一个缓冲区。C++ 标准库提供了以字符串对象作为缓冲区的流类。对于输出流,在将项插入到流中之后访问字符串,这意味着字符串将包含根据这些插入操作符格式化的项。同样,您可以提供一个带有格式化数据的字符串作为输入流的缓冲区,当您使用提取操作符从流中提取数据时,您实际上是在解析字符串并将部分字符串转换为数字。

此外,流类有一个locale对象,流对象将调用该区域的转换方面,将字符序列从一种编码转换为另一种编码。

输出浮点数

<ios>库有操纵器,可以改变流处理数字的方式。默认情况下,输出流将以十进制格式为范围0.001100000,内的数字打印浮点数,对于超出该范围的数字,它将使用带有尾数和指数的科学格式。这种混合格式是defaultfloat操纵器的默认行为。如果您总是想使用科学符号,那么您应该将scientific操纵器插入到输出流中。

如果您想仅使用十进制格式显示浮点数(即小数点左侧的整数和右侧的小数部分),则使用fixed操纵器修改输出流。通过调用precision方法可以改变小数位数:

    double d = 123456789.987654321; 
    cout << d << "n"; 
    cout << fixed; 
    cout << d << "n"; 
    cout.precision(9); 
    cout << d << "n"; 
    cout << scientific; 
    cout << d << "n";

前面代码的输出是:

 1.23457e+08
 123456789.987654
 123456789.987654328
 1.234567900e+08

第一行显示科学符号用于大数。第二行显示fixed的默认行为,就是给小数加 6 位小数。这在代码中是通过调用precision方法给出 9 个小数位来改变的(在流中的<iomanip>库中插入setprecision操纵器可以达到同样的效果)。最后,格式从调用precision方法切换到尾数有 9 个小数位的科学格式。默认情况下,指数由小写的e标识。如果您愿意,您可以使用uppercase操纵器将其大写(小写使用nouppercase)。请注意,小数部分的存储方式意味着在有 9 个小数位的固定格式中,我们看到第九位数字是8而不是预期的1

也可以指定正数是否显示+符号;showpos操纵器将显示符号,但默认的noshowpos操纵器不会显示符号。showpoint机械手将确保即使浮点数是整数也显示小数点。默认为noshowpoint,表示没有小数部分,不显示小数点。

setw操纵器(在<iomanip>头中定义)可以用于整数和浮点数。实际上,该操纵器定义了流中的下一个(也是唯一的下一个)项目在控制台上打印时将占据的最小空间宽度:

    double d = 12.345678; 
    cout << fixed; 
    cout << setfill('#'); 
    cout << setw(15) << d << "n";

为了说明setw操纵器的效果,这段代码称为setfill操纵器,它表示应该打印一个散列符号(#)来代替空格。代码的其余部分表示应该使用固定格式(默认情况下为 6 位小数)打印数字,空格宽 15 个字符。结果是:

    ######12.345678

如果数字为负(或使用showpos,则默认情况下,符号与数字在一起;如果使用internal操纵器(在<ios>中定义),则符号将在为数字设置的空间中左对齐:

    double d = 12.345678; 
    cout << fixed; 
    cout << showpos << internal; 
    cout << setfill('#'); 
    cout << setw(15) << d << "n";

前面代码的结果如下:

    +#####12.345678

请注意,空格右侧的+符号由磅符号表示。

setw操纵器通常用于在格式化的列中输出数据表:

    vector<pair<string, double>> table 
    { { "one",0 },{ "two",0 },{ "three",0 },{ "four",0 } }; 

    double d = 0.1; 
    for (pair<string,double>& p : table) 
    { 
        p.second = d / 17.0; 
        d += 0.1; 
    } 

    cout << fixed << setprecision(6); 

    for (pair<string, double> p : table) 
    { 
        cout << setw(6)  << p.first << setw(10) << p.second << "n"; 
    }

这会用一个字符串和一个数字填充一对vectorvector用字符串值和零初始化,然后在for循环中改变浮点数(实际计算与此无关;重点是创建一些有多个小数位的数字)。数据分两列打印,数字以 6 位小数位打印。这意味着,包括前导零和小数点,每个数字将占用 8 个空格。文本列被指定为 6 个字符宽,数字列被指定为 10 个字符宽。默认情况下,当您指定列宽时,输出将右对齐,这意味着每个数字前面都有两个空格,并且根据字符串的长度填充文本。输出如下所示:

 one  0.005882
 two  0.011765
 three  0.017647
 four  0.023529

如果您希望一列中的项目左对齐,则可以使用left操纵器。这将影响所有列,直到使用right操纵器将对正改为向右:

    cout << fixed << setprecision(6) << left;

由此产生的输出将是:

 one   0.005882
 two   0.011765
 three 0.017647
 four  0.023529

如果您希望两列有不同的对齐方式,则需要在打印值之前设置对齐方式。例如,要左对齐文本,右对齐数字,请使用以下命令:

    for (pair<string, double> p : table) 
    { 
        cout << setw(6) << left << p.first  
            << setw(10) << right << p.second << "n"; 
    }

前面代码的结果如下:

 one     0.005882
 two     0.011765
 three   0.017647
 four    0.023529

输出整数

整数也可以使用setwsetfill方法按列打印。您可以插入操纵器来打印基数为 8 ( oct)、基数为 10 ( dec)和基数为 16 ( hex)的整数。(也可以使用setbase操纵器,传递想要使用的基数,但只允许 8、10、16 三个值。)数字可以用指示的基数打印(八进制以0为前缀,六进制以0x为前缀),也可以不用showbasenoshowbase操纵器打印。如果使用hex,那么9上面的数字就是字母af,默认为小写。如果您希望这些是大写的,那么您可以使用uppercase操纵器(小写的带有nouppercase)。

输出时间和金钱

<iomanip>中的put_time函数被传递一个用时间和日期以及格式字符串初始化的tm结构。该函数返回_Timeobj类的一个实例。顾名思义,你并不真的需要创建这个类的变量;相反,应该使用函数将特定格式的时间/日期插入到流中。有一个插入操作符将打印一个_Timeobj对象。该函数是这样使用的:

    time_t t = time(nullptr); 
    tm *pt = localtime(&t); 
    cout << put_time(pt, "time = %X date = %x") << "n";

由此产生的输出是:

    time = 20:08:04 date = 01/02/17

该函数将使用流中的区域设置,因此如果您在流中注入一个区域设置,然后调用put_time,时间/日期将使用该区域设置的格式字符串和时间/日期本地化规则进行格式化。格式字符串使用strftime的格式标记:

    time_t t = time(nullptr); 
    tm *pt = localtime(&t); 
    cout << put_time(pt, "month = %B day = %A") << "n"; 
    cout.imbue(locale("french")); 
    cout << put_time(pt, "month = %B day = %A") << "n";

前面代码的输出是:

 month = March day = Thursday
 month = mars day = jeudi

类似地,put_money函数返回一个_Monobj对象。同样,这只是传递给此函数的参数的容器,不需要使用此类的实例。相反,您应该将此函数插入到输出流中。实际工作发生在获取当前区域设置的 money 方面的插入操作符中,该操作符使用它将数字格式化为适当的小数位数并确定小数点字符;如果使用千位分隔符,在插入到适当的位置之前,使用什么字符。

    Cout << showbase; 
    cout.imbue(locale("German")); 
    cout << "German" << "n"; 
    cout << put_money(109900, false) << "n"; 
    cout << put_money("1099", true) << "n"; 
    cout.imbue(locale("American")); 
    cout << "American" << "n"; 
    cout << put_money(109900, false) << "n"; 
    cout << put_money("1099", true) << "n";

前面代码的输出是:

 German
 1.099,00 euros
 EUR10,99
 American
 $1,099.00
 USD10.99

您可以在double或字符串中提供一个数字作为欧分或美分,put_money函数使用适当的小数点(,表示德国人,.表示美国人)和适当的千位分隔符(.表示德国人,,表示美国人)将数字格式化为欧元或美元。将showbase操纵器插入输出流意味着put_money功能将显示货币符号,否则将只显示格式化的数字。put_money功能的第二个参数指定是使用货币字符(false)还是国际符号(true)。

使用流将数字转换为字符串

流缓冲类负责从适当的源(文件、控制台等)获取字符和写入字符,并从抽象类basic_streambuf派生自<streambuf>。这个基类定义了两个虚拟方法,overflowunderflow,,它们被派生类重写,以向与派生类相关联的设备写入字符和从该设备读取字符。stream buffer 类执行获取或放入项到流中的基本操作,由于 buffer 处理字符,该类是用字符类型和字符特征的参数模板化的。

顾名思义,如果使用basic_stringbuf,流缓冲区将是一个字符串,因此读取字符的来源和写入字符的目的地是该字符串。如果使用此类为流对象提供缓冲区,这意味着您可以使用为流编写的插入或提取运算符将格式化数据写入字符串或从字符串中读取格式化数据。basic_stringbuf缓冲区是可扩展的,因此当您在流中插入项目时,缓冲区将适当扩展。有typedef,这里的缓冲是一个string ( stringbuf)或者一个wstring ( wstringbuf)。

例如,假设您已经定义了一个类,并且还定义了一个插入操作符,这样您就可以将其与cout对象一起使用,将值打印到控制台:

    struct point 
    { 
        double x = 0.0, y = 0.0; 
        point(){} 
        point(double _x, double _y) : x(_x), y(_y) {} 
    }; 
    ostream& operator<<(ostream& out, const point& p) 
    { 
        out << "(" << p.x << "," << p.y << ")"; 
        return out; 
    }

将它用于cout对象很简单——考虑下面这段代码:

    point p(10.0, -5.0); 
    cout << p << "n";         // (10,-5)

您可以使用stringbuf将格式化的输出定向到字符串,而不是控制台:

    stringbuf buffer;  
    ostream out(&buffer); 
    out << p; 
    string str = buffer.str(); // contains (10,-5)

由于流对象处理格式化,这意味着您可以插入任何有插入操作符的数据类型,并且您可以使用任何ostream格式化方法和任何操纵器。所有这些方法和操纵器的格式化输出将被插入缓冲区中的字符串对象。

另一种选择是使用<sstream>中的basic_ostringstream类。该类基于用作缓冲区的字符串的字符类型(因此string版本为ostringstream)。它是从ostream类派生出来的,所以你可以在任何使用ostream对象的地方使用实例。格式化结果可通过str方法访问:

    ostringstream os; 
    os << hex; 
    os << 42; 
    cout << "The value is: " << os.str() << "n";

该代码获取十六进制(2a)中42的值;这是通过在流中插入hex操纵器,然后插入整数来实现的。通过调用str方法获得格式化字符串。

使用流从字符串中读取数字

cin对象是istream类的一个实例(在<istream>库中),可以从控制台输入字符,并将其转换为您指定的数字形式。<ifstream>库中的ifstream类还允许您从文件中输入字符并将其转换为数字形式。与输出流一样,可以将流类与字符串缓冲区一起使用,这样就可以从字符串对象转换为数值。

<sstream>库中的basic_istringstream类是从basic_istream类派生的,因此您可以创建流对象并从这些对象中提取项目(数字和字符串)。该类在一个字符串对象上提供这个流接口(关键字typedefistringstream基于一个stringwistringstream基于一个wstring)。当您构造这个类的对象时,您用一个包含数字的string初始化对象,然后您使用>>操作符提取基本内置类型的对象,就像您使用cin从控制台提取那些项目一样。

需要重申的是,提取操作符将空格视为流中各项之间的分隔符,因此它们将忽略所有前导空格,读取下一个空格之前的非空格字符,并尝试将该子字符串转换为适当的类型,如下所示:

    istringstream ss("-1.0e-6"); 
    double d; 
    ss >> d;

这将使用-1e-6的值初始化d变量。如同cin,一样,你必须知道流中项目的格式;因此,如果您试图提取一个整数,而不是从前面示例中的字符串中提取一个double,那么当它到达小数点时,对象将停止提取字符。如果某些字符串没有转换,您可以将其余的提取到字符串对象中:

    istringstream ss("-1.0e-6"); 
    int i; 
    ss >> i; 
    string str; 
    ss >> str; 
    cout << "extracted " << i << " remainder " << str << "n";

这将在控制台上打印以下内容:

    extracted -1 remainder .0e-6

如果字符串中有多个数字,可以通过多次调用>>运算符来提取这些数字。该流还支持一些操纵器。例如,如果字符串中的数字是hex格式,您可以使用hex操纵器通知流这种情况,如下所示:

    istringstream ss("0xff"); 
    int i; 
    ss >> hex; 
    ss >> i;

这表示字符串中的数字是十六进制格式,变量i将被初始化为值 255。如果字符串包含非数值,那么 stream 对象仍会尝试将字符串转换为适当的格式。在下面的代码片段中,您可以通过调用fail函数来测试这样的提取是否失败:

    istringstream ss("Paul was born in 1942"); 
    int year; 
    ss >> year; 
    if (ss.fail()) cout << "failed to read number" << "n";

如果知道字符串包含文本,可以将其提取到字符串对象中,但请记住,空白字符被视为分隔符:

    istringstream ss("Paul was born in 1942"); 
    string str; 
    ss >> str >> str >> str >> str; 
    int year; 
    ss >> year;

这里,数字前有四个字,所以代码读一个string四次。如果不知道数字在字符串中的位置,但知道字符串中有一个数字,可以移动内部缓冲区指针,直到它指向一个数字:

    istringstream ss("Paul was born in 1942"); 
    string str;    
    while (ss.eof() && !(isdigit(ss.peek()))) ss.get(); 
    int year; 
    ss >> year; 
    if (!ss.fail()) cout << "the year was " << year << "n";

peek方法返回当前位置的字符,但不移动缓冲区指针。该代码检查该字符是否为数字,如果不是,则通过调用get方法移动内部缓冲区指针。(这段代码测试eof方法,以确保在缓冲区结束后没有读取字符的尝试。)如果您知道数字从哪里开始,那么您可以调用seekg方法将内部缓冲区指针移动到指定位置。

<istream>库有一个名为ws的操纵器,可以从流中移除空白。回想一下,我们之前说过,没有从字符串中删除空白的函数。这是真的,因为ws操纵器从中移除空白,而不是从字符串中移除空白,但是由于您可以使用字符串作为流的缓冲区,这意味着您可以使用此函数间接从字符串中移除空白:

    string str = "  hello  "; 
    cout << "|" << str1 << "|n"; // |  hello  | 
    istringstream ss(str); 
    ss >> ws; 
    string str1; 
    ss >> str1; 
    ut << "|" << str1 << "|n";   // |hello|

ws函数本质上遍历输入流中的项目,当字符不是空白时返回。如果流是文件或控制台流,则ws功能将从这些流中读入字符;在这种情况下,缓冲区由已经分配的字符串提供,因此它跳过了字符串开头的空白。请注意,流类将后续空格视为流中值之间的分隔符,因此在本例中,流将从缓冲区读入字符,直到出现空格,并且本质上将向左- 和向右修剪字符串。然而,这不一定是你想要的。如果字符串中有几个单词用空格填充,那么这段代码将只提供第一个单词。

<iomanip>库中的get_moneyget_time操纵器允许您使用区域设置的金钱和时间方面从字符串中提取金钱和时间:

    tm indpday = {}; 
    string str = "4/7/17"; 
    istringstream ss(str); 
    ss.imbue(locale("french")); 
    ss >> get_time(&indpday, "%x"); 
    if (!ss.fail())  
    { 
       cout.imbue(locale("american")); 
       cout << put_time(&indpday, "%x") << "n";  
    }

在前面的代码中,首先使用法语格式的日期(日/月/年)初始化流,然后使用区域设置的标准日期表示通过get_time提取日期。日期被解析成tm结构,然后使用put_time以美国地区的标准日期表示打印出来。结果是:

    7/4/2017

使用正则表达式

正则表达式是一种文本模式,正则表达式解析器可以使用它在字符串中搜索与该模式匹配的文本,如果需要,可以用其他文本替换匹配的项。

定义正则表达式

一个正则表达式 ( 正则表达式)由定义模式的字符组成。表达式包含对解析器有意义的特殊符号,如果您想在表达式的搜索模式中使用这些符号,那么您可以用反斜杠(\)来转义它们。您的代码通常会将表达式作为string对象传递给regex类的实例作为构造函数参数。该对象然后被传递给<regex>中的函数,该函数将使用表达式来解析匹配模式的序列的文本。

下表总结了regex类可以匹配的一些模式。

| 图案 | 解释 | | | 文字 | 匹配精确的字符 | li匹配flip lip plier | | [组] | 匹配组中的单个字符 | [at]匹配catcattoppear | | [^group] | 匹配不在组中的单个字符 | [^at]匹配 c at,t o p,到 pp 耳朵,p e ar,豌豆 r | | [第一个-最后一个] | 匹配范围firstlast中的任何字符 | [0-9]匹配数字 1 02,1 0 2,10 2 | | {n} | 该元素被精确匹配 n 次 | 91{2} 匹配 911 | | {n,} | 元素被匹配 n 次或更多次 | wel{1,}匹配well欢迎到来 | | {n,m} | 该元素匹配 n 到 m 次 | 9{2,4}匹配9999999999999 9 但不匹配 9 | | 。 | 通配符,除n外的任何字符 | a.e匹配ateare | | * | 元素被匹配零次或多次 | d*.d匹配.10.110.1但不匹配 10 | | + | 元素被匹配一次或多次 | d*.d匹配0.110.1但不是 10 或. 1 | | ? | 元素被匹配零次或一次 | tr?ap匹配traptap | | | | 匹配由| | th(e&#124;is&#124;at)匹配thethisthat | | [[:class:]] | 匹配字符类 | [[:upper:]]匹配大写字符:I am R ichard | | n | 匹配换行符 | | | s | 匹配任何单个空格 | | | d | 匹配任何一个数字 | d[0-9] | | w | 匹配可以在单词中的字符(大写和小写字符) | | | b | 匹配字母数字字符和非字母数字字符之间的边界 | d{2}b匹配 9 99和 99 99 bd{2}匹配99 9 和99 99 | | $ | 行尾 | s$匹配一行末尾的一个空格 | | ^ | 行首 | ^d如果一行以数字开头,则匹配 |

您可以使用正则表达式来定义要匹配的模式——Visual c++ 编辑器允许您在搜索对话框中这样做(这是开发表达式的一个很好的测试平台)。

定义要匹配的模式要比定义要匹配的模式而不是容易得多。例如,表达式w+b<w+>将匹配字符串"vector<int>",因为它有一个或多个单词字符后跟一个非单词字符(<),后跟一个或多个单词字符后跟>。该模式与字符串"#include <regex>"不匹配,因为include后面有一个空格,b表示字母数字字符和非字母数字字符之间有边界。

表格中的th(e|is|at)示例显示,当您想要提供替代方案时,可以使用括号来对模式进行分组。然而,括号还有另一个用途——它们允许您捕获组。因此,如果您想要执行替换操作,您可以搜索一个模式作为一个组,然后稍后将该组称为一个命名的子组(例如,搜索(Joe)以便您可以将Joe替换为Tom)。您也可以引用表达式中由括号指定的子表达式(称为反向引用):

    ([A-Za-z]+) +1

这个表达式表示:搜索一个或多个字符在 A 到 Z 和 A 到 Z 范围内的单词;这个单词叫做 1,所以找到它出现两次的地方,中间留一个空格

标准库类

要执行匹配或替换,必须创建一个正则表达式对象。这是类basic_regex的一个对象,它有字符类型的模板参数和一个正则表达式特征类。这个类有两个typedef:charregex和【宽字符】的wregex,它们具有regex_traitswregex_traits类所描述的特征。

traits 类决定了 regex 类如何解析表达式。例如,回想一下以前的文本,您可以使用w表示单词,d表示数字,s表示空格。[[::]]语法允许您为字符类使用更具描述性的名称:alnumdigitlower等等。由于这些是依赖于字符集的文本序列,因此 traits 类将有适当的代码来测试表达式是否使用了受支持的字符类。

适当的正则表达式类将解析表达式,以使<regex>库中的函数能够使用该表达式来识别某些文本中的模式:

    regex rx("([A-Za-z]+) +1");

这将使用反向引用来搜索重复的单词。请注意,正则表达式使用1作为反向引用,但是在字符串中,反斜杠必须被转义(\)。如果你使用像sd这样的角色类,那么你需要进行大量的逃跑。相反,您可以使用原始字符串(R"()"),但请记住,引号内的第一组括号是原始字符串语法的一部分,并不构成正则表达式组:

    regex rx(R"(([A-Za-z]+) +1)");

哪个更易读完全取决于你;两者都在双引号中引入了额外的字符,这可能会混淆快速浏览正则表达式匹配的内容。

请记住,正则表达式本身本质上是一个程序,因此regex解析器将确定该表达式是否有效,如果它不是对象,构造函数将抛出类型为regex_error的异常。异常处理将在下一章中解释,但重要的是要指出,如果异常没有被捕获,它将导致应用在运行时中止。异常的what方法将返回错误的基本描述,code方法将返回regex_constants命名空间中的error_type枚举中的一个常量。没有指示表达式中错误发生的位置。您应该在外部工具中彻底测试您的表达式(例如 Visual C++ 搜索)。

构造函数可以用一个字符串(C 或 C++)或一对迭代器来调用字符串(或其他容器)中的一系列字符,或者您可以传递一个初始化列表,其中列表中的每个项目都是一个字符。regex 语言有各种不同的风格;basic_regex类的默认值是 ECMAScript 。如果您想要不同的语言(基本 POSIX、扩展 POSIX、awk、grep 或 egrep),您可以将在regex_constants命名空间中的syntax_option_type枚举中定义的常量之一(副本也可以作为在basic_regex类中定义的常量)作为构造函数参数传递。

您只能指定一种语言风格,但可以将其与其他一些syntax_option_type常量结合使用:icase指定不区分大小写,collate在匹配中使用区域设置,nosubs表示您不想捕获组,optimize优化匹配。

该类使用方法getloc获取解析器使用的区域设置,imbue重置区域设置。如果你imbue一个地区,那么你将不能使用regex对象做任何匹配,直到你用assign方法重置它。这意味着有两种方法可以使用regex对象。如果要使用当前区域设置,则将正则表达式传递给构造函数:如果要使用不同的区域设置,用默认构造函数创建一个空的regex对象,然后用区域设置调用imbue,用assign方法传递正则表达式。解析完正则表达式后,您可以调用mark_count方法来获取表达式中的捕获组数量(假设您没有使用nosubs)。

匹配表达式

一旦你构造了一个regex对象,你可以把它传递给<regex>库中的方法来搜索字符串中的模式。regex_match函数通过字符串(C 或 C++)或迭代器传递给容器中的一系列字符和一个构造的regex对象。最简单的形式是,只有当表达式与搜索字符串完全匹配时,函数才会返回true:

    regex rx("[at]"); // search for either a or t 
    cout << boolalpha; 
    cout << regex_match("a", rx) << "n";  // true 
    cout << regex_match("a", rx) << "n";  // true 
    cout << regex_match("at", rx) << "n"; // false

在前面的代码中,搜索表达式是针对给定范围内的单个字符(at,因此对regex_match的前两次调用返回true,因为搜索到的字符串是一个字符。最后一次调用返回false,因为匹配和搜索到的字符串不一样。如果去掉正则表达式中的[],那么第三次调用将返回true,因为您正在寻找精确的字符串at。如果正则表达式是[at]+以便您寻找一个或多个字符at,那么这三个调用都会返回true。您可以通过传递match_flag_type枚举中的一个或多个常量来改变匹配的确定方式。

如果您将对match_results对象的引用传递给该函数,那么在搜索之后,该对象将包含关于匹配的位置和字符串的信息。match_results对象是一个包含sub_match对象的容器。如果函数成功,则意味着整个搜索字符串与表达式匹配,在这种情况下,返回的第一个sub_match项将是整个搜索字符串。如果表达式有子组(用括号标识的模式),那么这些子组将是match_results对象中的附加sub_match对象。

    string str("trumpet"); 
    regex rx("(trump)(.*)"); 
    match_results<string::const_iterator> sm; 
    if (regex_match(str, sm, rx)) 
    { 
        cout << "the matches were: "; 
        for (unsigned i = 0; i < sm.size(); ++ i)  
        { 
            cout << "[" << sm[i] << "," << sm.position(i) << "] "; 
        } 
        cout << "n"; 
    } // the matches were: [trumpet,0] [trump,0] [et,5]

这里,表达式是文字trump后跟任意数量的字符。整个字符串与这个表达式匹配,并且有两个子组:文字字符串trump和去除trump后剩下的任何东西。

match_results类和sub_match类都是基于用于指示匹配项的迭代器类型的模板。模板参数分别为const char*const wchar_t*的有typedef调用的cmatchwcmatch,参数分别为stringwstring对象中使用的迭代器的有smatchwsmatch(类似的还有子匹配类:csub_matchwcsub_matchssub_matchwssub_match)。

regex_match函数可能非常严格,因为它寻找模式和搜索字符串之间的精确匹配。regex_search函数更灵活,因为如果搜索字符串中有一个子字符串与表达式匹配,它将返回true。请注意,即使搜索字符串中有多个匹配项,regex_search功能也只会找到第一个。如果您想解析字符串,您必须多次调用该函数,直到它指示不再有匹配项。这就是迭代器访问搜索字符串的重载变得有用的地方:

    regex rx("bd{2}b"); 
    smatch mr; 
    string str = "1 4 10 42 100 999"; 
    string::const_iterator cit = str.begin(); 
    while (regex_search(cit, str.cend(), mr, rx)) 
    { 
        cout << mr[0] << "n"; 
        cit += mr.position() + mr.length(); 
    }

这里,表达式将匹配一个由空白包围的 2 位数(d{2})(两个b模式意味着前后的边界)。这个循环从一个指向字符串开头的迭代器开始,当找到匹配项时,这个迭代器会增加到那个位置,然后增加匹配项的长度。regex_iterator对象,进一步解释,包装这种行为。

类赋予迭代器对所包含的对象的访问权,这样你就可以使用远程的 T2。最初,容器似乎以一种奇怪的方式工作,因为它知道sub_match对象在搜索到的字符串中的位置(通过position方法,该方法获取子匹配对象的索引),但是sub_match对象似乎只知道它所引用的字符串。然而,仔细观察sub_match类,它显示它是从pair派生的,其中两个参数都是字符串迭代器。这意味着sub_match对象具有指定子字符串的原始字符串中的范围的迭代器。match_result对象知道原始字符串的开始,可以使用sub_match.first迭代器来确定子字符串开始的字符位置。

match_result对象有一个[]运算符(和str方法),返回指定组的子串;这将是一个使用原始字符串中字符范围的迭代器构造的字符串。prefix方法返回匹配前的字符串,suffix方法返回匹配后的字符串。所以,在之前的代码中,第一个匹配将是10,前缀将是1 4,后缀将是42 100 999。相比之下,如果访问sub_match对象本身,它只知道它的长度和字符串,这是通过调用str方法获得的。

match_result对象也可以通过format方法返回结果。这采用一个格式字符串,其中匹配的组通过由$符号($1$2等)标识的编号占位符来标识。输出可以是流,也可以作为字符串从方法返回:

    string str("trumpet"); 
    regex rx("(trump)(.*)"); 
    match_results<string::const_iterator> sm; 
    if (regex_match(str, sm, rx)) 
    { 
        string fmt = "Results: [$1] [$2]"; 
        cout << sm.format(fmt) << "n"; 
    } // Results: [trump] [et]

使用regex_matchregex_search,可以使用括号来标识子组。如果模式匹配,那么您可以使用通过引用该函数传递的适当的match_results对象来获得这些子组。如前所示,match_results对象是sub_match对象的容器。子匹配可以与<!===<=>>=运算符进行比较,这些运算符比较迭代器指向的项(即子字符串)。此外,sub_match对象可以插入到流中。

使用迭代器

该库还为正则表达式提供了一个迭代器类,这为解析字符串提供了一种不同的方法。因为类将涉及字符串的比较,所以它是以元素类型和特征为模板的。该类将需要遍历字符串,因此第一个模板参数是字符串迭代器类型,元素和特征类型可以从中推导出来。regex_iterator类是一个前向迭代器,所以它有一个++ 操作符,并且它提供了一个*操作符来访问一个match_result对象。在前面的代码中,您看到一个match_result对象被传递给了regex_matchregex_search函数,这些函数使用它来包含它们的结果。这就提出了什么代码填充通过regex_iterator访问的match_result对象的问题。答案在于迭代器的++ 运算符:

    string str = "the cat sat on the mat in the bathroom"; 
    regex rx("(b(.at)([^ ]*)"); 
    regex_iterator<string::iterator> next(str.begin(), str.end(), rx); 
    regex_iterator<string::iterator> end; 

    for (; next != end; ++ next) 
    { 
        cout << next->position() << " " << next->str() << ", "; 
    } 
    cout << "n"; 
    // 4 cat, 8 sat, 19 mat, 30 bathroom

在该代码中,在字符串中搜索第二个和第三个字母为at的单词。b表示模式必须在单词的开头(.表示单词可以以任何字母开头)。这三个字符周围有一个捕获组,除空格外,还有一个或多个字符的第二个捕获组。

迭代器对象next由要搜索的字符串和regex对象的迭代器构成。++ 操作员基本上调用regex_search功能,同时保持该地点的位置以执行下一次搜索。如果搜索没有找到模式,那么操作符返回序列的结尾迭代器,它是由默认构造函数(在这个代码中是end对象)创建的迭代器。这段代码打印出了完全匹配,因为我们使用了str方法的默认参数(0)。如果您想要匹配实际的子字符串,使用str(1),结果将是:

    4 cat, 8 sat, 19 mat, 30 bat

由于*(和->)操作符赋予了对match_result对象的访问权,所以您也可以访问prefix方法来获取匹配之前的字符串,suffix方法将返回匹配之后的字符串。

regex_iterator类允许您迭代匹配的子字符串,而regex_token_iterator更进一步,它还允许您访问所有子匹配。在使用上,本类除了构造上与regex_iterator,相同。regex_token_iterator构造器有一个参数来指示您希望通过*操作器访问哪个子匹配。值-1表示您想要前缀,值0表示您想要整个匹配,值1或以上表示您想要编号的子匹配。如果你愿意,你可以传递一个带有你想要的子匹配类型的int vector或 C 数组:

    using iter = regex_token_iterator<string::iterator>; 
    string str = "the cat sat on the mat in the bathroom"; 
    regex rx("b(.at)([^ ]*)");  
    iter next, end; 

    // get the text between the matches 
    next = iter(str.begin(), str.end(), rx, -1); 
    for (; next != end; ++ next) cout << next->str() << ", "; 
    cout << "n"; 
    // the ,  ,  on the ,  in the , 

    // get the complete match 
    next = iter(str.begin(), str.end(), rx, 0); 
    for (; next != end; ++ next) cout << next->str() << ", "; 
    cout << "n"; 
    // cat, sat, mat, bathroom, 

    // get the sub match 1 
    next = iter(str.begin(), str.end(), rx, 1); 
    for (; next != end; ++ next) cout << next->str() << ", "; 
    cout << "n"; 
    // cat, sat, mat, bat, 

    // get the sub match 2 
    next = iter(str.begin(), str.end(), rx, 2); 
    for (; next != end; ++ next) cout << next->str() << ", "; 
    cout << "n"; 
    // , , , hroom,

替换字符串

regex_replace方法与其他方法类似,它采用一个字符串(一个 C 字符串或 C++ string对象,或一系列字符的迭代器)、regex对象和可选标志。此外,该函数有一个格式字符串,并返回一个string。格式字符串本质上是从匹配结果传递到正则表达式的每个results_match对象的format方法。然后,该格式化字符串被用作相应匹配子字符串的替换。如果没有匹配项,则返回搜索到的字符串的副本。

    string str = "use the list<int> class in the example"; 
    regex rx("b(list)(<w*> )"); 
    string result = regex_replace(str, rx, "vector$2"); 
    cout << result << "n"; // use the vector<int> class in the example

在前面的代码中,我们说整个匹配的字符串(应该是list<后面跟着一些文本后面跟着>和一个空格)应该替换为vector,后面跟着第二子匹配(<后面跟着一些文本后面跟着>和一个空格)。结果是list<int>将被vector<int>取代。

使用字符串

该示例将以文本文件形式读入电子邮件并进行处理。互联网邮件格式的电子邮件分为两部分:邮件头和邮件正文。这是简单的处理,因此不会尝试处理 MIME 电子邮件正文格式(尽管这段代码可以作为起点)。邮件正文将在第一个空行后开始,互联网标准规定该行长度不应超过 78 个字符。如果长度较长,不得超过 998 个字符。这意味着换行符(回车符、换行符对)用于维护这个规则,并且一个段落的结尾用一个空行来表示。

标题更复杂。最简单的形式是,一个标题在一行上,形式为name:value。标头名称与标头值之间用冒号隔开。标题可以使用一种称为折叠空白的格式拆分为多行,其中拆分标题的换行符位于空白(空格、制表符等)之前。这意味着以空白开始的一行是前一行标题的延续。标题通常包含由分号分隔的name=value对,因此能够分隔这些子项非常有用。有时这些子项没有值,即会有一个以分号结尾的子项。

该示例将一封电子邮件作为一系列字符串,并使用这些规则创建一个对象,该对象包含一组标题和一个包含正文的字符串。

创建项目

为项目创建一个文件夹,并创建一个名为email_parser.cpp的 C++ 文件。由于此应用将读取文件并处理字符串,因此添加适当库的 includes 并添加代码以从命令行获取文件名:

    #include <iostream> 
    #include <fstream> 
    #include <string> 

    using namespace std; 

    void usage() 
    { 
        cout << "usage: email_parser file" << "n"; 
        cout << "where file is the path to a file" << "n"; 
    } 

    int main(int argc, char *argv[]) 
    { 
        if (argc <= 1) 
        { 
            usage(); 
            return 1; 
        } 

        ifstream stm; 
        stm.open(argv[1], ios_base::in); 
        if (!stm.is_open()) 
        { 
            usage(); 
            cout << "cannot open " << argv[1] << "n"; 
            return 1; 
        } 

        return 0; 
    }

标题将有一个名称和一个正文。正文可以是单个字符串,也可以是一个或多个子项。创建一个类来表示头的主体,并且暂时把它当作一行。在usage函数上方添加以下类:

    class header_body 
    { 
        string body; 
    public: 
        header_body() = default; 
        header_body(const string& b) : body(b) {} 
        string get_body() const { return body; } 
    };

这只是将类包裹在一个string周围;稍后我们将添加代码来分离body数据成员中的子项。现在创建一个类来表示电子邮件。在header_body类后添加以下代码:

    class email 
    { 
        using iter = vector<pair<string, header_body>>::iterator; 
        vector<pair<string, header_body>> headers; 
        string body; 

    public: 
        email() : body("") {} 

        // accessors 
        string get_body() const { return body; } 
        string get_headers() const; 
        iter begin() { return headers.begin(); } 
        iter end() { return headers.end(); } 

        // two stage construction 
        void parse(istream& fin); 
    private: 
        void process_headers(const vector<string>& lines); 
    };

headers数据成员将标题保存为名称/值对。这些项目存储在一个vector而不是一个map中,因为当一封电子邮件从一个邮件服务器传递到另一个邮件服务器时,邮件中已经存在的每个服务器可能会添加邮件头,所以邮件头是重复的。我们可以使用multimap,但是我们将失去标题的顺序,因为multimap将按照有助于搜索项目的顺序存储项目。

A vector按照项目插入容器的顺序保存项目,由于我们将连续解析电子邮件,这意味着headers数据成员的标题项目与电子邮件中的顺序相同。添加适当的包含,以便您可以使用vector类。

正文和标题作为单个字符串有访问器。此外,还有从headers数据成员返回迭代器的访问器,这样外部代码就可以遍历headers数据成员(这个类的完整实现将有允许您按名称搜索标题的访问器,但是对于本例来说,只允许迭代)。

该类支持两阶段构造,其中大部分工作是通过将输入流传递给parse方法来完成的。parse方法在电子邮件中读取为一个vector对象中的一系列行,并调用私有函数process_headers,将这些行解释为标题。

get_headers方法很简单:它只是遍历标题,并在格式为name: value的每一行放置一个标题。添加内联函数:

    string get_headers() const 
    { 
        string all = ""; 
        for (auto a : headers) 
        { 
            all += a.first + ": " + a.second.get_body(); 
            all += "n"; 
        } 
        return all; 
    }

接下来,您需要从文件中读入电子邮件,并提取邮件正文和邮件头。main函数已经有了打开文件的代码,所以创建一个email对象,并将文件的ifstream对象传递给parse方法。现在使用访问器打印出解析后的电子邮件。在main功能的末尾添加以下内容:

 email eml; eml.parse(stm); cout << eml.get_headers(); cout << "n"; cout << eml.get_body() << "n"; 

        return 0; 
    }

email类声明之后,添加parse函数的定义:

    void email::parse(istream& fin) 
    { 
        string line; 
        vector<string> headerLines; 
        while (getline(fin, line)) 
        { 
            if (line.empty()) 
            { 
                // end of headers 
                break; 
            } 
            headerLines.push_back(line); 
        } 

        process_headers(headerLines); 

        while (getline(fin, line)) 
        { 
            if (line.empty()) body.append("n"); 
            else body.append(line); 
        } 
    }

这个方法很简单:它反复调用<string>库中的getline函数来读取一个string,直到检测到一个换行符。在方法的前半部分,字符串存储在vector中,然后传递给process_headers方法。如果读入的字符串是空的,这意味着一个空行已经被读入——在这种情况下,所有的标题都已经被读入。在方法的后半部分,读入电子邮件的正文。

getline函数将用于格式化电子邮件的换行符剥离为 78 个字符的行长度,因此循环只是将这些行作为一个字符串追加。如果一个空行被读入,它表示一个段落的结束,因此一个换行符被添加到正文字符串中。

parse方法后,增加process_headers方法:

    void email::process_headers(const vector<string>& lines) 
    { 
        string header = ""; 
        string body = ""; 
        for (string line : lines) 
        { 
            if (isspace(line[0])) body.append(line); 
            else 
            { 
                if (!header.empty()) 
                { 
                    headers.push_back(make_pair(header, body)); 
                    header.clear(); 
                    body.clear(); 
                } 

                size_t pos = line.find(':'); 
                header = line.substr(0, pos); 
                pos++ ; 
                while (isspace(line[pos])) pos++ ; 
                body = line.substr(pos); 
            } 
        } 

        if (!header.empty()) 
        { 
            headers.push_back(make_pair(header, body)); 
        } 
    }

这段代码遍历集合中的每一行,当它有一个完整的头时,它将字符串拆分为冒号上的名称/正文对。在循环中,第一行测试第一个字符是否是空白;如果没有,则检查header变量是否有值;如果是,在清除headerbody变量之前,名称/主体对存储在类headers数据成员中。

下面的代码对从集合中读取的行进行操作。这段代码假设这是标题行的开始,因此在字符串中搜索冒号并在这一点上进行拆分。标题的名称在冒号之前,标题的正文(去掉前导空白)在冒号之后。由于我们不知道标题正文是否会折叠到下一行,因此不存储名称/正文;取而代之的是,允许while循环再重复一次,这样就可以测试下一行的第一个字符,看它是否是空白,如果是,就将其追加到正文中。保持名称/主体对直到while循环的下一次迭代的动作意味着最后一行不会存储在循环中,因此在方法的末尾有一个测试来查看header变量是否为空,如果不是,则存储名称/主体对。

现在可以编译代码(记得使用/EHsc开关)测试没有错别字。要测试代码,您应该将来自电子邮件客户端的电子邮件保存为文件,然后使用该文件的路径运行email_parser应用。以下是互联网消息格式 RFC 5322 中给出的示例电子邮件之一,您可以将其放入文本文件中以测试代码:

    Received: from x.y.test
 by example.net
 via TCP
 with ESMTP
 id ABC12345
 for <mary@example.net>;  21 Nov 1997 10:05:43 -0600
Received: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600
From: John Doe <jdoe@node.example>
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.node.example>

This is a message just to say hello.
So, "Hello".

您可以用一封电子邮件来测试应用,以表明解析已经考虑了标题格式,包括折叠空白。

正在处理标题子项

下一个操作是将标题正文处理为子项。为此,将以下突出显示的声明添加到header_body类的public部分:

    public: 
        header_body() = default; 
        header_body(const string& b) : body(b) {} 
        string get_body() const { return body; } 
        vector<pair<string, string>> subitems(); 
    };

每个子项都是一个名称/值对,由于子项的顺序可能很重要,因此子项存储在vector中。更改main功能,取消对get_headers的调用,改为单独打印每个标题:

    email eml; 
    eml.parse(stm); 
    for (auto header : eml) { cout << header.first << " : "; vector<pair<string, string>> subItems = header.second.subitems(); if (subItems.size() == 0) { cout << header.second.get_body() << "n"; } else { cout << "n"; for (auto sub : subItems) { cout << "   " << sub.first; if (!sub.second.empty()) 
                cout << " = " << sub.second;         
                cout << "n"; } } } 
    cout << "n"; 
    cout << eml.get_body() << endl;

由于email类实现了beginend方法,这意味着远程for循环将调用这些方法来访问email::headers数据成员上的迭代器。每个迭代器将访问一个pair<string,header_body>对象,所以在这段代码中,我们首先打印出标题名,然后访问header_body对象上的子项。如果没有子项,表头还是会有一些文字,但是不会拆分成子项,所以我们调用get_body方法,让字符串打印出来。如果有子项,则打印出来。有些物品会有身体,有些不会。如果项目有正文,则该子项以name = value的形式打印。

最后一个操作是解析标题正文,将它们拆分为子项。在header_body类下面,添加方法的定义:

    vector<pair<string, string>> header_body::subitems() 
    { 
        vector<pair<string, string>> subitems; 
        if (body.find(';') == body.npos) return subitems; 

        return subitems; 
    }

由于子项是用分号分隔的,因此有一个简单的测试来寻找body字符串上的分号。如果没有分号,则返回空的vector

现在代码必须重复解析字符串,提取子项。有几个案例需要解决。大多数子项都是name=value;,形式,因此必须提取该子项,并在等于字符处拆分,并丢弃分号。

有些子项没有值,并且是name;形式,在这种情况下,分号将被丢弃,并且子项值以空字符串存储。最后,标题中的最后一项不能以分号结束,因此必须考虑这一点。

添加以下while循环:

    vector<pair<string, string>> subitems; 
    if (body.find(';') == body.npos) return subitems; 
    size_t start = 0;
 size_t end = start; while (end != body.npos){}

顾名思义,start变量是子项的起始索引,end是子项的结束索引。第一个动作是忽略任何空白,所以在while循环内添加:

    while (start != body.length() && isspace(body[start])) 
    { 
        start++ ; 
    } 
    if (start == body.length()) break;

这只是在引用空白字符时增加start索引,只要它没有到达字符串的末尾。如果到达字符串的末尾,这意味着没有更多的字符,因此循环结束。

接下来,添加以下内容来搜索=;字符,并处理其中一种搜索情况:

    string name = ""; 
    string value = ""; 
    size_t eq = body.find('=', start); 
    end = body.find(';', start); 

    if (eq == body.npos) 
    { 
        if (end == body.npos) name = body.substr(start); 
        else name = body.substr(start, end - start); 
    } 
    else 
    {
    } 
    subitems.push_back(make_pair(name, value)); 
    start = end + 1;

如果找不到搜索到的项目,find方法将返回npos值。第一个调用寻找=字符,第二个调用寻找分号。如果找不到=,那么该物品没有价值,只有一个名字。如果找不到分号,则表示name是从start索引到字符串末尾的整个字符串。如果有分号,则name是从start索引开始直到end指示的索引(因此要复制的字符数是end-start)。如果找到一个=字符,那么字符串需要在这一点上被拆分,并且该代码将在一会儿后显示。一旦namevalue变量被赋予值,这些变量被插入到subitems数据成员中,并且start索引被移动到end索引之后的字符。如果end指数为npos,则start指数的值将无效,但这并不重要,因为while循环将测试end指数的值,如果指数为npos,则将打破循环。

最后,您需要添加子项中有=字符时的代码。添加以下突出显示的文本:

    if (eq == body.npos) 
    { 
        if (end == body.npos) name = body.substr(start); 
        else name = body.substr(start, end - start); 
    } 
    else 
    { 
 if (end == body.npos) { name = body.substr(start, eq - start); value = body.substr(eq + 1); } else { if (eq < end) { name = body.substr(start, eq - start); value = body.substr(eq + 1, end - eq - 1); } else { name = body.substr(start, end - start); } } 
    }

第一行测试分号搜索是否失败。在这种情况下,名称从start索引开始,直到等于字符之前的字符,值是等于符号之后的文本,直到字符串结束。

如果等号和分号字符有有效的索引,那么还有一种情况需要检查。equals 字符的位置可能在分号之后,这意味着该子项没有值,而 equals 字符将用于后续子项。

此时,您可以编译代码,并使用包含电子邮件的文件进行测试。程序的输出应该是邮件拆分为标题和正文,每个标题拆分为子项,可以是简单的字符串,也可以是name=value对。

摘要

在本章中,您已经看到了支持字符串的各种 C++ 标准库类。您已经看到了如何从流中读取字符串,如何将字符串写入流,如何在数字和字符串之间转换,以及如何使用正则表达式操作字符串。当您编写代码时,您将不可避免地花时间运行您的代码,以检查它是否按照您的规范工作。这将包括提供检查算法结果的代码、将中间代码记录到调试设备的代码,当然还有在调试器下运行代码。下一章是关于调试代码的!**