C++_Primer_学习笔记_第十七章(特殊标准库设施)

第四部分(高级主题)

1).标准库的附加特性,

  1. 求解大规模问题很有用
  2. 或者适用于特殊问题

2).我们使用的标准库的名字,实际上都是使用名字为std的命名空间中的名字。

第十七章(标准库特殊设施)

/1.tuple类型

1).tuple类似于pair成员。不同tuple类型的成员类型也不同,但一个tuple可以由任意数量的成员。每一个确定的tuple类型的成员数量也是固定的。

  • 可以应用在一些数据组合成单一的对象,但是有不必构建一个类。
  • tuple类型和它的伴随类,函数都定义在头文件tuple中。
  • 相关的操作见p636。

//1.定义和初始化tuple

1).

{
    // 当我们定义一个tuple时,需要指出每一个成员的类型
    tuple<size_t, size_t, size_t> threeD;//三个成员都为0
    tuple<string, vector<double>, int, list<int>> someval("contents", {1, 3, 4}, 34, {1, 2, 3});
    // 构建一个tuple的对象可以使用它的默认构造函数,进行  值  初始化
    // 或者为每一个成员提供一个初始值。

    // 注意这个构造函数时explicit的
    // 必须使用直接初始化
    tuple<size_t, size_t, size_t> threeD = {1, 2, 3};//错误
    tuple<size_t, size_t, size_t> threeD{1, 2, 3};//注意这里也可以使用{}

    // 类似于,make_pair函数,标准库定义了make_tuple函数。
    auto item = make_tuple("isbn", 2, 23.00);
    // make_tuple就是一个函数模板
    //并且它通过推断实参类型,得到tuple的类型
    // tuple
}

2).访问tuple的成员

  • tuple的成员都是未命名的,使用名为get的标准库 函数模板。
{
    // 使用get
    auto book = get<0>(item);//返回item的第一个成员
    auto = cnt = get<1>(item);//返回item的第二个成员
    // 我们必须指定一个显式模板实参,指定我们要访问第几个成员,
    // 必须是一个整型常量表达式,从0开始计数。
    // 并且传递一个tuple对象。
    // 函数返回的是指定成员的引用。
    get<2>(item) *= .8;//打八折

    // 如果不知道一个tuple准确的类型细节信息,可以使用两个辅助类模板来查询tuple的成员的数量和类型
    typedef decltype(item) trans;//trans 是item的类型
    size_t sz = tuple_size<trans>::value;//返回的是tuple的成员的数量
    tuple_element<1, trans>::type cnt = get<1>(item);//cnt是一个int类型

    // 使用之前需要知道一个tuple对象的类型,
    // 然后对两个类模板进行实例化。
    // 取它的value和type成员即可。  
    // 其中,value是一个public static
    // type是一个public成员,
    // 类似于get,tuple_element的索引下标也是从0开始。

}

3).关系和相等运算符

  • tuple的比较运算符,比较左右两个tuple里的成员,这一点和容器对应;但是只有两个tuple中的成员数量一样时,我们才可以比较它们。并且比较时,我们需要保证对于每一个成员相应的比较运算是有定义的。
{
    tuple<string, string> dou("1", "2");
    tuple<size_t, size_t> twoD(1, 2);
    bool b = (dou == twoD);//错误,不能比较size_t和string
    tuple<size_t, size_t, size_t> threeD(1, 2, 3);
    b = (twoD < twoD);//错误,成员数量不一致
    tuple<size_t, size_t> origin(0, 0);
    b = (origin < twoD);//正确,b为true

    // 由于在tuple中定义了==和<运算,所以可以将tuple传递给排序算法,可以在无序容器中作为关键字类型。 
}

//2.使用tuple返回多个值

1).

{
    // 每一个元素代表一家店的销售记录
    vector<vector<Sales_data>> files;
    // 对于一本书,我们在整个files中搜索出售过这本书的书店,对于每一个加与匹配的销售记录的书店,我们将创建一个tuple
    // 来保存这加书店的索引和两个迭代器。
    // 索引指出书店在files中的位置,迭代器标记给定书籍在此书店vector的起止位置。

    // 返回tuple的函数
    typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator, vector<Sales_data>::const_iterator
        >  matches;

    // findBook返回一个vector,每一个销售了给定书籍的店都有一项
    vector<matches> 
    findBook(const vector<vector<Sales_data>> &files, 
        const string &book) {
            vector<matches> ret;//初始化为空的vector
            // const迭代器,是底层的const
            for (auto it = files.cbegin(); it != files.cend(); ++it) {
                auto found = equal_range(it->cbegin(), it->cend(), 
                    book, compareIsbn);

                if (found.first != found.second)    //此书店销售了给定的书籍
                    ret.push_back(make_tuple(it - files.cbegin(), found.first, found.secod);
                
            }
            return ret;
    }
    // 默认情况下,equal_range是使用<进行比较,我们使用compareIsbn可调用对象。
}

3).对返回的结果进行处理

{
    void reportResult(istream &is, ostream &os, const vector<vector<Sales_data>> &files) {
        string s;//需要查找的书籍
        while (is >> s) {
            auto trans = findBook(files, s);
            if (trans.empty()) {
                cont << s << "not found in ant stores" << endl; 
                continue;
            }
            for (const auto &store : trans) {
                os << "store: " << getr<0>(store) << "sales: " << accumulate(get<1>(store), get<2>(store), Sales_data(s)) << endl; 
            }
        }
    }
}

/2.bitset类型

1).bitset类可以处理超过最长整型类型大小的位集合。而且位运算也变得更加肉容易。bitset类定义在头文件bitset中。

//1.定义和初始化bitset

1).bitset,是一个类模板,它类似array类,具有固定的大小。当我们定义一个bitset时,需要声明它包含多少个二进制位。

  • 它的大小必须是一个常量表达式。
{
    bitset<32> bitvec(1U);//32位,最低位为1,其他位为0;
    // 这句语句定义bitset为一个包含32位
    // 的bitset。
    // 就像vector中的包含未命名元素一样
    // bitset包含的二进制位也是命名的。我们使用位置来进行访问
    // 二进制的位也是从0开始编号的
    // 因此,编号是从0-31。
    // 编号从0开始的二进制位被称为低位
    // 编号到31号结束的二进制位被称为高位。
}

