Skip to content

Latest commit

 

History

History
1947 lines (1448 loc) · 99.4 KB

File metadata and controls

1947 lines (1448 loc) · 99.4 KB

九、使用数字和字符串

本章包含的配方如下:

  • 在数字和字符串类型之间转换
  • 数值类型的限制和其他属性
  • 生成伪随机数
  • 初始化伪随机数发生器内部状态的所有位
  • 使用原始字符串避免转义字符
  • 创建熟的用户定义文本
  • 创建原始用户定义的文字
  • 创建字符串助手库
  • 使用正则表达式验证字符串的格式
  • 使用正则表达式解析字符串的内容
  • 使用正则表达式替换字符串的内容
  • 使用字符串视图代替常量字符串引用

在数字和字符串类型之间转换

在数字和字符串类型之间转换是一个普遍存在的操作。在 C++ 11 之前,很少有人支持将数字转换为字符串并返回,开发人员不得不主要求助于类型不安全的函数,并且通常编写自己的实用函数,以避免一遍又一遍地编写相同的代码。使用 C++ 11,标准库提供了在数字和字符串之间转换的实用函数。在本食谱中,您将学习如何使用现代 C++ 标准函数在数字和字符串之间进行转换,反之亦然。

准备好

本食谱中提到的所有实用功能都可以在<string>标题中找到。

怎么做...

当需要在数字和字符串之间进行转换时,请使用以下标准转换函数:

  • 要从整数或浮点类型转换为字符串类型,请使用std::to_string()std::to_wstring(),如以下代码片段所示:
        auto si = std::to_string(42);      // si="42" 
        auto sl = std::to_string(42l);     // sl="42" 
        auto su = std::to_string(42u);     // su="42" 
        auto sd = std::to_wstring(42.0);   // sd=L"42.000000" 
        auto sld = std::to_wstring(42.0l); // sld=L"42.000000"
  • 要从字符串类型转换为整数类型,请使用std::stoi()std::stol()std::stoll()std::stoul()std::stoull();请参考以下代码片段:
        auto i1 = std::stoi("42");                 // i1 = 42 
        auto i2 = std::stoi("101010", nullptr, 2); // i2 = 42 
        auto i3 = std::stoi("052", nullptr, 8);    // i3 = 42 
        auto i4 = std::stoi("0x2A", nullptr, 16);  // i4 = 42
  • 要从字符串类型转换为浮点类型,请使用std::stof()std::stod()std::stold(),如以下代码片段所示:
        // d1 = 123.45000000000000 
        auto d1 = std::stod("123.45"); 
        // d2 = 123.45000000000000 
        auto d2 = std::stod("1.2345e+2"); 
        // d3 = 123.44999980926514 
        auto d3 = std::stod("0xF.6E6666p3");

它是如何工作的...

要将整型或浮点型转换为字符串型,可以使用std::to_string()std::to_wstring()功能。这些函数在<string>头中可用,并具有有符号和无符号整数和实数类型的重载。当为每种类型调用适当的格式说明符时,它们会产生与std::sprintf()std::swprintf()相同的结果。下面的代码片段列出了这两个函数的所有重载。

    std::string to_string(int value); 
    std::string to_string(long value); 
    std::string to_string(long long value); 
    std::string to_string(unsigned value); 
    std::string to_string(unsigned long value); 
    std::string to_string(unsigned long long value); 
    std::string to_string(float value); 
    std::string to_string(double value); 
    std::string to_string(long double value); 
    std::wstring to_wstring(int value); 
    std::wstring to_wstring(long value); 
    std::wstring to_wstring(long long value); 
    std::wstring to_wstring(unsigned value); 
    std::wstring to_wstring(unsigned long value); 
    std::wstring to_wstring(unsigned long long value); 
    std::wstring to_wstring(float value); 
    std::wstring to_wstring(double value); 
    std::wstring to_wstring(long double value);

