Skip to content

Latest commit

 

History

History
1201 lines (897 loc) · 73.9 KB

File metadata and controls

1201 lines (897 loc) · 73.9 KB

九、使用字符串

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

使用 String 类作为容器

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类,但它同样适用于其他类。

对于不同大小的字符,比较、复制和访问字符串中的字符需要不同的代码,而特征模板参数提供了实现。 对于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 亿个字符。 这显然超过了机器中的内存! 其他 Size 方法返回更有意义的值。 length方法返回与size方法相同的值,即字符串中有多少项(字符)。 capacity方法根据字符数指示已经为字符串分配了多少内存。

您可以通过调用compare方法将string与另一个进行比较。 这将返回int而不是bool(但请注意,可以将int静默转换为bool),其中返回值0表示两个字符串相同。 如果它们不相同,则如果参数字符串大于操作数字符串,则此方法返回负值;如果参数小于操作数字符串,则返回正值。 在这方面,大于小于将按字母顺序测试字符串的排序。 此外,还为<<===>=>定义了用于比较字符串对象的全局运算符。

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

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

更改字符串

String 类具有标准的容器访问方法,因此您可以通过使用at方法和[]运算符的引用(读写访问)来访问单个字符。 您可以使用assign方法替换整个字符串,或者使用swap方法交换两个 String 对象的内容。 此外,可以使用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

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

搜索字符串

find方法是使用字符、C 字符串或 C++ 字符串传递的,您可以提供初始搜索位置来开始搜索。 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,当find_first_of在字符串中找到字符eh时,它将返回。 在本例中,字符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 | 将数字形式的时间和日期与字符串相互转换 |

刻面类用于将数据转换为字符串,因此它们都有一个用于所用字符类型的模板参数。 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 Runtime Librarysetlocale函数使用的语言字符串。 第二行获取将数字时间转换为字符串的方面,因此函数模板参数为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结构,这是一种不寻常的情况。 对于其他类型,有插入操作符将它们放入流中,因此流可以使用区域设置来国际化其显示类型的方式。 例如,您可以将double插入到cout对象中,该值将打印到控制台。 默认的区域设置是美式英语,使用句点来分隔整数和小数部分,但在其他文化中使用逗号。

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方法还将调用 Csetlocale函数来更改 C 运行时库函数使用的区域设置。 这一点很重要,因为一些 C++ 函数(例如to_stringstod,如下文所述)将使用 C 运行时库函数来转换值。 但是,C 运行时库对 C++ 标准库一无所知,因此调用 Csetlocale函数来更改默认区域设置不会影响后续创建的流对象。

值得指出的是,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对象(istream类的实例)和ifstream类可以从格式化的流中读取数据。

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

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

此外,流类有一个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"; 
    }

这将用一个字符串和一个数字填充vector对。 用字符串值和零来初始化vector,然后在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对象。 同样,这只是传递给此函数的参数的容器,您不应该使用此类的实例。 相反,您需要将此函数插入到输出流中。 实际工作发生在获取当前语言环境中的货币方面的插入操作符中,它使用该操作符将数字格式化为适当的小数位数并确定小数点字符;如果使用千位分隔符,则在将其插入到适当的位置之前使用哪个字符。

    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)。

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

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

顾名思义,如果使用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格式化方法和任何操纵器。 所有这些方法和操纵器的格式化输出将被插入到缓冲区中的 String 对象中。

另一种选择是使用<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>库中)还允许您从文件中输入字符并将其转换为数字形式。 与输出流一样,您可以使用带有字符串缓冲区的流类,以便可以将字符串对象转换为数字值。

basic_istringstream类(在<sstream>库中)派生自basic_istream类,因此您可以创建流对象并从这些对象中提取项目(数字和字符串)。 该类在一个 String 对象上提供这个流接口(typedef的关键字istringstream基于 astringwistringstream基于 awstring)。 在构造此类对象时,使用包含数字的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。 如果字符串包含非数字值,则流对象仍会尝试将字符串转换为适当的格式。 在下面的代码片段中,您可以通过调用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

使用正则表达式

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

定义正则表达式

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

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