2).初始化方式表见p641表格。

  1. 使用unsigned值初始化bitset
{
    // 当我们使用一个整型值来初始化bitset时,此值被转换为unsigned long long类型,并且被当作位模式来处理
    // bitset的二进制位将是此模式的一个副本
    // 如果bitset的大小等于一个unsigned long long 中的二进制位数,则剩余的高位被置为零
    // 如果bitset的大小小于一个unsigned long long中的二进制位数。
    // 则只是用给定值中的低位,超出的部分就被丢弃。

    bitset<13> bitvec1(0xbeef);
    bitset<20> bitvec2(0xbeff);
    bitset<128> bitvec3(~0ULL);//0-63位为1,其余的为0;
    // 在64位的及其中,long long 0ULL是64个bit。
}
  1. 用一个string来初始化bitset
{
    // 我们可以使用一个string或者一个字符数组指针来初始化bitset
    // 两种情况下,字符都直接表示位模式
    // 当我们使用字符串表示数时,字符串中下标最小的字符对应最高位
    bitset<32> bitvec4("1100");//表示的二进制位为,0011,其余的位置是0
    // 这是因为如果string中包含的字符数比bitset少,则bitset的高位置为0

    // 使用字串来初始化一个bitset
    string str("10010010010010010101");
    bitset<32> bitvec5(str, 5, 4);//从str[5]开始的4位二进制位,1100。
    // 结果同上
    bitset<32> bitvec6(str, str.size() - 4);//使用最后4个字符
    // 初始化和以上的方式一样。
    // 低位赋值给高位,
    // 是平移不是翻转。
}

练习,

  • 17.9,如果输入的不是二进制数,会抛出,invalid_argument的异常。

//2.bitset操作

1).有多种检测或者设置位的方法。也支持位运算,并且含义也是与我们将这些运算符作用于unsigned的含义是相同的。

  • 操作表,见p643。
{
    // count, size, all, none,等不接受参数,返回的是整个bitset的状态。
    // set, reset, flip, 改变bitset的状态。
    // 改变bitset的状态的成员函数都是重载的
    // 对于每一个函数,不接受参数的版本是对整个集合执行给定的操作,
    // 接受一个位置参数的版本则对指定位置进行操作
    bitset<32> bitvec(1U);//低位为1,其他为0
    bool is_set = bitvec.any();//返回true,因为有一位置位
    boll is_not_set = bitvec.none();//false
    bool all_set = bitvec.all();//false
    size_t onBits = bitvec.count();//1
    size_t sz = bitvec.size();//32
    bitvec.flip();//翻转所有的位
    bitvec.reset();//所有位复位
    bitvec.set();//所有位置位

    // 置位就是等于1。
    // size操作时一个constexpr操作,允许我们使用在任何要求常量表达式的地方。

    // 可以接受参数的重载版本
    bitvec.flip(0);//翻转第一位
    bitvec.set(bitvec.size() - 1);//将最后一位置位
    b.set(0, 0);//复位最后一位
    b.reset(i);//复位第i位
    b.test(0);//返回false,因为第一位时复位的。

    // 下标运算对const属性进行了重载
    // const版本在指定位置位时返回true,
    // 否则返回false
    // 非const版本,返回bitset定义的一个特殊类型,允许我们操作指定位的值
    b[0] = 0;//将第一位复位
    b[31] = b[0];

    b[0].flip();//反转第一位
    ~b[0];//翻转第一位

    bool a = b[0];//转为bool值。

}

2).提取bitset的值

{
    // to_ulong, to_ullong操作都返回一个值,保存了于bitset对象相同的位模式
    //只有当bitset的大小等于对应的大小时,才能使用这两个操作
    unsigned long ulong = bitvec3.to_ulong();
    // 如果bitset中的值不能放入给定的类型时,两个两个操作会抛出一个overflow_error的异常。
}

3).bitset的IO运算符

{
    // 输入运算从一个输入流中读取字符,保存在一个临时的string对象中。
    // 直到读取的字符数达到对应的bitset大小时或者遇到不是1或者0的字符时,或者遇到文件尾或者遇到错误时,读取程序才会结束
    // 随即用临时的string对象来初始化bitset。
    // 此时的规则就和用string来初始化bitset是一样的
    bitset<16> bits;
    cin >> bits;
    cout << "bits: " << bits << endl; //这里输出的是string,也就是刚刚读入的string
}

4).使用bitset

{
    // 实现评分程序
    bool status;
    unsigned long quizA = 0;//此值将被当作位集合来使用
    quizA |= 1UL << 27; //指出第27个学生通过了测验
    status = quizA & (1UL << 27);//检查第二十七位学生是否通过了测验
    quizA &= ~(1UL << 27);//第二十七位学生没有通过测验
    // 使用标准库bitset进行等价的操作
    bitset<30> quizB;
    quizB.set(27);
    status = quizB[27];
    quizB.reset(27);
}

练习,

  • 17.13,好题。锻炼模板的使用,类外定义成员函数的注意事项。

/3.正则表达式

1).重点介绍如何使用。RE库(正则表达式库)定义在头文件regex中。包含的组件见p645。
2).regex可以做一些什么。

  1. regex类表示一个正则表达式。除了赋值和初始化之外,还支持一些其他的操作。见p647。
  2. 函数regex_match,regex_search确定一个给定的字符序列与一个给定的regex是否匹配。如果整个输入序列和表达式匹配,则regex_match返回true,如果输入序列中的一个字串与表达式匹配,则regex_search函数返回true

3).p646列出了,regex的函数的参数。这些函数都是返回bool,。而且被重载了。其中一个版本接受类型为smatch的附加参数。如果匹配成功,这些函数将成功匹配的相关信息保存在给定的smatch对象中。

//1.使用正则表达式库

1).从简单的例子开始。