当涉及到相反的转换时,有一整套功能的名称格式为ston(string to number),其中 n 代表 i ( integer)、 l ( long)、 ll ( long long)、 ul ( unsigned long)或full(【T4 下面的列表显示了所有这些函数,每个函数都有两个重载,一个以std::string为参数,另一个以std::wstring为参数:

    int stoi(const std::string& str, std::size_t* pos = 0,  
             int base = 10); 
    int stoi(const std::wstring& str, std::size_t* pos = 0,  
             int base = 10); 
    long stol(const std::string& str, std::size_t* pos = 0,  
             int base = 10); 
    long stol(const std::wstring& str, std::size_t* pos = 0,  
             int base = 10); 
    long long stoll(const std::string& str, std::size_t* pos = 0,  
                    int base = 10); 
    long long stoll(const std::wstring& str, std::size_t* pos = 0,  
                    int base = 10); 
    unsigned long stoul(const std::string& str, std::size_t* pos = 0, 
                        int base = 10); 
    unsigned long stoul(const std::wstring& str, std::size_t* pos = 0,  
                        int base = 10); 
    unsigned long long stoull(const std::string& str,  
                              std::size_t* pos = 0, int base = 10); 
    unsigned long long stoull(const std::wstring& str,  
                              std::size_t* pos = 0, int base = 10); 
    float       stof(const std::string& str, std::size_t* pos = 0); 
    float       stof(const std::wstring& str, std::size_t* pos = 0); 
    double      stod(const std::string& str, std::size_t* pos = 0); 
    double      stod(const std::wstring& str, std::size_t* pos = 0); 
    long double stold(const std::string& str, std::size_t* pos = 0); 
    long double stold(const std::wstring& str, std::size_t* pos = 0);

字符串到整数类型函数的工作方式是丢弃非空白字符前的所有空白,然后尽可能多的字符组成一个有符号或无符号的数字(取决于具体情况),然后将其转换为请求的整数类型(stoi()将返回一个integerstoul()将返回一个unsigned long,以此类推)。在以下所有示例中,结果都是整数42,除了最后一个示例中的结果是-42:

    auto i1 = std::stoi("42");             // i1 = 42 
    auto i2 = std::stoi("   42");          // i2 = 42 
    auto i3 = std::stoi("   42fortytwo");  // i3 = 42 
    auto i4 = std::stoi("+42");            // i4 = 42 
    auto i5 = std::stoi("-42");            // i5 = -42

有效的整数可以由以下部分组成:

  • 一个符号,加号(+)或减号(-)(可选)。
  • 前缀0表示八进制基数(可选)。
  • 前缀0x0X表示十六进制基数(可选)。
  • 数字序列。

可选前缀0(八进制)仅在指定的基数为80时使用。同样,可选前缀0x0X(十六进制)仅在指定的基数为160时使用。

将字符串转换为整数的函数有三个参数:

  • 输入字符串。
  • 一个指针,当不为空时,它将接收已处理的字符数,并且可以包括任何被丢弃的前导空格、符号和基本前缀,因此它不应该与整数值的位数相混淆。
  • 表示基数的数字;默认情况下,这是10

输入字符串中的有效数字取决于基数。对于基数2,唯一有效的数字是01;对于基地5,他们是01234。对于基数11,有效数字为0-9,字符为Aa。这种情况一直持续到我们到达具有有效字符0-9A-Za-z的基地36

以下是将各种基数的数字转换为十进制整数的字符串的更多示例。同样,在所有情况下,结果要么是42要么是-42:

    auto i6 = std::stoi("052", nullptr, 8); 
    auto i7 = std::stoi("052", nullptr, 0); 
    auto i8 = std::stoi("0x2A", nullptr, 16); 
    auto i9 = std::stoi("0x2A", nullptr, 0); 
    auto i10 = std::stoi("101010", nullptr, 2); 
    auto i11 = std::stoi("22", nullptr, 20); 
    auto i12 = std::stoi("-22", nullptr, 20); 

    auto pos = size_t{ 0 }; 
    auto i13 = std::stoi("42", &pos);      // pos = 2 
    auto i14 = std::stoi("-42", &pos);     // pos = 3 
    auto i15 = std::stoi("  +42dec", &pos);// pos = 5

需要注意的一点是,如果转换失败,这些转换函数会抛出。有两种可能引发的异常:

  • std::invalid_argument:如果无法进行转换:
        try 
        { 
           auto i16 = std::stoi(""); 
        } 
        catch (std::exception const & e) 
        { 
           // prints "invalid stoi argument" 
           std::cout << e.what() << std::endl; 
        }
  • std::out_of_range:如果转换后的值超出了结果类型的范围(或者基础函数将errno设置为ERANGE):
        try 
        { 
           // OK
           auto i17 = std::stoll("12345678901234");  
           // throws std::out_of_range 
           auto i18 = std::stoi("12345678901234"); 
        } 
        catch (std::exception const & e) 
        { 
           // prints "stoi argument out of range"
           std::cout << e.what() << std::endl; 
        }

将字符串转换为浮点类型的另一组函数非常相似,只是它们没有数字基的参数。有效的浮点值在输入字符串中可以有不同的表示形式:

  • 十进制浮点表达式(可选符号,带可选点的十进制数字序列,可选eE,后跟带可选符号的指数)。
  • 二进制浮点表达式(可选符号,0x0X前缀,带可选点的十六进制数字序列,可选的pP后跟带可选符号的指数)。
  • Infinity 表达式(可选符号,后跟不区分大小写的INFINFINITY)。
  • 非数字表达式(可选符号后跟不区分大小写的NAN和可能的其他字母数字字符)。

以下是将字符串转换为双精度的各种示例:

    auto d1 = std::stod("123.45");         // d1 =  123.45000000000000 
    auto d2 = std::stod("+123.45");        // d2 =  123.45000000000000 
    auto d3 = std::stod("-123.45");        // d3 = -123.45000000000000 
    auto d4 = std::stod("  123.45");       // d4 =  123.45000000000000 
    auto d5 = std::stod("  -123.45abc");   // d5 = -123.45000000000000 
    auto d6 = std::stod("1.2345e+2");      // d6 =  123.45000000000000 
    auto d7 = std::stod("0xF.6E6666p3");   // d7 =  123.44999980926514 

    auto d8 = std::stod("INF");            // d8 = inf 
    auto d9 = std::stod("-infinity");      // d9 = -inf 
    auto d10 = std::stod("NAN");           // d10 = nan 
    auto d11 = std::stod("-nanabc");       // d11 = -nan

前面以0xF.6E6666p3形式看到的浮点数基数 2 科学记数法,不是本食谱的主题。然而,为了清楚理解,提供了简短的描述;但是,建议您查看其他参考资料以了解详细信息。基数为 2 的科学记数法中的浮点常数由几个部分组成:

  • 十六进制前缀0x
  • 整数部分,在这个例子中是F,十进制是 15。
  • 小数部分,在本例中为6E6666,或二进制中的011011100110011001100110。要将其转换为十进制,我们需要加上 2 的逆幂:1/4 + 1/8 + 1/32 + 1/64 + 1/128 + ...
  • 后缀,代表 2 的幂;在本例中,p3表示 3 的 2 次方。

十进制等效值是由有效部分(由整数部分和小数部分组成)和基数乘以指数的幂来确定的。对于给定的十六进制基数为 2 的浮点文字,有效位为15.4312499...(注意第七位之后的数字未显示),基数为 2,指数为 3。所以结果就是15.4212499... * 8,也就是123.44999980926514

请参见

  • 数值类型的极限和其他属性

数值类型的限制和其他属性

有时,需要知道并使用用数字类型表示的最小值和最大值,如charintdouble。很多开发者都在为此使用标准的 C 宏,比如CHAR_MIN / CHAR_MAXINT_MIN / INT_MAX,或者DBL_MIN / DBL_MAX。C++ 提供了一个名为numeric_limits的类模板,它对每个数值类型都进行了专门化,使您能够查询一个类型的最小值和最大值,但不限于此,它还为类型属性查询提供了额外的常量,例如一个类型是否有符号,它需要多少位来表示它的值,浮点类型是否可以表示无穷大,以及许多其他内容。在 C++ 11 之前,numeric_limits<T>的使用受到限制,因为它不能在需要常量的地方使用(例子可以包括数组的大小和开关情况)。因此,开发人员更喜欢在整个代码中使用 C 宏。在 C++ 11 中,情况不再如此,因为numeric_limits<T>的所有静态成员现在都是constexpr,这意味着它们可以在任何需要常量表达式的地方使用。

准备好

numeric_limits<T>类模板在<limits>头中的名称空间std中可用。

怎么做...

使用std::numeric_limits<T>查询数字类型T的各种属性:

  • 使用min()max()静态方法获得一个类型的最小和最大的有限个数:
        template<typename T, typename I> 
        T minimum(I const start, I const end) 
        { 
          T minval = std::numeric_limits<T>::max(); 
          for (auto i = start; i < end; ++ i) 
          { 
            if (*i < minval) 
              minval = *i; 
          } 
          return minval; 
        } 

        int range[std::numeric_limits<char>::max() + 1] = { 0 }; 

        switch(get_value()) 
        { 
          case std::numeric_limits<int>::min(): 
          break; 
        }
  • 使用其他静态方法和静态常数来检索数值类型的其他属性:
        auto n = 42; 
        std::bitset<std::numeric_limits<decltype(n)>::digits>  
          bits { static_cast<unsigned long long>(n) };

In C++ 11, there is no limitation to where std::numeric_limits<T> can be used; therefore, preferably use it over C macros in your modern C++ code.

它是如何工作的...

std::numeric_limits<T>是一个类模板,允许开发人员查询数值类型的属性。实际值可通过专门化获得,标准库为所有内置数值类型提供专门化(charshortintlongfloatdouble等)。此外,第三方可以为其他类型提供额外的实现。一个例子可以是实现一个bigint整数类型和一个decimal类型的数值库,并为这些类型提供numeric_limits的专门化(例如numeric_limits<bigint>numeric_limits<decimal>)。

<limits>标题中提供了以下数值类型的专门化。请注意,char16_tchar32_t的专门化在 C++ 11 中是新的;其他的之前都有。除了前面列出的专门化之外,该库还包括这些数字类型的每个cv-qualified版本的专门化,它们与不合格的专门化相同。比如考虑类型int;有四个实际的专业(它们是相同的):numeric_limits<int>numeric_limits<const int>numeric_limits<volatile int>numeric_limits<const volatile int>:

    template<> class numeric_limits<bool>; 
    template<> class numeric_limits<char>; 
    template<> class numeric_limits<signed char>; 
    template<> class numeric_limits<unsigned char>; 
template<> class numeric_limits<wchar_t>; 
    template<> class numeric_limits<char16_t>; 
    template<> class numeric_limits<char32_t>; 
    template<> class numeric_limits<short>; 
    template<> class numeric_limits<unsigned short>; 
    template<> class numeric_limits<int>; 
    template<> class numeric_limits<unsigned int>; 
    template<> class numeric_limits<long>; 
    template<> class numeric_limits<unsigned long>; 
    template<> class numeric_limits<long long>; 
    template<> class numeric_limits<unsigned long long>; 
    template<> class numeric_limits<float>; 
    template<> class numeric_limits<double>; 
    template<> class numeric_limits<long double>;

如前所述,在 C++ 11 中,numeric_limits的所有静态成员都是constexpr,这意味着它们可以用在所有需要常量表达式的地方。与 C++ 宏相比,这些宏有几个主要优点:

  • 它们更容易记住,因为您唯一需要知道的是您无论如何都应该知道的类型的名称,而不是无数个宏的名称。
  • 它们支持 C 语言中没有的类型,如char16_tchar32_t
  • 对于不知道类型的模板,它们是唯一可能的解决方案。
  • 最小值和最大值只是它提供的各种类型属性中的两个;因此,它的实际用途超出了数字限制。顺便说一下,由于这个原因,这个类应该被称为numeric_properties,而不是numeric_limits

以下函数模板print_type_properties()打印该类型的最小和最大有限值以及其他信息:

    template <typename T> 
    void print_type_properties() 
    { 
      std::cout  
        << "min="  
        << std::numeric_limits<T>::min()        << std::endl 
        << "max=" 
        << std::numeric_limits<T>::max()        << std::endl 
        << "bits=" 
        << std::numeric_limits<T>::digits       << std::endl 
        << "decdigits=" 
        << std::numeric_limits<T>::digits10     << std::endl 
        << "integral=" 
        << std::numeric_limits<T>::is_integer   << std::endl 
        << "signed=" 
        << std::numeric_limits<T>::is_signed    << std::endl 
        << "exact=" 
        << std::numeric_limits<T>::is_exact     << std::endl 
        << "infinity=" 
        << std::numeric_limits<T>::has_infinity << std::endl; 
    }

如果我们为无符号的shortintdouble调用print_type_properties()函数,它将有以下输出:

| unsigned short | int | double | | 最小值=0 最大值=65535 位=16decdigits=4 积分=1 有符号=0 精确=1 无穷大=0 | min=-2147483648max=2147483647 位=31decdigits=9 积分=1 有符号=1 精确=1 无穷大=0 | 最小值=2.22507e-308 最大值=1.79769e+308 位=53decdigits=15 积分=0 有符号=1 精确=0 无穷大=1 |

需要注意的一点是digitsdigits10常数之间的差异:

  • digits表示整数类型的位数(不包括符号位,如果存在)和填充位(如果有),以及浮点类型尾数的位数。
  • digits10是一个类型可以表示的十进制位数,没有变化。为了更好地理解这一点,让我们考虑unsigned short的情况。这是一个 16 位整型。它可以表示 0 到 65536 之间的数字。它可以表示最多五个十进制数字,从 10,000 到 65,536,但它不能表示所有五个十进制数字,因为从 65,537 到 99,999 的数字需要更多的位。因此,在不需要更多位的情况下,它所能表示的最大数字有四个十进制数字(从 1000 到 9999 的数字)。这是由digits10表示的值。对于积分类型,与常数digits有直接关系;对于积分型Tdigits10的值为std::numeric_limits<T>::digits * std::log10(2)

生成伪随机数

从游戏到密码学,从采样到预测,生成随机数对于各种各样的应用都是必要的。然而,术语随机数实际上并不正确,因为通过数学公式生成的数字是确定性的,不会产生真正的随机数,而是看起来随机的数字,被称为伪随机。真正的随机性只能通过基于物理过程的硬件设备来实现,甚至这一点也可能受到挑战,因为人们甚至可能认为宇宙实际上是确定性的。现代 C++ 通过包含数字生成器和分布的伪随机数库来支持生成伪随机数。理论上,它也可以产生真正的随机数,但实际上,这些可能只是伪随机的。

准备好

在这个配方中,我们讨论了生成伪随机数的标准支持。理解随机数和伪随机数的区别是关键。另一方面,熟悉各种统计分布是一个优势。但是,您必须知道什么是均匀分布,因为库中的所有引擎都生成均匀分布的数字。

怎么做...

要在应用中生成伪随机数,您应该执行以下步骤:

  1. 包括标题<random>:
        #include <random>
  1. 使用std::random_device生成器为伪随机引擎播种:
        std::random_device rd{};
  1. 使用可用的引擎之一来生成数字,并用随机种子初始化它:
        auto mtgen = std::mt19937{ rd() };
  1. 使用可用分布之一将引擎输出转换为所需的统计分布之一:
        auto ud = std::uniform_int_distribution<>{ 1, 6 };
  1. 生成伪随机数:
        for(auto i = 0; i < 20; ++ i) 
          auto number = ud(mtgen);

它是如何工作的...

伪随机数字库包含两种类型的组件:

  • 引擎,是随机数的发生器;这些可以产生具有均匀分布的伪随机数,或者如果可用,产生实际随机数。
  • 分布将发动机的输出转换为统计分布。

所有引擎(除random_device外)均产生均匀分布的整数,所有引擎执行以下方法:

  • min():这是一个静态方法,返回生成器可以产生的最小值。
  • max():这是一个静态方法,返回生成器可以产生的最大值。
  • seed():这会用一个起始值初始化算法(除了random_device,它不能被播种)。
  • operator():这将生成一个新的数字,均匀分布在min()max()之间。
  • discard():这将生成并丢弃给定数量的伪随机数。

以下发动机可供选择:

  • linear_congruential_engine:这是一个线性同余生成器,使用以下公式产生数字:

(c)调制解调器{ > t0±x(I)} =(a * x(I-1)< = m { > t1 }。

  • mersenne_twister_engine:这是一个默森扭扭发生器,在 W * (N-1) * R 位保持一个值;每次需要生成数字时,它都会提取 W 位。当所有的位都被使用时,它通过移位和混合位来扭曲大的值,以便它有一组新的位可以从中提取。
  • subtract_with_carry_engine:这是一个基于以下公式实现进位减法算法的生成器:

x(i) = (x(i - R) - x(i - S) - cy(i - 1)) mod M

在上式中, cy 定义为:

cy(i) = x(i - S) - x(i - R) - cy(i - 1) < 0 ? 1 : 0

此外,该库还提供了引擎适配器,这些适配器也是包装另一个引擎并根据基本引擎的输出生成数字的引擎。引擎适配器为基本引擎实现了前面提到的相同方法。下列发动机适配器可用:

  • discard_block_engine:一个生成器,从基础引擎生成的每个 P 数块中只保留 R 数,丢弃其余的。
  • independent_bits_engine:生成位数与基础引擎不同的数字的生成器。
  • shuffle_order_engine:一个生成器,保存由基础引擎产生的 K 个号码的混洗表,并从该表中返回号码,用基础引擎产生的号码替换它们。

所有这些引擎和引擎适配器都在产生伪随机数。然而,该库提供了另一个名为random_device的引擎,该引擎被认为会产生非确定性的数字,但这并不是一个实际的限制,因为随机熵的物理来源可能不可用。因此,random_device的实现实际上可以基于伪随机引擎。random_device类不能像其他引擎一样进行种子化,它有一个名为entropy()的额外方法,返回随机设备熵,对于确定性生成器为 0,对于非确定性生成器为非零。然而,这不是确定设备实际上是确定性还是非确定性的可靠方法。例如,GNU libstdc++ 和 LLVM libc++ 都实现了一个非确定性设备,但是返回0表示熵。另一方面,VC++ boost.random分别返回3210,表示熵。

所有这些生成器生成均匀分布的整数。然而,这只是大多数应用中需要随机数的许多可能的统计分布之一。为了能够在其他分布中产生数字(整数或实数),该库提供了几个称为分布的类,它们根据引擎实现的统计分布转换引擎的输出。以下发行版可用:

| 类型 | 类名 | 数字 | 统计分布 | | 制服 | uniform_int_distribution | 整数 | 制服 | | | uniform_real_distribution | 真实的 | 制服 | | 伯努利 | bernoulli_distribution | 布尔 | 伯努利 | | | binomial_distribution | 整数 | 二项式 | | | negative_binomial_distribution | 整数 | 负二项式 | | | geometric_distribution | 整数 | 几何学的 | | 泊松 | poisson_distribution | 整数 | 泊松 | | | exponential_distribution | 真实的 | 指数的 | | | gamma_distribution | 真实的 | 微克 | | | weibull_distribution | 真实的 | (统计学家)威伯尔(或韦布尔) | | | extreme_value_distribution | 真实的 | 极端值 | | 常态 | normal_distribution | 真实的 | 标准正态(高斯) | | | lognormal_distribution | 真实的 | 对数正态的 | | | chi_squared_distribution | 真实的 | 卡方检定 | | | cauchy_distribution | 真实的 | 柯西 | | | fisher_f_distribution | 真实的 | 费希尔分布 | | | student_t_distribution | 真实的 | 学生 t 分布 | | 抽样 | discrete_distribution | 整数 | 分离的 | | | piecewise_constant_distribution | 真实的 | 分布在常数子区间上的值 | | | piecewise_linear_distribution | 真实的 | 在定义的子区间上分布的值 |

该库提供的每个引擎都有优点和缺点。线性同余发动机内部状态小,但速度不是很快。另一方面,带进位引擎的减法非常快,但是需要更多的内存用于内部状态。梅森尼绕口令是其中最慢的,也是内部状态最大的,但如果初始化得当,可以产生最长的不重复的数字序列。在下面的例子中,我们将使用std::mt19937,一个内部状态为 19,937 位的 32 位默森扭转器。

生成随机数的最简单方法如下所示:

    auto mtgen = std::mt19937 {}; 
    for (auto i = 0; i < 10; ++ i) 
      std::cout << mtgen() << std::endl;

在这个例子中,mtgen是一个std::mt19937默森龙卷风。要生成数字,您只需要使用 call 运算符,该运算符将内部状态提前并返回下一个伪随机数。然而,这个代码是有缺陷的,因为引擎没有种子。因此,它总是产生相同的数字序列,这在大多数情况下可能不是您想要的。

有不同的方法来初始化引擎。C rand 库常见的一种方法是使用当前时间。在现代 C++ 中,它应该是这样的:

    auto seed = std::chrono::high_resolution_clock::now() 
                .time_since_epoch() 
                .count(); 
    auto mtgen = std::mt19937{ static_cast<unsigned int>(seed) };

在这个例子中,seed是一个数字,表示从时钟的纪元到当前时刻的刻度数。然后,这个数字被用来为引擎播种。这种方法的问题是seed的值实际上是确定性的,在某些类别的应用中,它可能容易受到攻击。一种更可靠的方法是用实际的随机数给生成器播种。std::random_device类是一个应该返回真随机数的引擎,尽管实现实际上可能基于一个伪随机生成器:

    std::random_device rd; 
    auto mtgen = std::mt19937 {rd()};

所有引擎产生的数字都遵循统一的分布。为了将结果转换成另一个统计分布,我们必须使用一个分布类。为了展示生成的数字是如何根据选定的分布进行分布的,我们将使用以下函数。该函数生成指定数量的伪随机数,并计算它们在地图中的重复次数。地图中的值随后被用于生成一个条形图,显示每个数字出现的频率:

    void generate_and_print( 
      std::function<int(void)> gen,  
      int const iterations = 10000) 
    { 
      // map to store the numbers and their repetition 
      auto data = std::map<int, int>{}; 

      // generate random numbers 
      for (auto n = 0; n < iterations; ++ n) 
        ++ data[gen()]; 

      // find the element with the most repetitions 
      auto max = std::max_element( 
                 std::begin(data), std::end(data),  
                 [](auto kvp1, auto kvp2) { 
        return kvp1.second < kvp2.second; }); 

      // print the bars 
      for (auto i = max->second / 200; i > 0; --i) 
      { 
        for (auto kvp : data) 
        { 
          std::cout 
            << std::fixed << std::setprecision(1) << std::setw(3) 
            << (kvp.second / 200 >= i ? (char)219 : ' '); 
        } 

        std::cout << std::endl; 
      } 

      // print the numbers 
      for (auto kvp : data) 
      { 
        std::cout 
          << std::fixed << std::setprecision(1) << std::setw(3) 
          << kvp.first; 
      } 

      std::cout << std::endl; 
    }

以下代码使用std::mt19937引擎生成随机数,随机数在[1, 6]范围内均匀分布;这基本上就是你掷骰子得到的结果:

    std::random_device rd{}; 
    auto mtgen = std::mt19937{ rd() }; 
    auto ud = std::uniform_int_distribution<>{ 1, 6 }; 
    generate_and_print([&mtgen, &ud]() {return ud(mtgen); });

程序的输出如下所示:

在下一个也是最后一个例子中,我们将分布改变为均值5和标准差2的正态分布。这种分布产生实数;因此,为了使用前面的generate_and_print()函数,数字必须四舍五入为整数:

    std::random_device rd{}; 
    auto mtgen = std::mt19937{ rd() }; 
    auto nd = std::normal_distribution<>{ 5, 2 }; 

    generate_and_print( 
      [&mtgen, &nd]() { 
        return static_cast<int>(std::round(nd(mtgen))); });

以下是早期代码的输出:

请参见

  • 初始化伪随机数发生器内部状态的所有位

初始化伪随机数发生器内部状态的所有位

在前面的配方中,我们已经研究了伪随机数库及其组件,以及如何使用它来产生不同统计分布的数字。该配方中忽略的一个重要因素是伪随机数发生器的正确初始化。在本食谱中,您将学习如何初始化生成器,以便产生最佳的伪随机数序列。

准备好

您应该阅读前面的食谱生成伪随机数,以了解伪随机数库提供的内容。

怎么做...

要正确初始化伪随机数发生器以产生最佳的伪随机数序列,请执行以下步骤:

  1. 使用std::random_device产生用作种子值的随机数:
        std::random_device rd;
  1. 为引擎的所有内部位生成随机数据:
        std::array<int, std::mt19937::state_size> seed_data {};
        std::generate(std::begin(seed_data), std::end(seed_data), 
                      std::ref(rd));
  1. 根据之前生成的伪随机数据创建一个std::seed_seq对象:
        std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
  1. 创建一个引擎对象,并初始化代表引擎内部状态的所有位;例如,一个mt19937有 19937 位的内部状态:
        auto eng = std::mt19937{ seq };
  1. 根据应用的要求使用适当的分布:
        auto dist = std::uniform_real_distribution<>{ 0, 1 };

它是如何工作的...

在前面配方中显示的所有例子中,我们使用了std::mt19937引擎来产生伪随机数。虽然梅森扭扭器比其他引擎慢,但它可以产生最长的不重复数字序列,并具有最好的频谱特性。但是,以前面配方中显示的方式初始化发动机不会有这种效果。仔细分析(这超出了本食谱或本书的目的),可以看出,引擎倾向于重复产生一些值,而忽略其他值,从而产生的数字不是均匀分布,而是二项式或泊松分布。问题是mt19937的内部状态有 624 个 32 位整数,在之前配方的例子中,我们只初始化了其中的一个。

使用伪随机数字库时,请记住以下经验法则(显示在信息框中):

In order to produce the best results, engines must have all their internal state properly initialized before generating numbers.

伪随机数库为此提供了一个名为std::seed_seq的类。这是一个生成器,可以植入任意数量的 32 位整数,并生成在 32 位空间中均匀分布的所需数量的整数。

在前面的代码中*是怎么做到的...*部分,我们定义了一个名为seed_data的数组,其 32 位整数的个数等于mt19937生成器的内部状态;那是 624 个整数。然后,我们用一个std::random_device产生的随机数初始化数组。这个阵列后来被用来播种std::seed_seq,而std::seed_seq又被用来播种mt19937发电机。

创建熟的用户定义文本

文字是内置类型(数字、布尔、字符、字符串和指针)的常量,不能在程序中更改。该语言定义了一系列前缀和后缀来指定文字(前缀/后缀实际上是文字的一部分)。C++ 11 允许通过定义名为文字运算符的函数来创建用户定义的文字,该运算符引入了用于指定文字的后缀。这些仅适用于数字字符和字符串类型。这为在未来的版本中定义两种标准文字提供了可能性,并允许开发人员创建自己的文字。在这个食谱中,我们将看到如何创建我们自己的烹饪文字。

准备好

用户定义的文字可以有两种形式:生的熟的。原始文字不被编译器处理,而熟文字是由编译器处理的值(示例可以包括处理字符串中的转义序列或识别数字值,如从文字 0xBAD 开始的整数 2898)。原始文字仅适用于整数和浮点类型,而熟文字也适用于字符和字符串文字。

怎么做...

要创建熟的用户定义文本,您应该遵循以下步骤:

  1. 在单独的名称空间中定义文字,以避免名称冲突。
  2. 始终用下划线(_)作为用户定义后缀的前缀。
  3. 为熟字面值定义以下形式的字面值运算符:
        T operator "" _suffix(unsigned long long int); 
        T operator "" _suffix(long double); 
        T operator "" _suffix(char); 
        T operator "" _suffix(wchar_t); 
        T operator "" _suffix(char16_t); 
        T operator "" _suffix(char32_t); 
        T operator "" _suffix(char const *, std::size_t); 
        T operator "" _suffix(wchar_t const *, std::size_t); 
        T operator "" _suffix(char16_t const *, std::size_t); 
        T operator "" _suffix(char32_t const *, std::size_t);

以下示例创建一个用于指定千字节的用户定义文字:

    namespace compunits 
    { 
      constexpr size_t operator "" _KB(unsigned long long const size) 
      { 
        return static_cast<size_t>(size * 1024); 
      } 
    } 

    auto size{ 4_KB };         // size_t size = 4096; 

    using byte = unsigned char; 
    auto buffer = std::array<byte, 1_KB>{};

它是如何工作的...

当编译器遇到带有用户定义后缀S的用户定义文字时(对于第三方后缀,它总是有一个前导下划线,因为没有前导下划线的后缀是为标准库保留的),它会进行非限定名称查找,以便用名称运算符"operator "" S标识函数。如果它找到一个,那么它根据文字的类型和文字运算符的类型调用它。否则,编译器会产生错误。

*中的例子是如何做到的...*部分,文字运算符称为operator "" _KB,参数类型为unsigned long long int。这是文字运算符处理整数类型的唯一可能的整数类型。类似地,对于浮点用户定义的文字,参数类型必须是long double,因为对于数值类型,文字运算符必须能够处理最大可能的值。这个文字操作符返回一个constexpr值,这样就可以在需要编译时值的地方使用它,比如指定数组的大小,如上例所示。

当编译器识别出用户定义的文字并必须调用适当的用户定义文字运算符时,它将根据以下规则从重载集中选取重载:

  • 对于整数文字:它按照以下顺序调用:取unsigned long long的运算符、取const char*的原始文字运算符或文字运算符模板。
  • 对于浮点文字:它按照以下顺序调用:取long double的运算符、取const char*的原始文字运算符或文字运算符模板。
  • 对于字符文字:它根据字符类型调用适当的运算符(charwchar_tchar16_tchar32_t)。
  • 对于字符串文字:它调用适当的运算符,这取决于指向字符串的指针的字符串类型和大小。

在下面的例子中,我们定义了一个单位和数量的系统。我们希望用公斤、件、升和其他类型的单位来操作。这在可以处理订单的系统中可能很有用,您需要为每件商品指定数量和单位。命名空间units中定义了以下内容:

  • 可能的单位类型(千克、米、升和件)的限定范围的枚举:
        enum class unit { kilogram, liter, meter, piece, };
  • 指定特定单位数量(如 3.5 公斤或 42 件)的类别模板:
        template <unit U> 
        class quantity 
        {
          const double amount; 
          public: 
            constexpr explicit quantity(double const a) : 
              amount(a) {} 

          explicit operator double() const { return amount; } 
        };
  • quantity类模板的operator+operator-功能是为了能够加减数量:
        template <unit U> 
        constexpr quantity<U> operator+(quantity<U> const &q1, 
                                        quantity<U> const &q2) 
        {
          return quantity<U>(static_cast<double>(q1) + 
                             static_cast<double>(q2)); 
        } 

        template <unit U> 
        constexpr quantity<U> operator-(quantity<U> const &q1, 
                                        quantity<U> const &q2)
        {
          return quantity<U>(static_cast<double>(q1) - 
                             static_cast<double>(q2));
        }
  • 用于创建quantity文字的文字运算符,在名为unit_literals的内部命名空间中定义。这样做的目的是避免可能与其他名称空间的文字发生名称冲突。如果确实发生了这样的冲突,开发人员可以在需要定义文字的范围内使用适当的名称空间来选择他们应该使用的名称空间:
        namespace unit_literals 
        { 
          constexpr quantity<unit::kilogram> operator "" _kg( 
              long double const amount) 
          { 
            return quantity<unit::kilogram>  
              { static_cast<double>(amount) }; 
          } 

          constexpr quantity<unit::kilogram> operator "" _kg( 
             unsigned long long const amount) 
          { 
            return quantity<unit::kilogram>  
              { static_cast<double>(amount) }; 
          } 

          constexpr quantity<unit::liter> operator "" _l( 
             long double const amount) 
          { 
             return quantity<unit::liter>  
               { static_cast<double>(amount) }; 
          } 

          constexpr quantity<unit::meter> operator "" _m( 
             long double const amount) 
          { 
            return quantity<unit::meter>  
              { static_cast<double>(amount) }; 
          } 

          constexpr quantity<unit::piece> operator "" _pcs( 
             unsigned long long const amount) 
          { 
            return quantity<unit::piece>  
              { static_cast<double>(amount) }; 
          } 
        }

通过仔细观察,您可以注意到前面定义的文字运算符是不同的:

  • _kg是为整数和浮点文字定义的;这使我们能够创建积分和浮点值,如1_kg1.0_kg
  • _l_m仅针对浮点文字定义;这意味着我们只能用浮点来定义这些单位的数量文字,比如4.5_l10.0_m
  • _pcs仅针对整型文字定义;也就是说我们只能定义整数个件的数量,比如42_pcs

有了这些文字运算符,我们就可以对各种量进行运算。以下示例显示了有效和无效操作:

    using namespace units; 
    using namespace unit_literals; 

    auto q1{ 1_kg };    // OK
    auto q2{ 4.5_kg };  // OK
    auto q3{ q1 + q2 }; // OK
    auto q4{ q2 - q1 }; // OK

    // error, cannot add meters and pieces 
    auto q5{ 1.0_m + 1_pcs }; 
    // error, cannot have an integer number of liters 
    auto q6{ 1_l }; 
    // error, can only have an integer number of pieces 
    auto q7{ 2.0_pcs}

q1是 1 公斤的量;这是一个整数值。由于存在重载operator "" _kg(unsigned long long const),可以从整数 1 正确创建文字。同样,q2是 4.5 公斤的量;这是真正的价值。由于存在overload operator "" _kg(long double),可以从双浮点值 4.5 创建文字。

另一方面,q6是 1 升的量。由于没有重载operator "" _l(unsigned long long),所以无法创建文字。这需要一个需要一个unsigned long long的过载,但是这样的过载是不存在的。类似地,q7是一个 2.0 块的量,但是块文字只能从整数值创建,因此,这会产生另一个编译器错误。

还有更多...

虽然用户定义的文字可以从 C++ 11 中获得,但是标准的文字运算符只能从 C++ 14 中获得。以下是这些标准文字运算符的列表:

  • operator""s用于定义std::basic_string文字:
        using namespace std::string_literals; 

        auto s1{  "text"s }; // std::string 
        auto s2{ L"text"s }; // std::wstring 
        auto s3{ u"text"s }; // std::u16string 
        auto s4{ U"text"s }; // std::u32string
  • 用于创建std::chrono::duration值的operator""hoperator""minoperator""soperator""msoperator""usoperator""ns:
        using namespace std::literals::chrono_literals; 

        // std::chrono::duration<long long> 
        auto timer {2h + 42min + 15s};
  • 用于创建std::complex值的operator""ifoperator""ioperator""il:
        using namespace std::literals::complex_literals; 

        auto c{ 12.0 + 4.5i }; // std::complex<double>

请参见

  • 使用原始字符串文字来避免转义字符
  • 创建原始用户定义文字

创建原始用户定义的文字

在前面的方法中,我们已经研究了 C++ 11 允许库实现者和开发人员创建用户定义的文本和 C++ 14 标准中可用的用户定义的文本的方式。但是,用户定义的文字有两种形式,一种是熟形式,其中文字值在提供给文字运算符之前由编译器处理,另一种是原始形式,其中文字不被编译器解析。后者仅适用于整型和浮点型。在本食谱中,我们将研究如何创建原始的用户定义文本。

准备好

在继续这个食谱之前,强烈建议您浏览前一个,创建熟的用户定义文字,因为这里不再重复关于用户定义文字的一般细节。

为了举例说明如何创建原始的用户定义文字,我们将定义二进制文字。这些二进制文字可以是 8 位、16 位和 32 位(无符号)类型。这些类型将被称为byte8byte16byte32,我们创建的文字将被称为_b8_b16_b32

怎么做...

要创建原始的用户定义文本,您应该遵循以下步骤:

  1. 在单独的名称空间中定义文字,以避免名称冲突。
  2. 始终用下划线(_)作为已用定义后缀的前缀。
  3. 定义以下形式的文字运算符或文字运算符模板:
        T operator "" _suffix(const char*); 

        template<char...> T operator "" _suffix();

以下示例显示了 8 位、16 位和 32 位二进制文字的可能实现:

    namespace binary 
    { 
      using byte8  = unsigned char; 
      using byte16 = unsigned short; 
      using byte32 = unsigned int; 

      namespace binary_literals 
      { 
        namespace binary_literals_internals 
        { 
          template <typename CharT, char... bits> 
          struct binary_struct; 

          template <typename CharT, char... bits> 
          struct binary_struct<CharT, '0', bits...> 
          { 
            static constexpr CharT value{ 
              binary_struct<CharT, bits...>::value }; 
          }; 

          template <typename CharT, char... bits> 
          struct binary_struct<CharT, '1', bits...> 
          { 
            static constexpr CharT value{ 
              static_cast<CharT>(1 << sizeof...(bits)) | 
              binary_struct<CharT, bits...>::value }; 
          }; 

          template <typename CharT> 
          struct binary_struct<CharT> 
          { 
            static constexpr CharT value{ 0 }; 
          }; 
        } 

        template<char... bits> 
        constexpr byte8 operator""_b8() 
        { 
          static_assert( 
            sizeof...(bits) <= 8, 
            "binary literal b8 must be up to 8 digits long"); 

          return binary_literals_internals:: 
                    binary_struct<byte8, bits...>::value; 
        } 

        template<char... bits> 
        constexpr byte16 operator""_b16() 
        { 
          static_assert( 
            sizeof...(bits) <= 16, 
            "binary literal b16 must be up to 16 digits long"); 

          return binary_literals_internals:: 
                    binary_struct<byte16, bits...>::value; 
        } 

        template<char... bits> 
        constexpr byte32 operator""_b32() 
        { 
          static_assert( 
             sizeof...(bits) <= 32, 
             "binary literal b32 must be up to 32 digits long"); 

          return binary_literals_internals:: 
                    binary_struct<byte32, bits...>::value; 
        } 

      } 
    }

它是如何工作的...

上一节中的实现使我们能够定义 1010_b8(十进制 10 的byte8值)或 000010101100_b16(十进制 2130496 的byte16值)形式的二进制文字。但是,我们希望确保不超过每种类型的位数。换句话说,像 111100001_b8 这样的值应该是非法的,编译器应该会产生错误。

首先,我们定义名为binary的命名空间内的所有内容,并从引入几个类型别名(byte8byte16byte32)开始。

文字运算符模板在名为binary_literal_internals的嵌套命名空间中定义。这是一种很好的做法,可以避免名称与其他名称空间中的其他文字运算符冲突。如果发生类似的情况,您可以选择在正确的范围内使用适当的命名空间(例如函数或块中的一个命名空间,以及另一个函数或块中的另一个命名空间)。

三个文字运算符模板非常相似。唯一不同的是它们的名称(_b8_16_b32)、返回类型(byte8byte16byte32)以及静态断言中检查位数的条件。

我们将在后面的食谱中探讨变量模板和模板递归的细节;然而,为了更好地理解,这就是这个特定实现的工作原理:bits是一个模板参数包,它不是一个单一的值,而是模板可以实例化的所有值。例如,如果我们考虑文字1010_b8,那么文字运算符模板将被实例化为operator"" _b8<'1', '0', '1', '0'>()。在继续计算二进制值之前,我们检查文本中的位数。对于_b8,不得超过八(包括任何尾随零)。同样的,_b16最多 16 位,_b32最多 32 位。为此,我们使用sizeof...运算符返回参数包中的元素数量(在本例中为bits)。

如果位数正确,我们可以继续扩展参数包,并递归计算二进制文字表示的十进制值。这是在额外的类模板及其专门化的帮助下完成的。这些模板被定义在另一个嵌套的名称空间中,称为binary_literals_internals。这也是一个很好的实践,因为它对客户端隐藏了(没有适当的限定)实现细节(除非显式的使用命名空间指令使它们对当前命名空间可用)。

Even though this looks like recursion, it is not a true runtime recursion, because after the compiler expands and generates the code from templates, what we end up with is basically calls to overloaded functions with a different number of parameters. This is later explained in the recipe Writing a function template with a variable number of arguments.

binary_struct类模板有一个模板类型CharT用于函数的返回类型(我们需要这个,因为我们的文字运算符模板应该返回byte8byte16byte32)和一个参数包:

    template <typename CharT, char... bits> 
    struct binary_struct;

参数包分解提供了这个类模板的几个专门化。当包的第一个数字为“0”时,计算值保持不变,我们继续扩展包的其余部分。如果包的第一个数字是“1”,则新值向左移动 1,包位的其余部分的数字或包的其余部分的值:

    template <typename CharT, char... bits> 
    struct binary_struct<CharT, '0', bits...> 
    { 
      static constexpr CharT value{ 
        binary_struct<CharT, bits...>::value }; 
    }; 

    template <typename CharT, char... bits> 
    struct binary_struct<CharT, '1', bits...> 
    { 
      static constexpr CharT value{ 
        static_cast<CharT>(1 << sizeof...(bits)) | 
        binary_struct<CharT, bits...>::value }; 
    };

最后一个特殊化涵盖了包装为空时的情况;在这种情况下,我们返回 0:

    template <typename CharT> 
    struct binary_struct<CharT> 
    { 
      static constexpr CharT value{ 0 }; 
    };

在定义了这些辅助类之后,我们可以按照预期实现byte8byte16byte32二进制文本。请注意,我们需要将名称空间binary_literals的内容带入当前名称空间,以便使用文字运算符模板:

    using namespace binary; 
    using namespace binary_literals; 
    auto b1 = 1010_b8; 
    auto b2 = 101010101010_b16; 
    auto b3 = 101010101010101010101010_b32;

以下定义触发编译器错误,因为不满足static_assert中的条件:

    // binary literal b8 must be up to 8 digits long 
    auto b4 = 0011111111_b8; 
    // binary literal b16 must be up to 16 digits long 
    auto b5 = 001111111111111111_b16; 
    // binary literal b32 must be up to 32 digits long 
auto b6 = 0011111111111111111111111111111111_b32;

请参见

  • 使用原始字符串文字来避免转义字符 s
  • 创建熟的用户定义文字
  • 编写可变参数数的函数模板 食谱第 10 章探索函数
  • 创建类型别名和别名模板第八章学习现代核心语言功能

使用原始字符串避免转义字符

字符串可能包含特殊字符,如不可打印字符(换行符、水平和垂直制表符等)、字符串和字符分隔符(双引号和单引号)或任意八进制、十六进制或 Unicode 值。这些特殊字符以转义序列引入,转义序列以反斜杠开头,后跟字符(示例包括'")、其指定字母(示例包括新行的n、水平制表符的t)或其值(示例包括八进制 050、十六进制 X7 或 Unicode U16F0)。因此,反斜杠字符本身必须用另一个反斜杠字符进行转义。这导致更复杂的文字字符串,很难阅读。

为了避免转义字符,C++ 11 引入了不处理转义序列的原始字符串。在本食谱中,您将学习如何使用各种形式的原始字符串。

准备好

在本食谱中,以及本书的其余部分,我将使用s后缀来定义basic_string文字。这已经包含在食谱创建烹饪用户定义的文字中。

怎么做...

为了避免转义字符,请使用以下内容定义字符串文字:

  1. R"( literal )"为默认形式:
        auto filename {R"(C:\Users\Marius\Documents\)"s};
        auto pattern {R"((\w+)=(\d+)$)"s}; 

        auto sqlselect { 
          R"(SELECT * 
          FROM Books 
          WHERE Publisher='Paktpub' 
          ORDER BY PubDate DESC)"s};
  1. R"delimiter( literal )delimiter"其中delimiter是实际字符串中不存在的任何字符序列,而序列)"实际上应该是字符串的一部分。下面是一个用!!定界的例子:
        auto text{ R"!!(This text contains both "( and )".)!!"s }; 
        std::cout << text << std::endl;

它是如何工作的...

当使用字符串文字时,不处理转义,字符串的实际内容写在分隔符之间(换句话说,你看到的就是你得到的)。以下示例显示了显示为相同的原始文字字符串的内容;但是,第二个仍然包含转义字符。由于在字符串的情况下不处理这些,它们将像在输出中一样被打印:

    auto filename1 {R"(C:\Users\Marius\Documents\)"s}; 
    auto filename2 {R"(C:\\Users\\Marius\\Documents\\)"s}; 

    // prints C:\Users\Marius\Documents\  
    std::cout << filename1 << std::endl; 

    // prints C:\\Users\\Marius\\Documents\\  
    std::cout << filename2 << std::endl;

如果文本必须包含)"序列,那么必须使用不同的分隔符,以R"delimiter( literal )delimiter"形式。根据标准,分隔符中可能的字符如下:

any member of the basic source character set except: space, the left parenthesis (the right parenthesis ), the backslash , and the control characters representing horizontal tab, vertical tab, form feed, and newline.

原始字符串文字可以以Lu8uU之一作为前缀,以表示宽、UTF-8、UTF-16 或 UTF-32 字符串文字。以下是这种字符串文字的示例。请注意,字符串末尾出现的字符串文字operator ""s使编译器将类型推断为各种字符串类,而不是字符数组:

    auto t1{ LR"(text)"  };  // const wchar_t* 
    auto t2{ u8R"(text)" };  // const char* 
    auto t3{ uR"(text)"  };  // const char16_t* 
    auto t4{ UR"(text)"  };  // const char32_t* 

    auto t5{ LR"(text)"s  }; // wstring 
    auto t6{ u8R"(text)"s }; // string 
    auto t7{ uR"(text)"s  }; // u16string 
    auto t8{ UR"(text)"s  }; // u32string

请参见

  • 创建熟的用户定义文字

创建字符串助手库

标准库中的字符串类型是一种通用实现,缺少许多有用的方法,例如改变大小写、修剪、拆分以及其他可能满足不同开发人员需求的方法。存在提供丰富字符串功能集的第三方库。然而,在这个食谱中,我们将看看实现几个简单但有帮助的方法,你可能经常需要在实践中。目的是更好地了解如何使用字符串方法和标准的通用算法来操作字符串,同时也是为了参考可在应用中使用的可重用代码。

在本食谱中,我们将实现一个小型字符串实用程序库,它将提供以下功能:

  • 将字符串更改为小写或大写。
  • 反转一根绳子。
  • 从字符串的开头和/或结尾修剪空白。
  • 从字符串的开头和/或结尾修剪一组特定的字符。
  • 删除字符串中任何位置出现的字符。
  • 使用特定分隔符标记字符串。

准备好

我们将要实现的字符串库应该与所有标准字符串类型一起工作,std::stringstd::wstringstd::u16stringstd::u32string。为了避免指定像std::basic_string<CharT, std::char_traits<CharT>, std::allocator<CharT>>这样的长名称,我们将为字符串和字符串流使用以下别名模板:

    template <typename CharT> 
    using tstring =  
       std::basic_string<CharT, std::char_traits<CharT>,  
                         std::allocator<CharT>>; 

    template <typename CharT> 
    using tstringstream =  
       std::basic_stringstream<CharT, std::char_traits<CharT>,  
                               std::allocator<CharT>>;

为了实现这些字符串助手函数,我们需要包含字符串的标题<string>和我们将使用的通用标准算法的标题<algorithm>

在这个食谱的所有例子中,我们将使用标准的用户定义的文字操作符来处理来自 C++ 14 的字符串,为此我们需要显式地使用std::string_literals命名空间。

怎么做...

  1. 要将字符串转换为小写或大写,请使用通用算法std::transform()对字符串字符应用tolower()toupper()功能:
        template<typename CharT> 
        inline tstring<CharT> to_upper(tstring<CharT> text) 
        { 
          std::transform(std::begin(text), std::end(text), 
                         std::begin(text), toupper); 
          return text; 
        } 

        template<typename CharT> 
        inline tstring<CharT> to_lower(tstring<CharT> text) 
        { 
          std::transform(std::begin(text), std::end(text),  
                         std::begin(text), tolower); 
          return text; 
        }
  1. 要反转字符串,请使用通用算法std::reverse():
        template<typename CharT> 
        inline tstring<CharT> reverse(tstring<CharT> text) 
        { 
          std::reverse(std::begin(text), std::end(text)); 
          return text; 
        }
  1. 要修剪一根弦,在开始、结束或两者时,使用std::basic_string的方法find_first_not_of()find_last_not_of():
        template<typename CharT> 
        inline tstring<CharT> trim(tstring<CharT> const & text) 
        { 
          auto first{ text.find_first_not_of(' ') }; 
          auto last{ text.find_last_not_of(' ') }; 
          return text.substr(first, (last - first + 1)); 
        } 

        template<typename CharT> 
        inline tstring<CharT> trimleft(tstring<CharT> const & text) 
        { 
          auto first{ text.find_first_not_of(' ') }; 
          return text.substr(first, text.size() - first); 
        } 

        template<typename CharT> 
        inline tstring<CharT> trimright(tstring<CharT> const & text) 
        { 
          auto last{ text.find_last_not_of(' ') }; 
          return text.substr(0, last + 1); 
        }
  1. 要从字符串中修剪给定集合中的字符,请使用std::basic_string的方法find_first_not_of()find_last_not_of()的重载,它们采用定义要查找的字符集的字符串参数:
        template<typename CharT> 
        inline tstring<CharT> trim(tstring<CharT> const & text,  
                                   tstring<CharT> const & chars) 
        { 
          auto first{ text.find_first_not_of(chars) }; 
          auto last{ text.find_last_not_of(chars) }; 
          return text.substr(first, (last - first + 1)); 
        } 

        template<typename CharT> 
        inline tstring<CharT> trimleft(tstring<CharT> const & text,  
                                       tstring<CharT> const & chars) 
        { 
          auto first{ text.find_first_not_of(chars) }; 
          return text.substr(first, text.size() - first); 
        } 

        template<typename CharT> 
        inline tstring<CharT> trimright(tstring<CharT> const &text, 
                                        tstring<CharT> const &chars) 
        { 
          auto last{ text.find_last_not_of(chars) }; 
          return text.substr(0, last + 1); 
        }
  1. 要从字符串中删除字符,请使用std::remove_if()std::basic_string::erase():
        template<typename CharT> 
        inline tstring<CharT> remove(tstring<CharT> text,  
                                     CharT const ch) 
        { 
          auto start = std::remove_if( 
                          std::begin(text), std::end(text),  
                          [=](CharT const c) {return c ==  ch; }); 
          text.erase(start, std::end(text)); 
          return text; 
        }
  1. 要根据指定的分隔符分割字符串,请使用std::getline()从用字符串内容初始化的std::basic_stringstream中读取。从流中提取的标记被推入字符串向量:
        template<typename CharT> 
        inline std::vector<tstring<CharT>> split 
           (tstring<CharT> text, CharT const delimiter) 
        {
          auto sstr = tstringstream<CharT>{ text }; 
          auto tokens = std::vector<tstring<CharT>>{}; 
          auto token = tstring<CharT>{}; 
          while (std::getline(sstr, token, delimiter))  
          { 
            if (!token.empty()) tokens.push_back(token); 
          } 
          return tokens; 
        }

它是如何工作的...

为了实现库中的实用函数,我们有两个选项:

  • 函数会修改由引用传递的字符串。
  • 函数不会改变原始字符串,而是返回一个新字符串。

第二个选项的优点是它保留了原始字符串,这在许多情况下可能会有所帮助。否则,在这些情况下,您将首先必须制作一个字符串的副本,并更改该副本。该配方中提供的实现采用了第二种方法。

我们在*中实现的第一个功能是如何做到的...*段分别为to_upper()to_lower()。这些函数将字符串的内容更改为大写或小写。实现这一点的最简单方法是使用std::transform()标准算法。这是一个通用算法,它将一个函数应用于一个范围(由开始和结束迭代器定义)的每个元素,并将结果存储在另一个只需要指定开始迭代器的范围中。输出范围可以与输入范围相同,这正是我们对字符串所做的转换。应用功能为toupper()tolower():

    auto ut{ string_library::to_upper("this is not UPPERCASE"s) };  
    // ut = "THIS IS NOT UPPERCASE" 

    auto lt{ string_library::to_lower("THIS IS NOT lowercase"s) };  
    // lt = "this is not lowercase"

我们考虑的下一个函数是reverse(),顾名思义,它反转字符串的内容。为此,我们使用了std::reverse()标准算法。这个通用算法反转由开始和结束迭代器定义的范围的元素:

    auto rt{string_library::reverse("cookbook"s)}; // rt = "koobkooc"

说到修剪,可以在字符串的开头、结尾或两边进行修剪。正因为如此,我们实现了三个不同的功能:trim()用于两端的修剪,trimleft()用于字符串开头的修剪,trimright()用于字符串结尾的修剪。第一个版本的函数只修剪空格。为了找到合适的修剪部位,我们使用了std::basic_stringfind_first_not_of()find_last_not_of()方法。这些函数返回字符串中不是指定字符的第一个和最后一个字符。随后,对std::basic_stringsubstr()方法的调用返回一个新字符串。substr()方法获取字符串中的一个索引和一些要复制到新字符串中的元素:

    auto text1{"   this is an example   "s}; 
    // t1 = "this is an example" 
    auto t1{ string_library::trim(text1) }; 
    // t2 = "this is an example   " 
    auto t2{ string_library::trimleft(text1) }; 
    // t3 = "   this is an example" 
    auto t3{ string_library::trimright(text1) };

从字符串中删除其他字符和空格有时会很有用。为此,我们为修剪函数提供了重载,这些重载指定了要移除的一组字符。该集合也被指定为字符串。该实现与前一个非常相似,因为find_first_not_of()find_last_not_of()都有重载,重载采用包含要从搜索中排除的字符的字符串:

    auto chars1{" !%\n\r"s}; 
    auto text3{"!!  this % needs a lot\rof trimming  !\n"s}; 
    auto t7{ string_library::trim(text3, chars1) };        
    // t7 = "this % needs a lot\rof trimming" 
    auto t8{ string_library::trimleft(text3, chars1) };    
    // t8 = "this % needs a lot\rof trimming  !\n" 
    auto t9{ string_library::trimright(text3, chars1) };   
    // t9 = "!!  this % needs a lot\rof trimming"

如果需要从字符串的任何部分删除字符,修剪方法没有帮助,因为它们只处理字符串开头和结尾的连续字符序列。然而,为此,我们实现了一个简单的remove()方法。这使用std:remove_if()标准算法。std::remove()std::remove_if()的工作方式起初可能不太直观。它们通过重新排列范围的内容(使用移动赋值)从第一个和最后一个迭代器定义的范围中移除满足条件的元素。需要移除的元素被放在范围的末尾,函数返回一个迭代器到范围中代表被移除元素的第一个元素。这个迭代器基本上定义了被修改的范围的新结束。如果没有删除任何元素,返回的迭代器就是原始范围的结束迭代器。这个返回的迭代器的值然后被用来调用std::basic_string::erase()方法,该方法实际上擦除了由两个迭代器定义的字符串的内容。我们这里的两个迭代器是std::remove_if()返回的迭代器和字符串的末尾:

    auto text4{"must remove all * from text**"s}; 
    auto t10{ string_library::remove(text4, '*') };  
    // t10 = "must remove all  from text" 
    auto t11{ string_library::remove(text4, '!') };  
    // t11 = "must remove all * from text**"

我们实现的最后一个方法根据指定的分隔符拆分字符串的内容。有多种方法可以实现这一点。在这个实现中,我们使用了std::getline()。此函数从输入流中读取字符,直到找到指定的分隔符,并将字符放入字符串中。在开始从输入缓冲区读取之前,它调用输出字符串上的erase()来清除其内容。在循环中调用此方法会生成放置在向量中的标记。在我们的实现中,从结果集中跳过了空令牌:

    auto text5{"this text will be split   "s}; 
    auto tokens1{ string_library::split(text5, ' ') };  
    // tokens1 = {"this", "text", "will", "be", "split"} 
    auto tokens2{ string_library::split(""s, ' ') };    
    // tokens2 = {}

请参见

  • 创建熟的用户定义文字
  • 创建类型别名和别名模板第八章学习现代核心语言功能

使用正则表达式验证字符串的格式

正则表达式是一种用于在文本中执行模式匹配和替换的语言。C++ 11 通过标题<regex>中提供的一组类、算法和迭代器支持标准库中的正则表达式。在这个方法中,我们将看到如何使用正则表达式来验证字符串是否匹配模式(示例可以包括验证电子邮件或 IP 地址格式)。

准备好

在整个食谱中,我们将在必要时解释我们使用的正则表达式的细节。但是,为了使用 C++ 正则表达式标准库,您应该至少有一些正则表达式的基本知识。对正则表达式语法和标准的描述超出了本书的目的;如果您不熟悉正则表达式,建议您在继续学习专注于正则表达式的食谱之前,先阅读更多关于正则表达式的内容。

怎么做...

为了验证字符串是否与正则表达式匹配,请执行以下步骤:

  1. 包括头文件<regex><string>以及 C++ 14 标准用户定义字符串的命名空间std::string_literals:
        #include <regex> 
        #include <string> 
        using namespace std::string_literals;
  1. 使用原始字符串文字指定正则表达式,以避免转义反斜杠(这种情况经常发生)。以下正则表达式验证大多数电子邮件格式:
        auto pattern {R"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$)"s};
  1. 创建一个std::regex / std::wregex对象(取决于所使用的字符集)来封装正则表达式:
        auto rx = std::regex{pattern};
  1. 若要忽略大小写或指定其他解析选项,请使用重载构造函数,该构造函数具有用于正则表达式标志的额外参数:
        auto rx = std::regex{pattern, std::regex_constants::icase}; 
  1. 使用std::regex_match()将正则表达式与整个字符串匹配:
        auto valid = std::regex_match("marius@domain.com"s, rx);

它是如何工作的...

考虑到验证电子邮件地址格式的问题,尽管这看起来是一个微不足道的问题,但实际上很难找到一个简单的正则表达式来涵盖有效电子邮件格式的所有可能情况。在这个方法中,我们不会试图找到最终的正则表达式,而是应用一个在大多数情况下足够好的正则表达式。我们将用于此目的的正则表达式如下:

    ^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$

下表解释了正则表达式的结构:

| 部分 | 描述 | | ^ | 字符串开头 | | [A-Z0-9._%+-]+ | 范围 A-Z、0-9 或-、%、+或-中的至少一个字符,表示电子邮件地址的本地部分 | | @ | 字符@ | | [A-Z0-9.-]+ | A-Z、0-9 或-、%、+或-中的至少一个字符,表示域部分的主机名 | | \. | 分隔域主机名和标签的点 | | [A-Z]{2,} | 一个域的域名系统标签,可以包含 2 到 63 个字符 | | $ | 字符串的结尾 |

请记住,在实践中,域名由主机名和以点分隔的域名标签列表组成。例子包括localhostgmail.comyahoo.co.uk。我们使用的这个正则表达式与没有 DNS 标签的域不匹配,比如 localhost(一封电子邮件,比如root@localhost是一封有效的电子邮件)。域名也可以是括号内指定的 IP 地址,如[192.168.100.11](如john.doe@[192.168.100.11])。包含此类域的电子邮件地址将与上面定义的正则表达式不匹配。即使这些相当罕见的格式不匹配,正则表达式也可以覆盖大多数电子邮件格式。

The regular expression in the example in this chapter is provided for didactical purposes only, and it is not intended for being used as it is in production code. As explained earlier, this sample does not cover all possible e-mail formats.

我们从包含必要的头开始,正则表达式为<regex>,字符串为<string>。下面显示的is_valid_email()功能(基本包含了的样本)如何操作... section)获取一个表示电子邮件地址的字符串,并返回一个布尔值,指示电子邮件是否具有有效的格式。我们首先构造一个std::regex对象来封装用原始字符串表示的正则表达式。使用原始字符串文字很有帮助,因为它避免了转义反斜杠,反斜杠也用于正则表达式中的转义字符。然后,该函数调用std::regex_match(),传递输入文本和正则表达式:

    bool is_valid_email_format(std::string const & email) 
    { 
      auto pattern {R"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$)"s}; 

      auto rx = std::regex{pattern}; 

      return std::regex_match(email, rx); 
    }