| 图案 | 说明 | 示例 | | 字面意思 | 完全匹配的字符 | li匹配flip``lip``plier | | [组] | 匹配组中的单个字符 | [at]catcattoppear匹配 | | [^组] | 匹配不在组中的单个字符 | [^at]cat,top 匹配到ppear,peAr,豌豆r | | [倒数第一名] | 匹配范围firstlast中的任何字符 | [0-9]匹配数字102、102、102 | | {n} | 该元素恰好匹配 n 次 | 91{2}与911匹配 | | {n,} | 元素匹配 n 次或更多次 | wel{1,}匹配well和**,欢迎**到来 | | {n,m} | 元素匹配 n 到 m 次 | 9{2,4}匹配99999999999999,但不匹配 9 | | 。 | 通配符,除n以外的任何字符 | a.eateare匹配 | | *** | 元素匹配零次或多次 | d*.d.10.110.1匹配,但不匹配 10 | | ++ | 元素匹配一次或多次 | d*.d0.110.1匹配,但与 10 或.1 不匹配 | | ? | 元素匹配零次或一次 | tr?aptraptap匹配 | | ||| | 匹配由|分隔的任何一个元素 | th(e&#124;is&#124;at)thethisthat匹配 | | [[:class:]] | 匹配字符类 | [[:upper:]]匹配大写字符:IamRichard | | 毫微 / 中性的 / 脚注 / 名词 | 匹配换行符 | | | 南方 / 款 / 秒 / 先令 | 匹配任何单个空格 | | | 双角动量的 / 女儿 / 一天 / 白天 | 匹配任何单个数字 | d[0-9] | | 重量 / 广泛的 / 用 / 随着 | 匹配可以在单词中的字符(大写和小写字符) | | | 由…击中 / 把…贮存入仓 | 在字母数字字符和非字母数字字符之间的边界处匹配 | d{2}b匹配 999和 9999 bd{2}匹配999 和9999 | | 美元 | 这条线的末尾 | 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的对象,它具有字符类型的模板参数和正则表达式特征类。 这个类有两个typedefregex表示charwregex表示宽字符,它们的特性由regex_traitswregex_traits类描述。

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

适当的 regex 类将解析该表达式,以使<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),可以将syntax_option_type枚举中定义的一个常量作为构造函数参数传递给regex_constants命名空间(副本也可以作为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类都是在用于指示匹配项的迭代器类型上模板化的。 有typedef调用的cmatchwcmatch,其中模板参数分别是const char*const wchar_t*,以及smatchwsmatch,其中参数分别是stringwstring对象中使用的迭代器(类似地,还有子匹配类: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(); 
    }

在这里,表达式将匹配用空格括起来的两位数(d{2})(两个b模式表示前后的边界)。 循环以指向字符串开头的迭代器开始,当找到匹配项时,该迭代器递增到该位置,然后递增匹配的长度。 进一步解释的regex_iterator对象包装了这个行为。

match_results类提供对包含的sub_match对象的迭代器访问,因此您可以使用 Rangefor。 最初,容器似乎以一种奇怪的方式工作,因为它知道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++ 文件。 由于此应用将读取文件并处理字符串,因此为适当的库添加 Include,并添加代码以从命令行获取文件名:

    #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将按有助于搜索项目的顺序存储项目。 Avector保持项在容器中插入的顺序,由于我们将按顺序解析电子邮件,这意味着headers数据成员将以与电子邮件中相同的顺序获得标题项。 添加适当的 Include 以便可以使用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应用。 以下是 Internet 消息格式 RFC 5322 的示例电子邮件之一,您可以将其放入文本文件中以测试代码:

    Received: from x.y.test
 by example.net
 via TCP
 with ESMTP
 id ABC12345
 for <[email protected]>;  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 <[email protected]>
To: Mary Smith <[email protected]>
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方法,这意味着 Rangefor循环将调用这些方法来访问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索引到字符串末尾的整个字符串。 如果有分号,则namestart索引一直到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索引到等号前的字符,值是紧跟在等号之后直到字符串末尾的文本。

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

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

简略的 / 概括的 / 简易判罪的 / 简易的

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