{
    // 查找违反拼写规则“i除非在c之后,否则必须在e之前”的单词。
    // 查找不在字符c之后的字符串ei
    string  pattern("[^c]ei");
    // 我们需要包含pattern的整个单词
    pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
    regex r(pattern);//构造一个用于查找模式的regex
    smatch results;//定义一个对象保存搜索的结果
    string test_str = "receipt freind theif receive";
    // 用r在test_str中查找与pattern匹配的子串
    if (regex_search(test_str, results, r))     //如果有匹配的子串
        cout << results.str() << endl;//打印匹配的单词 


    //[^c]表示匹配任意非c的字符,
    // [^c]ei,表示三个字符,第一个不是c后面两个是ei
    //  如果我们想要得到完整的单词,
    // 由于regex使用的正则表达式语言是ECMAScript。在ECMAScript中,模式[[:alpha:]]表示任意的字母
    // 符号+表示1个或者多个字符
    // 符号*表示0个或者多个字符。

    // 我们将正则表达式存入在string中后,
    // 用来初始化一个名字为r的regex对象。

    // 如果regex_search函数匹配到字串,就会返回true。使用smatch对象results中的str成员来打印,模式匹配的部分。
    // 由于函数regex_search在输入序列中只要找到一个匹配的字串就会返回,停止查找,所以
    // 输出结果就是freind
}

2).指定regex对象的选项见表格p647

  • 当我们定义一个regex或者对一个regex使用assign赋新值的时候,可以指定一些标志来影响regex如何操作。这些标志控制regex对象的处理过程。详见p647。
  1. 对于指出编写正则表达式所用的语言的6个标志,我们必须设置其中一个,而且只能设置一个。默认情况下,ECMAScript标志被设置。从而,regex会使用ECMA-262规范。这也是很多Web浏览器所使用的正则表达式语言。
  2. 其他的三个标志允许我们指定正则表达式处理过程中与语言无关的方面。例如,我们可以指定希望正则表达式匹配过程中以大小写无关的方式进行。
{
    // 使用icase标志查找具有特定扩展名称的文件
    // 大多数系统都是以大小写无关的方式来识别扩展名的
    // 例如c++程序的扩展名可以是cc, Cc, cC, CC等。效果是一样的
    // 一个或者多个字母或者数字后面加上一个.
    // 再接上cpp或者cxx或者cc
    regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
    smatch results;
    string filename;
    while (cin >> filename) {
        if (regex_search(filename, results, r)) {
            cout << results.str() << endl;//打印匹配的结果。
        }
    }

    // 此时正则表达式会匹配指定文件的扩展名而不会理会大小写

    // 在正则表达式中,.表示匹配任意字符
    // 在其前面加上一个\去掉其特殊含义
    // 由于反斜杠也是一个特殊字符,所以
    // 得到一个.需要这样做\\.
}

3).我们可以将正则表达式本身看作是,用一种简单程序设计语言编写的“程序”。这种语言不是由c++编译器解释的。正则表达式是在运行时,当一个regex对象被初始化或者被赋予一个新模式时,才被“编译”的。与任何其他程序设计语言一样,我们用这种语言编写的正则表达式也可能会有错误。需要意识到的是,一个正则表达式的语法是否正确是在运行时才解析的。

  1. 如果我们编写的正则表达式存在错误,则在运行时会抛出一个regex_error的异常。
  2. 类似于标准异常类型,regex_error有一个what操作描述发生了什么错误;还有一个名为code的成员,返回某一个错误类型对应的数值编码;它返回的值是由具体实现定义的。RE库能抛出的标准错误,见表17.7(p649)
{
    try {
        // 漏掉一个方括号的错误
        regex r("[[:alnum:]+\\.(cpp|cxx|cc)&", regex::icase);
    } catch (regex_error e) {
        cout << e.what() << "\ncode :" << e.code << endl;
    }

    // 生成结果就是,
    regex_error(error_brack)
    The expression contained mismatched [ and ].
    code: 4
    // 我们的编译器定义了code成员,返回的是表17.7的错误类型的编号,
    // 一样的编号从0开始
    // 例如以上的错误error_brack
    // 就是第五个错误类型,编号为4.

    // 正则表达式的编译时一个非常慢的操作,特别时你是哟共了扩展的正则表达式语法
    // 或者是复杂的正则表达式时
    // 因此,构造一个regex对象以及对一个已存在的regex赋予一个新的正则表达式可能是非常耗时间的
    // 为了最小化这种开销,避免创建不必要的regex
    // 例如,当我们需要在循环里面使用正则表达式时,应该在循环体外面创建它
    // 而不是在循环体内创建
}

4).正则表达式类型和输入序列类型

  1. 输入的序列可以是普通的char数据,也可以是wchar_t数据
  2. 字符可以是保存在string,也可以是在char数组中。(宽字符版本,wstringwchar_t数组中。)
  • RE库,为这些不同的输入序列都定义了对应的类型。
  1. regex类保存类型char的正则表达式;wregex类保存类型wchar_t的正则表达式,它的操作和regex完全相同。唯一的差别是wregex的初始值必须是哟共wchar_t而不是char
  2. smatch表示string类型的输入序列;cmatch表示字符数组的输入序列;wsmatch表示宽字符串wstring的输入序列;wcmatch表示宽字符数组的输入序列。
  • 我们使用的RE库类型,必须和输入序列类型匹配。详见p650。
{
    // 如果不匹配
    regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
    smatch results; //匹配的是string的序列
    if (regex_reach("myfile.cc", results, r));//错误,待匹配的序列是一个const char *

    //将上述的smatch 改为cmatch即可。
}

//2.匹配和Regex迭代器类型

1).我们可以使用sregex_iterator迭代器类获取所有的匹配。

  • regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上。迭代器的操作见表17.9(p651)
  • 当我们将一个sregex_iterator绑定到一个string序列和一个regex对象上时,迭代器自动定位到string中的第一个匹配的位置。即,sregex_iterator的构造函数自动对给定的stringregex调用regex_search。当我们解引用迭代器时,会得到一个对应一次所有结果的samtch对象,当我们递增迭代器时,regex_search会输入序列string中查找下一个匹配。

2).使用sregex_iterator