std::regex_match()方法试图将正则表达式与整个字符串进行匹配。如果成功则返回true,否则返回false:

    auto ltest = [](std::string const & email)  
    { 
      std::cout << std::setw(30) << std::left  
                << email << " : "  
                << (is_valid_email_format(email) ?  
                   "valid format" : "invalid format") 
                << std::endl; 
    }; 

    ltest("JOHN.DOE@DOMAIN.COM"s);         // valid format 
    ltest("JOHNDOE@DOMAIL.CO.UK"s);        // valid format 
    ltest("JOHNDOE@DOMAIL.INFO"s);         // valid format 
    ltest("J.O.H.N_D.O.E@DOMAIN.INFO"s);   // valid format 
    ltest("ROOT@LOCALHOST"s);              // invalid format 
    ltest("john.doe@domain.com"s);         // invalid format

在这个简单的测试中,唯一与正则表达式不匹配的电子邮件是ROOT@LOCALHOSTjohn.doe@domain.com。第一个包含没有带点前缀的 DNS 标签的域名,正则表达式中不包括这种情况。第二个只包含小写字母,在正则表达式中,本地部分和域名的有效字符集都是大写字母 A 到 z

我们可以指定匹配可以忽略大小写,而不是用额外的有效字符(如[A-Za-z0-9._%+-])使正则表达式复杂化。这可以通过给std::basic_regex类的构造函数增加一个参数来实现。用于此目的的可用常量在regex_constants命名空间中定义。以下对is_valid_email_format()的细微更改将使其忽略大小写,并允许小写和大写字母的电子邮件正确匹配正则表达式:

    bool is_valid_email_format(std::string const & email) 
    { 
      auto rx = std::regex{ 
        R"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$)"s, 
        std::regex_constants::icase}; 

      return std::regex_match(email, rx); 
    }