{
    string pattern("[^c]ei");
    pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
    regex r(pattern, regex::icase);

    for (sregex_iterator it(file.begin(), file.end(), r), end_it;
        it != end_it; ++it) {
            cout << it->str() << endl;//输出匹配的单词。
        }
    // 注意end_it是一个空的sregex_iterator;起到尾后迭代器的作用。

    // 如果我们还想要匹配结果的上下文信息
    // 见表17.10,17.11
    // smatch和ssub_match类型允许我们获得匹配的上下文信息。
    // 匹配类型,有两个名为prefix和suffix的成员。调用后分别返回表示输入序列中当前匹配之前和之后的部分的ssub_match对象
    // 一个ssub_match对象有两个名为str和length的成员,分别  返回  匹配的string和该string的大小
    for (sregex_iterator it(file.begin(), file.end(), r), end_it);
            it != end_it; ++it) {
        auto pos = it->prefix().length();       //前缀的大小
        pos = pos > 40 ? pos - 40 : 0;        //最多只要40个字符
        // 从0开始的位置。
        cout << it->prefix().str().substr(pos)
                << "\n\t\t>>> " << it->str() << " <<<\n"
                << it->suffix().str().substr(0, 40)
                << endl;
            }
}

//3.使用子表达式

1).正则表达式中的模式通常包含一个或者多个子表达式。一个子表达式是模式的一部分,本身也具有意义。正则表达式语法通常用括号表示子表达式。

  • 例如,匹配文件后缀时,就是用括号类分组可能的文件扩展名。每当我们使用括号分组多个可行选项时,同时也就声明了这些选项形成子表达式。
{
    // 模式中点之前的文件名也形成子表达式
    regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$",regex::icase)// 有两个子表达式,
    // 改写程序使之输出语句值打印文件名
    if (regex_search(filename, results, r))
        cout << results.str(1) << endl; //打印第一个子表达式

    // 匹配对象不仅仅提供匹配整体的信息之外,还提供访问 模式中子表达式的能力
    // 子匹配是按照位置来匹配的。第一个子匹配的位置为0,表示整个模式对应的匹配;
    // 随后是每一个子表达式对应的匹配。
    // 默认情况下就是0。
}

2).子表达式用于,数据验证

  • 验证必须匹配特定格式的数据
{   
    // 美国的电话号码验证
    // 首先将用一个正则表达式找到可能是电话号码的序列
    // 再调用一个函数完成数据的验证

    // 首先了解ECMAScript正则表达式语言的一些特性
    //1. \{d}表示单一个数字,\{d}{n}表示一个n个数字的序列(\{d}{3}表示匹配三个数字的序列)
    //2. 在方括号中的字符集合表示匹配这些字符中的任意一个。([-. ]匹配一个短横或者一个点
    // ,  或者一个空格   注,.在方括号里面没有特殊含义)
    //3. 后接'?'的组件时可选的。
    // \{d}{3}[-. ]?\{d}{4}
    //可以匹配444-9088/.0989/ 0989/9069。/表示或者,不是语言特性,笔者偷懒而已。
    //4. 类似于c++,ECMAScript使用反斜杠\表示没有特殊含义。
    // \(\)才表示括号,而不是特殊的字符

    // 由于反斜线是c++的特殊字符,在模式中使用\的地方,我们都必须用一个额外的\来告诉c++,我们使用的是一个\而不是特殊符号
    // \\{d}{3},来表示\{d}{3}这一正则表达式

    // 验证手机号码,我们需要得到模式的组成部分。
    // 因为我们不希望的到只有一个括号的情况
    // 为了获得匹配的组成部分,我们需要定义正则表达式时使用子表达式。
    "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
    // 其中\\d{3}和\\{d}{3}含义相同
    // ([-. ]?)和([-. ])?含义也是一样的

    string phone = ...;
    regex r(phone);
    smatch results;
    string s;

    while (getline(cin, s)) {
        for (sregex_iterator it(s.begin(), s.end(), r), end_it; 
                it != end_it; ++it) {
            if (valid(*it)) 
                cout << "valid: " << it->str() << endl;
            else
                cout << "not valid: " << it->str() << endl;
        }
    }

    // 使用子匹配的操作来编写valid函数
    // pattern有7个子表达式
    // smatch对象会有8个ssub_match元素
    // smatch[0]表示整个匹配的表达式,
    // 当调用valid时,我们知道一定有一个完整的匹配。
    // 如果子表达式是完整匹配的一部分,则其对应的ssub_match对象的matched成员是true
    // 主要检查是否是完整的括号或者是没有括号
    bool valid(const smatch &m) {
        // 如果区号左边的括号是存在的
        if (m[1].matched) {
            // 则区号后面必须有一个右括号
            // 之后紧跟剩余的号码或者一个空格
            return m[3].matched && 
                (m[4].matched == 0 || m[4].str() == " ")
        else {
            // 区号后面的不能有括号
            // 另外两个组成部分间的分隔符必须匹配,即使用的分隔符要相同。
            return !m[3].matched && 
                m[4].str() == m[6].str();
        }
        }
    }
}

练习

  • 17.22,任意多个空白字符。(\s)还是(\s)*

//4.使用regex_replace

1).用来查找并且替换一个序列的时候。详见p657(表17.12)。它接受一个输入字符序列和一个regex对象,以及我们想要的输出形式的字符串。

  • 替换字符串由我们想要的字符组合和匹配的字符串中对应的子表达式组成。
{
    // 使用第二个,第五个,第七个子表达式
    // 而忽略其他的子表达式
    // 我们使用$后跟子表达式的索引号来表示一个特定的子表达式。
    string fmt = "$2.$5.$7";//将号码格式改为ddd.ddd.dddd
    // 使用
    regex r(phone);//用来寻找模式的regex对象
    string number = "(908) 555-1800";
    cout << regex_replace(number, r, fmt) << endl;
    // 输出结果是
    908.555.1800
}

2).只是替换输入序列中的一部分

{
    // 可以用在一个很大的文本中
    // 处理电话号码的格式修改
    int main() {
        string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\d{3})([-. ])?(\d{4})";
        regex r(phone);
        string s;
        smatch m;
        string fmt = "$2.$5.$7";//用来修改格式
        while (getline(cin, s)) {
            cout << regex_replace(s, r, fmt) << endl;
        }
        return 0;
    }
}

3).用来控制匹配和格式的标志

  • 与控制regex对象匹配过程的标志一样。替换过程中有相似的控制匹配和格式的控制。(这些都是标准库定义的)。详见表格17.13。
  • 这些标志可以传递给函数regex_search,regex_match或者类smatch的format成员
  • 匹配和格式化标志的类型为match_flag_type。这些值均定义在regex_constants命名空间中。与bindplaceholdersregex_constants也是在命名空间std中的命名空间。
  • using std::regex_constants::format_no_copy;
{
    // 使用格式标志
    // 默认情况下regex_replace会将输入序列全部输出
    // 没有匹配的会原样输出,
    // 匹配的按格式字符串指定的格式输出
    string fmt2 = "$2.$5.$7 ";//电话号码后面放置一个空格符来作为分隔符
    cout << regex_replace(s, r, fmt2, format_no_copy) << endl;
    // 只输出它所改变的文本
}

/4.随机数

1).在新标准之前,c和c++都依赖于一个简单的c库函数rand。此函数生成均匀分布的伪随机数,范围在0-32767之间(系统相关的最大值)。

  • 问题
  1. 需要随机的浮点数,需要非均匀分布的数。为了转换生成的随机数,类型或者分布,常常会引入非随机性。
  • 解决,定义在头文件中random的随机数库通过一组协作的类来解决这些问题,
  1. 随机数引擎类,生成随机的unsigned整数序列,范围内的每一个数被生成的概率是相同的。
  2. 随机数分布类,使用引擎返回指定类型,给定范围的,服从特定概率分布的随机数。
  • c++程序不应该使用库函数rand,应该使用default_random_engine类和恰当的分布类对象。

//1.随机数引擎和分布

1).随机数 引擎 是一个函数对象类。定义了一个调用运算符函数。

  1. 该运算符函数不接受参数
  2. 返回一个随机的unsigned整数。
{
    default_random_engine e;
    for (size_t i = 0; i < 10; ++i)
        cout << e() << " ";

    // 标准库定义了多个引擎类,区别在于性能和随机性质量不同。
    // 每一个编译器都会指定其中一个作为default_random_engine类型
    // 该类型一般具有最常用的特性。
    // 标准库定义的引擎类见p783
    // 引擎类的操作见p660

    // 我们把以上称为原始随机数
    // 因为大多数情况下,以上的输出不能直接使用
    // 问题在于范围与我们的所需要的是不符合的,而且转换是困难的
}

2).分布类型和引擎

{
    // 使用分布类型对象得到指定范围的随机数
    // 生成0-9之间的(包含)均匀分布的随机数
    uniform_int_distribution<unsigned> u(0, 9);

    default_random_engine e;
    for (size_t i = 0; i < 10; ++i) {
        cout << u(e) << " ";
    }
    // uniform_int_distribution类型生成 均匀分布 的unsigned值。
    // 当我们定义该类型的对象时。
    // 可以提供想要的最大和最小值(包含)
}
  • 分布类型也是函数对象类。它接受一个随机数引擎类作为参数,分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布中。
{
    // 注意我们传递的是一个引擎对象,而不是它的一个随机数值
    u(e);//正确
    u(e());//编译错误
    // 原因在于,某一些分布可能需要调用引擎多次才可以得到一个值
}
  • 随机数发生器,指的是分布对象和引擎对象的组合。

3).比较随机数引擎和rand函数

比较类型 rand 引擎对象
生成数的范围 在0-RAND_MAX之间 它生成的unsigned在一个系统定义的范围内,可以调用该类型对象的min()max()返回值来得到。依赖于系统。

4).引擎生成一个数值序列

{
    // 对于一个给定的发生器,每一次运行它都会返回相同的数值序列
    // 序列不变这一事实可以用来调式
    // 使用时也需要注意这一点

    vector<unsigned> bad_randVec() {
        default_random_engine e;
        uniform_int_distribution<unsigned> u(0, 9);
        vector<unsigned> ret;
        for (size_t i = 0; i < 100; ++i) {
            ret.push_back(u(e));
        }
        return ret;
    }

    //每一次调用这个函数都会返回相同的vector
    // 编写此函数的正确方法是,将引擎和关联的分布对象定义为static

    // 因为我们希望引擎和分布对象保留状态,因此我们需要把它们定义为static
    // 从而每一次调用都生成新的数
    // 这样第一次调用生成前100个随机数,
    // 第二次调用哦生成接下来的100个数
    // 以此类推
}

5).设置随机数发生器种子

  • 提供种子,seed。种子就是一个数值, 引擎 利用它从序列中的一个新位置重新开始生成随机数。
  • 两种方式提供seed
  1. 创建时设置种子
  2. 调用引擎的seed成员
{
    default_random_engine e1;//使用默认的种子
    default_random_engine e2(234235242);//使用给定的种子值
    default_random_engine e3;  //使用默认的种子值
    e3.seed(32767);     //设置一个新的种子值
    default_random_engine e4(32767);
    //d3和e4将会生成相同的随机数值
    // 因为它们的种子是一样的

    // 选择一个好的种子是及其困难的。
    // 最常用方式就是调用系统函数time
    // 该函数定义在头文件ctime中
    // 返回从一个特定时刻到当前经过了多少秒
    // 函数接受单个指针参数
    // 它指向用于写入时间的数据结构
    // 指针为空,函数简单地返回时间
    default_random_engine e(time(0));

    // 由于time是以秒计时的,这种方式生成的种子适合间隔以秒为级别的或更长的应用。
    // 如果程序是需要反复进行,time作为种子的方式可能导致多次使用的都是同一个种子
}

//3.其他随机分布