这个is_valid_email_format()函数非常简单,如果正则表达式作为参数与要匹配的文本一起提供,它可以用于匹配任何内容。然而,能够用单个函数处理多字节字符串(std::string)和宽字符串(std::wstring)就不错了。这可以通过创建一个函数模板来实现,其中字符类型作为模板参数提供:

    template <typename CharT> 
    using tstring = std::basic_string<CharT, std::char_traits<CharT>,  
                                      std::allocator<CharT>>; 

    template <typename CharT> 
    bool is_valid_format(tstring<CharT> const & pattern,  
                         tstring<CharT> const & text) 
    { 
      auto rx = std::basic_regex<CharT>{  
        pattern, std::regex_constants::icase }; 

      return std::regex_match(text, rx); 
    }

我们首先为std::basic_string创建一个别名模板,以简化其使用。新的is_valid_format()函数是一个非常类似于我们实现is_valid_email() **的函数模板。**但是,我们现在使用std::basic_regex<CharT>代替 typedef std::regex,,也就是std::basic_regex<char>,,并且模式作为第一个参数提供。我们现在为依赖于这个函数模板的宽字符串实现一个名为is_valid_email_format_w()的新函数。然而,该函数模板可以被重用来实现其他验证,例如如果车牌具有特定的格式:

    bool is_valid_email_format_w(std::wstring const & text) 
    { 
      return is_valid_format( 
        LR"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$)"s,  
        text); 
    } 

    auto ltest2 = [](auto const & email) 
    { 
      std::wcout << std::setw(30) << std::left 
         << email << L" : " 
         << (is_valid_email_format_w(email) ? L"valid" : L"invalid") 
         << std::endl; 
    }; 

    ltest2(L"JOHN.DOE@DOMAIN.COM"s);       // valid
    ltest2(L"JOHNDOE@DOMAIL.CO.UK"s);      // valid
    ltest2(L"JOHNDOE@DOMAIL.INFO"s);       // valid
    ltest2(L"J.O.H.N_D.O.E@DOMAIN.INFO"s); // valid
    ltest2(L"ROOT@LOCALHOST"s);            // invalid
    ltest2(L"john.doe@domain.com"s);       // valid