1).解决不同分布和不同类型的问题(随机引擎只是生成均匀分布的unsigned)。标准库定义了不同的随机数分布类来满足这个需求。
2).生成随机实数

  • 解决0-1之间的随机数。最常用但不正确的从rand获得一个随机浮点数的方法是使用rand()/RAND_MAX。不正确的原因是随机整数的精度通常低于随机浮点数,这样一些浮点值就永远不会生成。
  • 使用新标准库设施,支持的操作见p664。
{
    // 定义一个uniform_real_distribution 对象
    // 让标准库从随机整数到随机浮点数的映射。
    // 我们同样可以指定范围。
    default_random_engine e;
    uniform_real_distribution<double> d(0, 1);
    // 使用
    u(e);



    // 分布类型都是模板。具有单一的模板类型参数,表示分布生成的随机数的类型
    // 这些分布类型要么生成整型要么生成浮点类型
    // 每一个分布模板都有一个默认模板实参,生成浮点型的分布类型默认生成的是double;
    // 生成整型值的分布默认是int。
    // 由于分布类型只有一个模板参数,因此当我们希望使用默认随机数类型时,要在后面加上<>
    uniform_real_distribution<> u(0, 1);//默认生成的是double

    // ------生成非均匀的随机数----------
    // 除了可以指定范围,类型,还可以指定分布。
    // 20种的分布类型,见p781。
    // 生成正态分布的值的序列,并画出值得分布
    default_random_engine e;
    normal_distribution<> n(4, 1.5);//均值4,标准差1.5
    vector<unsigned> vals(9);//均为0
    for (size_t i = 0; i != 200; ++i) {
        unsigned v = lround(n(e));//舍入到最接近的整数
        if (v < val.size())
            ++vals[v];      //统计出现的次数
    }
    for (size_t i = 0; i != vals.size(); ++i) {
        cout << i << ": " << string(vals[i], "*") << endl; 
    }

    //normal_distribution生成浮点值
    // 头文件的cmath中的lround函数。得到最接近的整数
    // 以4为均值,表示以4为中心
    // 由于是正态分布,我们希望99%的数都在0-8之间。
    // 先进行统计次数。
    // 打印一个星号组成的图,来表示随机分布

    // -----------bernoulli_distribution(伯努利随机分布)
    // 该类是一个普通类。此分布总是返回一个bool,它返回true的概率是一个常数,默认是0.5
    // 编写谁先行的程序
    // 1.可以使用uniform_int_distribution来选择谁先行,范围为0-1即可
    //2.    使用bernoulli随机分布。
    string resp;
    default_random_engine e;    //需要保持状态,在函数外围定义。
    bernoulli_distribution b;   //默认是50/50的概率
    // 分布对象也需要保持状态
    // bernoulli_distribution b(.55);//表示有.55的概率得到true
    do {
        bool first = b(e);
        cout << (first ? "We go first" : "You get to go first") << endl;

        // 传递谁先进行游戏
        cout << ((play(first)) ? "sorry you lost" : "congrats, you won") << endl;

        cout << "play again ? Enter 'yes' or 'no'" << endl;
    } while (cin >> resp && resp[0] == 'y');
    // 如果随机数引擎和分布定义在里面
    // 每一次得到的随机数一样的,游戏的先行者是固定的
}

/5.IO库再探

//1.格式化输入与输出

1).除了条件状态之外,每一个iostream对象还维护一个格式状态来控制IO如何格式化的细节。格式状态控制格式化的某一些方面,

  1. 整数的进制
  2. 浮点值的精度
  3. 输出元素的宽度

2).标准库定义了一组操作符,来修改流的格式状态。见表17.17(p670)、17.8。一个操作符是一个函数或者一个对象。它们可以用作输入或输出运算符的运算对象,也返回流的对像。因此我们可以在一条语句中组合操纵符和数据。
3).操纵符用于两大类输出控制。

  1. 控制数值的输出形式
  2. 控制补白的数量和位置
  • 大多数的改变格式状态的操纵符都是设置和复原成对的。当操纵符改变流的格式状态时,通常改变后的状态对所有后续的IO都生效。
  • 通常在不需要特殊格式时尽快将流恢复到默认的状态下。