在上面显示的所有例子中,唯一不匹配的是ROOT@LOCAHOST,正如已经预料到的那样。

事实上,std::regex_match()方法有几个重载,其中一些重载有一个参数,该参数是对存储匹配结果的std::match_results对象的引用。如果没有匹配,则std::match_results为空,大小为 0。否则,如果匹配,则std::match_results对象不为空,其大小为 1 加上匹配的子表达式数。

该函数的以下版本使用上述重载,并在std::smatch对象中返回匹配的子表达式。请注意,正则表达式发生了变化,因为定义了三个标题组-一个用于本地部分,一个用于域的主机名部分,一个用于域名系统标签。如果匹配成功,那么std::smatch对象将包含四个子匹配对象:第一个匹配整个字符串,第二个匹配第一个捕获组(本地部分),第三个匹配第二个捕获组(主机名),第四个匹配第三个也是最后一个捕获组(DNS 标签)。结果以元组形式返回,其中第一项实际上指示成功或失败:

    std::tuple<bool, std::string, std::string, std::string>
    is_valid_email_format_with_result(std::string const & email) 
    { 
      auto rx = std::regex{  
        R"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,})$)"s,  
        std::regex_constants::icase }; 
      auto result = std::smatch{}; 
      auto success = std::regex_match(email, result, rx); 

      return std::make_tuple( 
        success,  
        success ? result[1].str() : ""s, 
        success ? result[2].str() : ""s,  
        success ? result[3].str() : ""s); 
    }

按照前面的代码,我们使用 C++ 17 结构化绑定将元组的内容解包到命名变量中:

    auto ltest3 = [](std::string const & email) 
    { 
      auto [valid, localpart, hostname, dnslabel] =  
       is_valid_email_format_with_result(email); 

      std::cout << std::setw(30) << std::left 
         << email << " : " 
         << std::setw(10) << (valid ? "valid" : "invalid") 
         << "local=" << localpart  
         << ";domain=" << hostname  
         << ";dns=" << dnslabel 
         << std::endl; 
    }; 

    ltest3("JOHN.DOE@DOMAIN.COM"s); 
    ltest3("JOHNDOE@DOMAIL.CO.UK"s); 
    ltest3("JOHNDOE@DOMAIL.INFO"s); 
    ltest3("J.O.H.N_D.O.E@DOMAIN.INFO"s); 
    ltest3("ROOT@LOCALHOST"s); 
    ltest3("john.doe@domain.com"s);

程序的输出如下:

 JOHN.DOE@DOMAIN.COM            : valid 
 local=JOHN.DOE;domain=DOMAIN;dns=COM 
 JOHNDOE@DOMAIL.CO.UK           : valid 
 local=JOHNDOE;domain=DOMAIL.CO;dns=UK 
 JOHNDOE@DOMAIL.INFO            : valid 
 local=JOHNDOE;domain=DOMAIL;dns=INFO 
 J.O.H.N_D.O.E@DOMAIN.INFO      : valid 
 local=J.O.H.N_D.O.E;domain=DOMAIN;dns=INFO 
 ROOT@LOCALHOST                 : invalid 
 local=;domain=;dns= 
 john.doe@domain.com            : valid 
 local=john.doe;domain=domain;dns=com