4).应用

  1. 控制布尔值的格式
{
    cout << "default bool value: " << true << " " << false 
                << "\nalpha bool value: " << 
                boolalpha << 
                true << " " << false
                << endl; 
    // 输出结果就是1 0 true false 
    // 使用了boolalpha来覆盖默认的格式

    // 将它复原
    cout << boolalpha << true << noboolalpha ;
}
  1. 控制整型的进制
{
    // 默认情况下就是十进制
    cout << "default:" << 20 << " " << 1024 << endl;
    cout << "octal(8):" << otc << 20 << 1024 << endl;
    cout << "hex(16):" << hex << 20 << 1024 << endl;
    cout << "decimal(10):" << dec << 20 << 1024 << ednl; 
    // 输出
    20 1024
    24 2000
    14 400
    20 1024
    // 注意可以进行覆盖。otc之后hex或者dec
    // 以上的操纵符只对整型影响,对于浮点值的表示形式没有影响

    // --------在输出中指出进制
    // 解决我们不知道是几进制的问题
    // 显式进制
    // 显式的规范和我们在整型常量中指定进制的规范一样
    cout << showbase;   //打印整型值时显式进制
    ...
    cout << noshowbase;

    // 输出
    20 1024
    024 02000
    0x14 0x400
    20 1024
    // 默认情况下,十六进制值会以小写打印,0x也是小写的,
    // 我们可以使用uppercase操作符号来输出大写的X并将十六进制数字也将大写的放hi输出
    cout << uppercase << showbase << hex << ..... 
                << nouppercase << noshowcase << dec << endl; 
    // 输出
    0X14 0X400
}
  1. 控制浮点数格式
{
    // -----------指定打印精度
    // 默认情况下打印的是6位数字
    // -----------指定是否打印小数点
    // 如果浮点值没有小数部分,不打印小数点
    // -----------指定格式(十六进制如何?)
    // 根据浮点数的值选择打印成定点十进制或者科学计数法的形式。
    // 标准库会选择一种可读性好的格式;非常大或者非常小的值打印为科学计数法的形式,其他打印成十进制定点的形式


    // -----------指定打印的精度
    // 默认情况下,精度会控制打印的数字总数;打印时,浮点值按当前精度舍入而不是截断
    // 例如精度为4,则3.14159将打印为3.142
    // 如果精度为3,将打印为3.14

    // 使用IO对象的precision成员或者
    // setprecision操纵符来改变精度
    // precision成员是重载的,一个版本接受int,将精度设为该值,并返回旧精度
    // 另一个版本不接受参数,返回当亲啊的精度
    // setprecision操纵符接受一个参数,用来设置精度。

    // 操纵符setprecision和其他接收参数的操纵符都定义在头文件iomanip中

    cout << "Precision: " << cout.precision() << ",value:" << sqrt(2.0) << endl;

    // 将精度设置为12
    cout.precision(12);
    cout << "..." << ....

    cout << setprecision(3);
    .....
    // 输出
    61.41421
    121.41421356237
    31.41
    // sqrt标准库函数,定义在头文件cmath中。
    // sqrt是重载的,分别接受一个float,double,long double参数。返回实参的平法根。

    //--------------指定浮点数记数法
    // 使用操纵符scientific改变流的状态使用科学计数法
    // 使用操纵符fixed改变流的状态使用定点十进制
    // 新标准库中,允许使用hexfloat强制浮点数使用十六进制格式
    // 新标准中,还提供一种名为defaultfloat的操纵符,它将流恢复到默认状态--根据要打印的值选择计数法

    // 这些操纵符也会改变精度的默认含义
    // 在执行scientific,fixed或者hexfloat时
    // 精度值控制的是小数点后面的数字位数
    cout << 100 * sqrt(2.0) << '\n' 
            << scientific
            << fixed 
            << hexfloat
            << defaultfloat
            << endl;

    // 输出
    141.421
    1.414214e+002
    141.421356
    0x1.ad7bcp+7
    141.421
    // 同理我们可以使用uppercase将小写改成大写

    //--------------打印小数点

    cout << 10.0 << endl;//10
    cout << showpoint << 10.0 
            << noshowpoint << endl;/10.000000
}
  1. 控制补白
{
    // 1、setw指定下一个数字或字符串值得最小空间
    // 2、left表示左对齐输出
    // 3、right表示右对齐输出,这是默认格式
    // 4、internal控制负数的符号的位置,它是左对齐符号,右对齐值,控制填满之间的空间
    // 5、setfill允许指定一个字符来代替默认的空客来补白输出
    // setw不改变流的内部状态,它只是改变下一个输出的大小。
    int i = -16;
    double d = 3.14159;
    ....
    cout << internal << left << right ....
    cout << sefill('#') << setw(12) << d << endl;
    cout << setw(12) << i << endl; 
    cout << setfill(' ');
    // 输出
    // 右对齐
    #########-16
    #####3.14159
    // 左对齐
    -16#########
    3.14159#####
    // internal
    -#########16
    #####3.14159
    // 注意,小数点也是一个位

    // 操纵符号也是只对特定的类型起作用,对其他类型没有影响

}
  1. 控制输入格式
{
    // 默认情况下输入运算符会忽略空白,制表,换行,换纸,回车符号
    char ch;
    while (cin >> ch) {
        cout << ch;
    }
    // 输入 
    a   b   c
    d
    // 输出
    abcd
    // 循环一共执行了四次
    // 操纵符noskipws会零输入运算读物空白符,而不是跳过
    cin >> noskipws;
    while (cin >> ch)
        cout << ch;
    cin >> skipws;//恢复默认的状态

    // 此循环会执行7次。将普通字符和空白字符均读取
    // 输出
    a b   c
    d
}

//2.未格式化的输入和输出操作

1).到目前为止我们只使用过格式化IO操作。

  1. 输入运算符忽略空白符(包含空格符…)
  2. 输出运算符应用补白,精度规则。
  • 输入和输出运算符根据读取的数据类型来格式化它们。

2).标准库还提供了一组底层操作,支持未格式化IO。这些操作允许我们将一个流当作一个无解释的字节序列来处理。
3).单字节操作

  • 有几个未格式化操作每次一个字节的处理流。见表格17.19(p673)。它们会处理而不是忽略空白符。
{
    char ch;
    while (cin.get(ch)) 
        cout,put(ch);
    // 该程序保留输入中的空白,输出和输入完全一致,它的执行过程和前一个使用noskipws完全相同
}

4).将字符放回输入流

  • 有时候,我们需要读取一个字符才知道还没有准备好处理它。此时我们希望可以退回到流中。标准库提供了三种退回字符的方式。有细微的差别
  1. peek返回输入流中下一个字符的副本,但是不会将他从流中删除。
  2. unget使得输入流向后移动,从而最后读取的值又回到流中。即使我们不知道最后从流中读取什么值,仍然可以调用unget
  3. putback,是更加特殊的unget,它退回流中读取的最后一个值,但它必须接受一个参数,此参数必须和最后读取的值相同。
  • 一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值,即,标准库 不保证 中间 不进行 读取操作的情况下, 能 连续调用putback或者unget

5).从输入操作返回的int值

  • 函数peek和无参数的get版本都以int从输入流返回一个字符。
  • 原因,可以返回文件尾标记,我们使用char范围中的每一个值来表示一个真实字符,因此,取值范围中没有额外的值可以标志文件尾。
  • 返回int的函数将它们要返回的字符先转换为unsigned char,然后再将结果提升到int。因此,即使字符集中有字符映射到负值,这些操作返回的int也是正值。而标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不一样。头文件cstdio定义了一个名为EOFconst,我们可以使用它来确定返回的是否是文件尾,而不必记忆文件尾的实际数值。
{
    int ch;//使用一个int而不是char来保存get()的返回值
    while ((ch = cin.get() != EOF)) 
        cout.put(ch);
    // 接受参数的版本,可以自动的检测到EOF
    // 使用while检测流的状态
}

6).多字节操作

  • 一次处理大的数据块。速度快,但是类似于其他底层操作,这些操作容易出错。这些操作要求我们自己分配并管理保存和提取数据的字符数组。表17.20,(p674)列出了这些操作。
  • getgetline函数接受相同的参数,它们的行为类似但是不相同。sink都是一个char数组,用来保存数据,两个函数都一直读取数据,知道以下条件之一发生。
  1. 数组已经满了
  2. 遇到文件的末尾
  3. 遇到分隔符(由我们指定)
  • 两个函数的差别在于对于分隔符的处理,
  1. get将分割符留作istream的下一个字符
  2. getline则将其读取并且丢弃。
  • 无论哪一个函数都不会将分隔符保存再sink中。
  • 一个常见的错误是本想着从流中删除分隔符,但是没有做。