还有更多...

正则表达式有多个版本,C++ 标准库支持其中 6 个:ECMAScript、basic POSIX、扩展 POSIX、awk、grep、egrep (grep 带选项-E)。使用的默认语法是 ECMAScript,为了使用另一种语法,您必须在定义正则表达式时明确指定语法。除了指定语法,您还可以指定解析选项,例如通过忽略大小写进行匹配。

标准库提供了比我们目前看到的更多的类和算法。库中可用的主要类如下(均为类模板,为方便起见,typedef s 针对不同的字符类型提供):

  • 类模板std::basic_regex定义正则表达式对象:
        typedef basic_regex<char>    regex; 
        typedef basic_regex<wchar_t> wregex;
  • 类模板std::sub_match表示匹配捕获组的字符序列;这个类实际上是从std::pair派生出来的,它的firstsecond成员表示匹配序列中第一个和过去结束字符的迭代器;如果没有匹配序列,则两个迭代器相等:
        typedef sub_match<const char *>            csub_match; 
        typedef sub_match<const wchar_t *>         wcsub_match; 
        typedef sub_match<string::const_iterator>  ssub_match; 
        typedef sub_match<wstring::const_iterator> wssub_match;
  • 类模板std::match_results是匹配的集合;第一个元素始终是目标中的完全匹配,其他元素是子表达式的匹配:
        typedef match_results<const char *>            cmatch; 
        typedef match_results<const wchar_t *>         wcmatch; 
        typedef match_results<string::const_iterator>  smatch; 
        typedef match_results<wstring::const_iterator> wsmatch;

正则表达式标准库中可用的算法如下:

  • std::regex_match():这试图将正则表达式(由std::basic_regex实例表示)与整个字符串进行匹配。
  • std::regex_search():这试图将正则表达式(由std::basic_regex实例表示)与字符串的一部分(包括整个字符串)相匹配。
  • std::regex_replace():这将根据指定的格式替换正则表达式中的匹配项。

正则表达式标准库中可用的迭代器如下:

  • std::regex_interator:常量前向迭代器,用于遍历字符串中出现的模式。它有一个指向std::basic_regex的指针,该指针必须存在,直到迭代器被销毁。在创建和递增时,迭代器调用std::regex_search()并存储算法返回的std::match_results对象的副本。
  • std::regex_token_iterator:常量前向迭代器,用于遍历字符串中正则表达式的每个匹配的子匹配。在内部,它使用std::regex_iterator来遍历子匹配。因为它存储一个指向std::basic_regex实例的指针,正则表达式对象必须存在,直到迭代器被销毁。

请参见

  • 使用正则表达式解析字符串的内容
  • 使用正则表达式替换字符串的内容
  • 使用结构化绑定处理多返回值第八章食谱学习现代核心语言特性

使用正则表达式解析字符串的内容

在前面的食谱中,我们已经了解了如何使用std::regex_match()来验证字符串的内容是否符合特定的格式。该库提供了另一种称为std::regex_search()的算法,该算法将正则表达式与字符串的任何部分进行匹配,而不像regex_match()那样只匹配整个字符串。但是,该函数不允许搜索输入字符串中所有出现的正则表达式。为此,我们需要使用库中可用的迭代器类之一。

在本食谱中,您将学习如何使用正则表达式解析字符串的内容。为此,我们将考虑解析包含名称-值对的文本文件的问题。每个这样的对被定义在不同的行上,格式为name = value,但是以#开头的行代表注释,必须被忽略。以下是一个例子:

    #remove # to uncomment the following lines 
    timeout=120 
    server = 127.0.0.1 

    #retrycount=3

准备好

有关 C++ 11 中正则表达式支持的一般信息,请参考使用正则表达式验证字符串格式方法。继续这个食谱需要基本的正则表达式知识。

在以下示例中,text是一个变量,定义如下:

    auto text { 
      R"( 
        #remove # to uncomment the following lines 
        timeout=120 
        server = 127.0.0.1 

        #retrycount=3 
      )"s};

怎么做...

为了通过字符串搜索正则表达式的出现,您应该执行以下操作:

  1. 包括头文件<regex><string>以及 C++ 14 标准用户定义字符串的命名空间std::string_literals:
        #include <regex> 
        #include <string> 
        using namespace std::string_literals;
  1. 使用原始字符串文字指定正则表达式,以避免转义反斜杠(这种情况经常发生)。以下正则表达式验证了前面建议的文件格式:
        auto pattern {R"(^(?!#)(\w+)\s*=\s*([\w\d]+[\w\d._,\-:]*)$)"s};
  1. 创建一个std::regex / std::wregex对象(取决于所使用的字符集)来封装正则表达式:
        auto rx = std::regex{pattern};
  1. 要在给定文本中搜索正则表达式的第一次出现,请使用通用算法std::regex_search()(示例 1):
        auto match = std::smatch{}; 
        if (std::regex_search(text, match, rx)) 
        { 
          std::cout << match[1] << '=' << match[2] << std::endl; 
        }
  1. 要查找给定文本中正则表达式的所有出现,请使用迭代器std::regex_iterator(示例 2):
        auto end = std::sregex_iterator{}; 
        for (auto it=std::sregex_iterator{ std::begin(text),  
                                           std::end(text), rx }; 
             it != end; ++ it) 
        { 
          std::cout << ''' << (*it)[1] << "'='"  
                    << (*it)[2] << ''' << std::endl; 
        }
  1. 要遍历匹配的所有子表达式,请使用迭代器std::regex_token_iterator(示例 3):
        auto end = std::sregex_token_iterator{}; 
        for (auto it = std::sregex_token_iterator{ 
                          std::begin(text),  std::end(text), rx }; 
             it != end; ++ it) 
        { 
          std::cout << *it << std::endl; 
        }

它是如何工作的...