7).确定读入了多少个字符

  • 调用gcount来确定最后一个未格式输入操作读取了多少个字符。
  • 应该再任何后续未格式化输入操作之前调用gcount。(明确读取的字符数目)
  • 将字符退回流的单字符操作也是属于未格式化操作。如果在调用gcount之前调用了peek,unget,getback,则gcount返回值为0。

8).底层函数容易出错。

  1. 例如,将get,peek的返回值赋予一个char而不是int。这样做是错误的,但是编译器不会发现这个错误。具体发生什么错误,取决于机器和输入数据。
  • 在一个char被实现为unsigned的机器上,while ((ch = cin.get() != EOF))永远都不会停止循环。
  • 在一个视charsigned char上,会发生什么?有的不能确定循环的行为;有的遇到变量的值越界了,依赖编译器如何处理这种越界;有的恰好可以正常工作,除非遇到和EOF相等的情况,虽然在普通数据中共这种字符不太可能,但是底层的IO通常用于读取二进制值,二进制值不能直接映射到普通字符和数值。…
  • 总之,读写有类型的值,格式化的数值,这样的错误就不会发生。可以使用标准库更加安全,更加高层的操作。就使用标准库的操作。

练习

  • 理解未格式化版本的getline,暂时略过。

//3.流随机访问操作

1).各种流类型通常都支持对流中数据的随机访问。**我们可以重定位流,使之跳过一些数据,首先读取最后一行,然后读取第一行,以此类推。**标准库提供了一对函数(成员函数。),

  1. seek定位到流中的给定位置
  2. tell返回我们当前的位置。
  • 随机IO本质上是依赖于系统的,为了理解如何使用这些特性,必须查询系统文档。

2).虽然标准库为所有的流类型都定义了seektell函数,但他们是否会做有意义的事情,依赖于流绑定的设备。绑定到cin,cout,cerr,clog的流设备不支持随机访问——当我们cout直接输出数据时,类似向回跳十个位置这样的操作是没有意义的…对这些流我们可以调用seek,tell函数,但是在运行时会出错,将流置为无效状态。

  • 由于istream,ostream类型通常不支持随机访问,以下内容只是用于fstream,和sstream类型。

3).seek,tell函数,详见表格17.21(p676)

  • 为了支持随机访问,IO类型维护一个标记来确定下一个读写操作要在哪里进行。还提供了以上两个函数。实际上,标准库定义类两对的seek,tell分别用于输入流,输出流。差别在于后缀一个是g(get)读取,输入;一个是p(put)写入,输出。
  • 对于iostream,fstream,stringstream既可以读又可以写的关联的流,可以对这些类型使用g和p。

4).标准库虽然区分读写的版本,但是在流中只有单一的标记–不存在独立的读标记和写标记。

  • 当我们处理一个只读或者只写的流时,这种特性没有体现出来;因为我们试图对输出流使用g,或者对输入流使用p将会报错。
  • 而对于fstream,stringstream。它们有单一的缓冲区,用来保存读写的数据,标记只有一个,表示缓冲区当前的位置。标准库将g和p版本的读写都映射到这个单一的标记。由于只有单一的标记,我们要在读写操作间进行切换时,就必须使用seek操作来重定位标记。

5).重定位标记

{
    // 将标记移动到一个固定的位置
    seekg(new_position);//将读标记移动到一个指定的pos_type类型的位置上
    seekp(new_position);//将写标记移动到指定的pos_type位置上

    // 移动到给的那个起止点之前或之后指定的偏移位置
    seekg(offset, from);//将读标记移动到距from偏移量为offset的位置
    seekp(offset, from);
    // from可能值见表17.21(p676)

    // new_positon类型为pos_type
    // offset类型为off_type
    // 以上两个类型都是机器相关的
    // 定义在头文件istream和ostream中
    // pos_type表示一个文件位置
    // off_set的值可以是整的也可以为负
    // 通过正负确定是向前还是向后移动


    // -----------访问标记
    // tell函数返回一个类型为pos_type的值,表示当前的流的位置
    // tell函数通常是为了记住一个位置,以便稍后回来改位置
    ostringstream writeStr;
    ostringstream::pos_type mark = writeStr.tellp();
    // ...
    if (cancelEntry) {
        // 回到刚才的位置
        writeStr.seekp(mark);
    }

    // ---------读写同一个文件
    // 给定一个文件,在文件末尾写入新的一行,包含文件中每一行的相对起始位置。
    abcd
    efg
    hi
    j
    5 9 12 14
    // 注意偏移量包括行尾的不可见的换行符

    int main() {
        // 以读写的方式打开文件并且定位到文件尾
        fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out);
        if (!inOut) {
            cerr << "..." << endl;
            return EXIT_FALLURE;//这是一个预处理变量,定义在头文件cstdlib中
        }
        // inOut以ate的方式打开,因此一开始就是定位在文件的末尾
        auto end_mark = inOut.tellg();      //记住 读取 的末尾位置
        inOut.seekg(0, fstream::beg);//重定位到文件开始的位置
        size_t cnt = 0; //字节数计数器
        string line;//保存每一行
        // 如果读取没有失败
        // 并且还没有 读取 到达文件end_mark
        // 
        while (inOut && inOut.tellg() != end_mark 
                    && getline(inOut, line)) {
            cnt += line.size() + 1;//加1表示换行符
            auto mark = inOut.tellg();//记住当前 读取 的位置
            intOut.seekp(0, fstream::end);  //将 写 的标记移动到文件的末尾
            inOut << cnt ;//写入数据
            if (mark != end_mark) inOut << " ";//如果不是最后一行,打印一个分割符
            inOut.seekg(mark);//恢复到上一次 读 的位置
        }
        inOut.seekp(0, fstream::end);//定位到文件的末尾
        inOut << '\n';//写入换行符号。
        return 0}

    // 绝对位置是没有from的三个特殊值得
    // 使用重载的版本得到文件的起始和结束位置
}

/6.小结

1).match对象是sub_match对象的容器(反映的是整个正则表达式(0号元素)/子表达式的匹配结果。),而且它还有成员prefix(),suffix()返回匹配之前和之后的信息。也是一个sub_match对象。

你可能感兴趣的:(c++_primer_note,c++11,c++,c语言,编程语言,经验分享)