可以解析前面显示的输入文件的简单正则表达式可能如下所示:

    ^(?!#)(\w+)\s*=\s*([\w\d]+[\w\d._,\-:]*)$

这个正则表达式应该忽略所有以#开头的行;对于那些不以#开头的,匹配一个名称后跟等号,然后匹配一个可以由字母数字字符和其他几个字符(下划线、点、逗号等)组成的值。该正则表达式的确切含义解释如下:

| 部分 | 描述 | | ^ | 行首 | | (?!#) | 确保不可能匹配#字符的负前瞻 | | (\w)+ | 表示至少一个单词字符的标识符的捕获组 | | \s* | 有空白吗 | | = | 等号 | | \s* | 有空白吗 | | ([\w\d]+[\w\d._,\-:]*) | 表示以字母数字字符开头的值的捕获组,但也可以包含点、逗号、反斜杠、连字符、冒号或下划线。 | | $ | 行结束 |

我们可以使用std::regex_search()在输入文本的任何地方搜索匹配。这个算法有几个重载,但通常它们的工作方式是相同的。您必须指定要处理的字符范围、包含匹配结果的输出std::match_results对象和表示正则表达式和匹配标志(定义搜索方式)的std::basic_regex对象。如果找到匹配项,则该函数返回true,否则返回false

在前一节的第一个例子中(见第四个列表项),matchstd::smatch的一个实例,它是以string::const_iterator为模板类型的std::match_results的一个typedef。如果找到匹配项,该对象将包含所有匹配子表达式的值序列中的匹配信息。索引 0 处的子匹配始终是整个匹配。索引 1 处的子匹配是第一个匹配的子表达式,索引 2 处的子匹配是第二个匹配的子表达式,依此类推。由于我们的正则表达式中有两个捕获组(子表达式),如果成功的话,std::match_results将有三个子匹配。表示名称的标识符位于索引 1,等号后面的值位于索引 2。因此,该代码仅打印以下内容:

 timeout=120

std::regex_search()算法无法遍历文本中所有可能的匹配。为此,我们需要使用迭代器。std::regex_iterator就是为了这个目的。它不仅允许遍历所有匹配,还允许访问匹配的所有子匹配。迭代器实际上在构造和每次增量时调用std::regex_search(),并且它记住调用的结果std::match_results。默认构造函数创建一个迭代器,表示序列的结束,可用于测试匹配循环何时停止。

在上一节的第二个例子中(见第五个列表项),我们首先创建一个序列结束迭代器,然后开始遍历所有可能的匹配。构造时会调用std::regex_match(),如果找到匹配,我们可以通过当前迭代器访问它的结果。这将一直持续到没有找到匹配项(序列结束)。该代码将打印以下输出:

 'timeout'='120' 
 'server'='127.0.0.1'

std::regex_iterator的替代物是std::regex_token_iterator。这与std::regex_iterator的工作方式类似,事实上,它内部包含这样一个迭代器,除了它使我们能够从匹配中访问特定的子表达式。这在中的第三个例子中显示了如何做..。部分(第 6 个列表项)。我们从创建序列结束迭代器开始,然后循环匹配,直到到达序列结束。在我们使用的构造函数中,我们没有指定要通过迭代器访问的子表达式的索引;因此,使用默认值 0。这意味着该程序将打印整个匹配:

 timeout=120 
 server = 127.0.0.1

如果我们只想访问第一个子表达式(在我们的例子中,这意味着名称),我们所要做的就是在标记迭代器的构造函数中指定子表达式的索引。这一次,我们得到的输出只是名字:

    auto end = std::sregex_token_iterator{}; 
    for (auto it = std::sregex_token_iterator{ std::begin(text),  
                   std::end(text), rx, 1 }; 
         it != end; ++ it) 
    { 
      std::cout << *it << std::endl; 
    }

令牌迭代器的一个有趣之处在于,如果子表达式的索引为-1,它可以返回字符串中不匹配的部分,在这种情况下,它会返回一个std::match_results对象,该对象对应于最后一次匹配和序列结束之间的字符序列:

    auto end = std::sregex_token_iterator{}; 
    for (auto it = std::sregex_token_iterator{ std::begin(text),  
                   std::end(text), rx, -1 }; 
         it != end; ++ it) 
    { 
      std::cout << *it << std::endl; 
    }

该程序将输出以下内容(注意空行实际上是输出的一部分):

 #remove # to uncomment the following lines 

 #retrycount=3

请参见

  • 使用正则表达式验证字符串的格式
  • 使用正则表达式替换字符串的内容

使用正则表达式替换字符串的内容

在最后两个菜谱中,我们已经了解了如何匹配字符串上的正则表达式或字符串的一部分,并迭代匹配和子匹配。正则表达式库还支持基于正则表达式的文本替换。在这个食谱中,我们将看到如何使用std::regex_replace()来执行这样的文本转换。

准备好

有关 C++ 11 中正则表达式支持的一般信息,请参考使用正则表达式验证字符串格式方法。

怎么做...

为了使用正则表达式执行文本转换,您应该执行以下操作:

  1. 包括字符串的<regex><string>以及 C++ 14 标准用户定义文字的名称空间std::string_literals:
        #include <regex> 
        #include <string> 
        using namespace std::string_literals;
  1. 使用带有替换字符串的std::regex_replace()算法作为第三个参数。考虑这个例子:用三个连字符替换所有由三个字符组成的单词,即abc:
        auto text{"abc aa bca ca bbbb"s}; 
        auto rx = std::regex{ R"(\b[a|b|c]{3}\b)"s }; 
        auto newtext = std::regex_replace(text, rx, "---"s);
  1. 第三个参数使用std::regex_replace()算法,匹配标识符以$为前缀。例如,将“姓氏,名字”中的姓名替换为“名字姓氏”格式的姓名,如下所示:
        auto text{ "bancila, marius"s }; 
        auto rx = std::regex{ R"((\w+),\s*(\w+))"s }; 
        auto newtext = std::regex_replace(text, rx, "$2 $1"s);

它是如何工作的...

std::regex_replace()算法有几个不同参数类型的重载,但是参数的含义如下:

  • 对其执行替换的输入字符串。
  • 一个std::basic_regex对象,它封装了用于标识要替换的字符串部分的正则表达式。
  • 用于替换的字符串格式。
  • 可选匹配标志。

根据所使用的重载,返回值是作为参数提供的输出迭代器的字符串或副本。用于替换的字符串格式可以是简单的字符串,也可以是用$前缀表示的匹配标识符:

  • $&表示整个比赛。
  • $1$2$3等,表示第一、二、三子赛,以此类推。
  • `$``表示字符串在第一次匹配之前的部分。
  • $'表示最后一次匹配后的字符串部分。

在第一个例子中显示的*怎么做...*部分,初始文本包含两个由三个abc字符、abcbca组成的单词。正则表达式表示单词边界之间正好有三个字符的表达式。这意味着一个潜台词,如bbbb,将与表达不匹配。替换的结果是字符串文本将是--- aa --- ca bbbb

匹配的附加标志可以指定给std::regex_replace()算法。默认情况下,匹配标志是std::regex_constants::match_default,它基本上将 ECMAScript 指定为用于构造正则表达式的语法。例如,如果我们想只替换第一个事件,那么我们可以指定std::regex_constants::format_first_only。在下一个例子中,结果是--- aa bca ca bbbb,因为在找到第一个匹配后替换停止:

    auto text{ "abc aa bca ca bbbb"s }; 
    auto rx = std::regex{ R"(\b[a|b|c]{3}\b)"s }; 
    auto newtext = std::regex_replace(text, rx, "---"s, 
                     std::regex_constants::format_first_only);

但是,替换字符串可以包含整个匹配、特定子匹配或不匹配部分的特殊指示符,如前所述。在第二个例子中显示的*怎么做...*一节中,正则表达式标识一个至少包含一个字符的单词,后跟一个昏迷和可能的空格,然后是另一个至少包含一个字符的单词。第一个单词应该是姓,第二个单词应该是名。替换字符串具有$2 $1格式。这是一条用另一个字符串替换匹配表达式(在本例中,是整个原始字符串)的指令,该字符串由第二子匹配,后跟空格,然后是第一子匹配组成。

在这种情况下,整个字符串是匹配的。在下一个示例中,字符串内将有多个匹配项,它们都将被指定的字符串替换。在本例中,我们将不定冠词 a 替换为不定冠词a:

    auto text{"this is a example with a error"s}; 
    auto rx = std::regex{R"(\ba ((a|e|i|u|o)\w+))"s}; 
    auto newtext = std::regex_replace(text, rx, "an $1");

正则表达式将字母 a 识别为单个单词(\b表示单词边界,因此\ba表示单个字母 a 后跟空格的单词,以及至少两个以元音开头的字符的单词。当这样的匹配被识别时,它被替换为由固定字符串组成的字符串,后跟空格和匹配的第一个子表达式,即单词本身。在这个例子中,newtext字符串将是这是一个带有错误的例子。

除了子表达式的标识符($1$2等),还有整个匹配的其他标识符($&)、第一个匹配之前的字符串部分($``)和最后一个匹配之后的字符串部分($')。在最后一个示例中,我们将日期的格式从dd.mm.yyyy更改为yyyy.mm.dd`,但也显示了匹配的部分:

    auto text{"today is 1.06.2016!!"s}; 
    auto rx =  
       std::regex{R"((\d{1,2})(\.|-|/)(\d{1,2})(\.|-|/)(\d{4}))"s};       
    // today is 2016.06.1!! 
    auto newtext1 = std::regex_replace(text, rx, R"($5$4$3$2$1)"); 
    // today is [today is ][1.06.2016][!!]!! 
    auto newtext2 = std::regex_replace(text, rx, R"([$`][$&][$'])");

正则表达式匹配一个一位数或两位数,后跟一个点、连字符或斜杠;接着是另一个一位数或两位数的数字;然后是点、连字符或斜线;最后一个四位数。

对于newtext1,替换串为$5$4$3$2$1;这意味着年,其次是第二个分隔符,然后是月,第一个分隔符,最后是日。因此,对于输入字符串*“今天是 1.06.2016!”,结果是“今天是 2016.06.1!!"*。

对于newtext2,替换串为[$][$&][$']`;这意味着第一场比赛之前的部分,然后是整个比赛,最后是最后一场比赛之后的部分都在方括号中。然而,结果却不是*”【!!】【1.06.2016】【今天是】“或许你乍一看可能会有所期待,但是“今天是【今天是】【1.06.2016】【!!]!!"。原因是被替换的是匹配的表达式,在这种情况下,那只是日期(“1 . 06 . 2016”*)。这个子字符串被替换为由初始字符串的所有部分组成的另一个字符串。

请参见

  • 使用正则表达式验证字符串的格式
  • 使用正则表达式解析字符串的内容

使用字符串视图代替常量字符串引用

使用字符串时,即使您可能并不真正意识到,临时对象也会一直被创建。很多时候,临时对象是不相关的,只是为了将数据从一个地方复制到另一个地方(例如,从一个函数复制到它的调用者)。这代表了一个性能问题,因为它们需要内存分配和数据复制,这是需要避免的。为此,C++ 17 标准提供了一个名为std::basic_string_view的新字符串类模板,该模板表示对字符串(即字符序列)的非拥有常量引用。在这个食谱中,你将学习何时以及如何使用这个课程。

准备好

string_view类在string_view头中的名称空间std中可用。

怎么做...

您应该使用std::string_view将参数传递给函数(或从函数返回值),而不是std::string const &,除非您的代码需要调用其他带有std::string参数的函数(在这种情况下,转换是必要的):

    std::string_view get_filename(std::string_view str) 
    { 
      auto const pos1 {str.find_last_of('')}; 
      auto const pos2 {str.find_last_of('.')}; 
      return str.substr(pos1 + 1, pos2 - pos1 - 1); 
    } 

    char const file1[] {R"(c:\test\example1.doc)"}; 
    auto name1 = get_filename(file1); 

    std::string file2 {R"(c:\test\example2)"}; 
    auto name2 = get_filename(file2); 

    auto name3 = get_filename(std::string_view{file1, 16});

它是如何工作的...

在我们看新的字符串类型是如何工作的之前,让我们考虑下面这个函数的例子,它应该提取一个没有扩展名的文件名。这基本上是在 C++ 17 之前,您将如何编写上一节中的函数。

Note that in this example the file separator is \ (backslash) as in Windows. For Linux-based systems, it has to be changed to / (slash).

    std::string get_filename(std::string const & str) 
    { 
      auto const pos1 {str.find_last_of('')}; 
      auto const pos2 {str.find_last_of('.')}; 
      return str.substr(pos1 + 1, pos2 - pos1 - 1); 
    } 

    auto name1 = get_filename(R"(c:\test\example1.doc)"); // example1 
    auto name2 = get_filename(R"(c:\test\example2)");     // example2 
    if(get_filename(R"(c:\test\_sample_.tmp)").front() == '_') {}

这是一个比较简单的功能。它对std::string进行常量引用,并标识由最后一个文件分隔符和最后一个点限定的子字符串,该点基本上代表没有扩展名(也没有文件夹名称)的文件名。

然而,这种代码的问题在于,它会创建一个、两个,甚至更多的临时变量,这取决于编译器的优化。函数参数是常量std::string引用,但是函数是用字符串文字调用的,这意味着std::string需要从文字构造。这些临时人员需要分配和复制数据,这既耗时又耗费资源。在最后一个例子中,我们想要做的就是检查文件名的第一个字符是否是下划线,但是我们为此至少创建了两个临时字符串对象。

std::basic_string_view类模板就是为了解决这个问题。这个类模板非常类似于std::basic_string,两者几乎有相同的接口。其原因是std::basic_string_view旨在代替对std::basic_string的持续引用,而无需进一步的代码更改。

就像std::basic_string一样,所有类型的标准字符都有专门化:

    typedef basic_string_view<char>     string_view; 
    typedef basic_string_view<wchar_t>  wstring_view; 
    typedef basic_string_view<char16_t> u16string_view; 
    typedef basic_string_view<char32_t> u32string_view;

std::basic_string_view类模板定义了对连续字符序列的引用。顾名思义,它代表一个视图,不能用于修改字符的引用序列。std::basic_string_view对象的大小相对较小,因为它只需要一个指向序列中第一个字符和长度的指针。它不仅可以由一个std::basic_string对象构成,还可以由一个指针和一个长度构成,或者由一个空终止的字符序列构成(在这种情况下,需要对字符串进行初始遍历才能找到长度)。因此std::basic_string_view类模板也可以作为多种类型字符串的通用接口(只要数据只需要读取即可)。另一方面,从std::basic_string_view转换到std::basic_string很容易,因为前者既有to_string()又有转换operator std::basic_string来创建新的std::basic_string对象。

std::basic_string_view传递给函数并返回std::basic_string_view仍然会创建这种类型的临时对象,但这些都是堆栈上的小对象(对于 64 位平台,指针和大小可以是 16 字节);因此,它们应该比分配堆空间和复制数据产生更少的性能成本。

Notice that all major compilers provide an implementation of std::basic_string that includes a small string optimization. Although the implementation details are different, they typically rely on having a statically allocated buffer of a number of characters (16 for VC++ and gcc 5 or newer) that does not involve heap operations, which are only required when the size of the string exceeds that number of characters.

除了与std::basic_string相同的方法外,std::basic_string_view还有两种方法:

  • remove_prefix():缩小视图,以 N 字符开始,以 N 字符开始。
  • remove_suffix():通过减少 N 字符的长度来缩小视图。

在以下示例中,两个成员函数用于从空格开始和结束处修剪std::string_view。函数的实现首先寻找第一个不是空格的元素,然后寻找最后一个不是空格的元素。然后,它从末尾移除最后一个非空格字符之后的所有内容,从开头移除所有内容,直到第一个非空格字符。该函数返回在两端修剪的新视图:

    std::string_view trim_view(std::string_view str) 
    { 
      auto const pos1{ str.find_first_not_of(" ") }; 
      auto const pos2{ str.find_last_not_of(" ") }; 
      str.remove_suffix(str.length() - pos2 - 1); 
      str.remove_prefix(pos1); 

      return str; 
    } 

    auto sv1{ trim_view("sample") }; 
    auto sv2{ trim_view("  sample") }; 
    auto sv3{ trim_view("sample  ") }; 
    auto sv4{ trim_view("  sample  ") }; 

    auto s1{ sv1.to_string() }; 
    auto s2{ sv2.to_string() }; 
    auto s3{ sv3.to_string() }; 
    auto s4{ sv4.to_string() };

When using an std::basic_string_view, you must be aware of two things: you cannot change the underlying data referred by a view and you must manage the lifetime of the data, as the view is a non-owning reference.

请参见

  • 创建字符串助手库