美文网首页
《C++Primer》第十七章 标准库特殊设施

《C++Primer》第十七章 标准库特殊设施

作者: TOMOCAT | 来源:发表于2020-12-02 00:53 被阅读0次

    第十七章 标准库特殊设施

    tuple类型

    tuple是类似pair的模板,每个pair的成员类型都不相同,但是每个pair恰好有两个成员。我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据,这时候就可以用到tuple

    我们可以将tuple当做一个”快速而随意”的数据结构。

    它支持的操作包括:

    • tuple<T1, T2, ..., Tn> t;:成元素为n,第i个成员为Ti,所有成员进行值初始化
    • tuple<T1, T2, ..., Tn> t(v1, v2, ..., vn);:每个成员用对应的初始值vi进行初始化,此构造函数是explicit
    • make_tuple(v1, v2, ..., vn):返回一个用给定初始值初始化的tupletuple的类型从初始值的类型推断
    • t1 == t2, t1 != t2:两个tuple具有相同数量的成员且成员对应相等时则两个tuple相等
    • t1 relop t2:两个tuple必须具有相同数量的成员,用<运算符比较t1t2对应的成员
    • get<i>(t):返回t的第i个数据成员的引用,如果t是一个左值则返回左值引用,否则返回一个右值引用
    • tuple_size<tupleType>::value:一个类模板,可以通过tuple类型来初始化,表示给定tuple类型中成员的数量
    • tuple_element<i, tupleType>::type:一个类模板,可以通过一个整型常量和一个tuple类型初始化,返回tuple类型中指定成员的类型

    1. 定义和初始化tuple

    使用构造函数:

    tuple<size_t, size_t, size_t> threeD;    // 三个成员都值初始化为0
    tuple<string, vector<double>, int, list<int>>
        someVal("constants", {3.14, 2.718}, 42, {0,1,2,3,4,5}) // 提供初始值
        
    // 注意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_tuple

    auto item = make_tuple("0-999-78345-X", 3, 20.00);
    

    2. 访问tuple的成员

    使用get<i>(t)即可返回tuplei个成员的引用,如果我们不知道tuple准备的类型细节,可以使用两个辅助类模板来查询tuple成员的数量和类型:

    typedef decltype(item) trans;          // trans是item的类型(某种tuple)
    // 返回trans中成员数量
    size_t sz = tuple_size<trans>::value;  // 返回3
    // cnt的类型与item中第二个成员相同
    tuple_element<1, trans>::type cnt = get<1>(item);  // cnt的类型是一个int
    

    3. 使用tuple返回多个值

    tuple的一个常见用途就是从一个函数返回多个相关的值,如果函数返回两个值我们可以使用pair,返回三个值及以上我们就可以使用tuple了。

    bitset类型

    标注库定义了bitset类让位运算的使用更加容易,并且能够处理超过最长整形类型大小的位集合。

    1. 定义和初始化bitset

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

    bitset<32> bitvec(1U);  // 32位, 低位为1其他位为0
    

    初始化的方法:

    • bitset<n> b:每一位均为0
    • bitset<n> b(u)bunsigned long longu的低n位的拷贝,如果n大于unsigned long long的大小,那么超过的高位被置为0
    • bitset<n> b(s, pos, m, zero, one)bstring s从位置pos开始m个字符的拷贝。s只能包含字符zeroone,如果包含其他字符会抛出invalid_argument的错误。zero默认为0one默认为1
    • bitset<n> b(cp, pos, m, zero, one):和上面类似,只不过从cp指向的字符数组中拷贝字符

    2. bitset操作

    • b.any()b中是否存在置位的二进制位

    • b.all()b中所有位都置位了吗

    • b.none()b中不存在置位的二进制位吗

    • b.count()b中置位的位数

    • b.size():返回b的位数

    • b.test(pos):返回pos位置是否置位

    • b.set(pos, v):将位置pos处的位设置为boolv

    • b.set():将b中所有位置位

    • b.reset(pos):将pos复位

    • b.reset():将所有位复位

    • b.flip(pos):将位置pos处的位改变

    • b.flip():改变每一位的状态

    • b[pos]:访问pos

    • b.to_ulong():返回一个unsigned long

    • b.to_ullong():返回一个unsigned long long

    • b.to_string(zero, ont):返回一个string

    • os << b:将b中二进制位打印为字符1或者0

    • is >> b:从is读取字符存入b,当下一个字符不是10,或者已经读入b.size()个位时停止

    正则表达式

    正则表达式的组件包括:

    • regex:正则表达式的类
    • regex_match:将一个字符序列与一个正则表达式匹配
    • regex_search:寻找第一个与正则表达式匹配的子序列
    • regex_replace:使用给定格式替换一个正则表达式
    • sregex_interator:迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串
    • smatch:容器类,保存在string中搜索的结果
    • ssub_matchstring中匹配的子表达式的结果

    其中regex_searchregex_match的参数如下,它们都会返回bool值指出是否找到匹配:

    • (seq, m, r, mft)
    • (seq, r, mft)

    上述表示在字符序列seq中查找regex对象r中的正则表达式,其中seq可以是一个string,表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针。m是一个match对象,用于保存匹配结果的相关细节。mft是一个可选的regex_constants::match_flag_type值,它们会影响匹配过程。

    1. 使用正则表达式库

    指定regex对象的选项:

    • regex(re), regex(re, f)re表示一个正则表达式,f是指出对象如何处理的标志,默认值为ECMAScript
    • r1 = re:将r1中的正则表达式替换为re
    • r1.assign(re, f):替换
    • r.mark_count()r中子表达式的数目
    • r.flags():返回r的标志集

    定义regex可选的标志包括:

    • icase:忽略大小写
    • nosubs:不保存匹配的子表达式
    • optimize:执行速度优于构造速度
    • ECMASript:使用ECMA-262指定的语法
    • basic:使用POSIX基本的正则表达式语法
    • extended:使用POSIX扩展的正则表达式语法
    • awk:使用POSIX版本的awk语言的语法
    • grep:使用POSIX版本的grep的语法
    • egrep:使用POSIX版本的egrep的语法

    2. 使用正则表达式的错误

    需要意识的一点是,一个正则表达式的语法是否正确是在运行时解析的。

    如果我们编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常:

    try {
        // 错误: alnum漏掉了右括号, 构造函数会抛出异常
        regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", regex::icase);
    } catch (regex_error e)
        { cout << e.what() << "\ncode:" << e.code() << endl; }
    

    一个正则表达式是在运行时而非编译时编译的,并且正则表达式的编译是一个非常慢的操作,特别是使用了扩展的正则表达式或者是复杂的正则表达式时。为了最小化这种开销,你应该努力避免创建很多不必要的regex,特别是如果你在循环中能够使用正则表达式,那么你应该在循环外创建它而不是在每步迭代时都编译它。

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

    输入序列类型 对应的正则表达式类
    string regex, smatch, ssub_match, sregex_iterator
    const char* regex, cmatch, csub_match, cregex_iterator
    wstring wregex, wstmatch, wssub_match, wsregex_iterator
    const wchat_t* Wregex, wcmatch, wcsub_match, wcregex_iterator

    4. Regex迭代器类型

    sregex_iterator操作如下,下面这些操作也适用于cregex_iteratorwsregex_iteratorwcregex_iterator

    • sregex_iterator it(b, e, r):一个``sregex_iterator,遍历迭代器be表示的string,它调用sregex_search(b, e, r)it`定位到输入中第一个匹配的位置
    • ``sregex_iterator end;sregex_iterator`的尾后迭代器
    • *itit->:根据最后一个调用regex_search的结果,返回一个smatch对象的引用或一个指向smatch对象的指针
    • ++itit++:从输入序列当前匹配位置开始调用regex_search,前置版本返回递增后迭代器,后置版本返回旧值
    • itt1 == it2it1 != it2:如果两个都是尾后迭代器则相等,两个非尾后迭代器是从相同的输入序列和regex对象构造,则它们相等
    // 查找前一个字符不是c的字符串ei
    string pattern("[^c]ei");
    // 我们想要包含pattern的单词的全部内容
    pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
    regex r(pattern, regex::icase);  // 在进行匹配时忽视大小写
    // 反复调用regex_search来寻找文件中的所有匹配
    for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it)
        cout << it->str() << endl; // 打印匹配的单词
    

    5. 使用匹配数据

    我们可以对smatch进行操作获取匹配的上下文。例如:

    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个字符
        cout << it->prefix().str().substr(pos)   // 前缀的最后一部分, 最多40个字符
            << "\n\t\t>>> " << it->str() << " <<<\n"  // 匹配的单词
            << it->suffix().str().substr(0, 40)       // 后缀的第一部分
            << endl;
    }
    

    smatch操作包括,下面这些操作也适用于cmatchwsmatchwcmatch和对应的csub_matchwssub_matchwcsub_match

    • m.ready():如果已经通过调用regex_search或者regex_match设置了m则返回true,否则返回false。如果ready返回false则对m进行操作是未定义的
    • m.size():如果匹配失败则返回0,否则返回最近一次匹配的正则表达式中子表达式的数目
    • m.empty():如果m.size()为0则返回true
    • m.prefix():一个ssub_match对象,表示当前匹配之前的序列
    • m.suffix():一个ssub_match对象,表示当前匹配之后的部分
    • m.format(...):正则表达式替换操作

    下面接受一个索引的操作中,n的默认值为0且必须小于m.size(),第一个子匹配(索引为0)表示整个匹配:

    • m.length(n):第n个匹配的子表达式的大小
    • m.position(n):第n个子表达式距序列开始的距离
    • m.str(n):第n个子表达式匹配的string
    • m[n]:对应第n个子表达式的ssub_match对象
    • m.begin(), m.end():表示msub_match元素范围的迭代器
    • m.cbegin(), m.cend():返回常量迭代器

    6. 使用子表达式

    正则表达式中的模板通常包含一个或多个子表达式subexpression,正则表达式语法通常用括号表示子表达式。

    // r有两个子表达式: 第一个是点之前表示文件名的部分, 第二个表示文件扩展名
    regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase);
    

    举个例子,美国的电话号码有10个数字,包含一个区号和一个七位的本地号码,区号通常放在括号里里面,但这并不是必须的。剩余的七位数字可以用一个短横线、一个点或者一个空格分隔。但也可以完全不用分隔符。

    // 包含7个子表达式: (ddd)分隔符ddd分隔符dddd
    // 子表达式1,3,4,6是可选的;2,5,7保存号码
    "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{4})"
    
    1. (\\()?:表示区号部分可选的左括号
    2. (\\d{3}):表示区号
    3. (\\))?:表示区号部分可选的右括号
    4. ([-. ])?:表示区号部分可选的分隔符,横线、点或者空格
    5. (\\d{3}):表示号码的下三位数字
    6. ([-. ])?:可选的分隔符
    7. (\\d{4}):表示号码最后的四位数字

    另外需要注意的是,我们希望验证区号部分的数字如果用了左括号,那么它也必须使用右括号,即我们不希望匹配到(908.555.1800这样的号码。下面的代码读取一个文件,用此模式查找与完成的电话号码匹配的数据,然后调用一个valid的函数来检查号码格式是否合法:

    string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{4})";
    regex r(phone);  // regex对象, 用于查找我们的模式
    smatch m;
    string s;
    // 从输入文件读取每条记录
    while (getline(cin, s)) {
        // 对每个匹配的电话号码
        for (sregex_iterartor 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;
    }
    

    由于我们的pattern有七个子表达式,每个smatch对象会包含八个ssub_match元素。位置[0]表示整个匹配,[1]...[7]表示每个对应的子表达式。valid函数的写法如下:

    bool valid(const smatch& m)
    {
        // 如果区号前有一个左括号
        if(m[1].matched)
            // 则区号后必须有一个右括号,后面紧跟剩余号码或一个空格
            retrun m[3].matched
                && (m[4].matched == 0 || m[4].str() == " ");
        else
            // 否则,区号后不能有右括号
            // 令两个组成部分间的分隔符必须匹配
            return !m[3].matched
                && m[4].str() == m[6].str();
    }
    

    7. 使用regex_replace

    当我们希望在输入序列汇总查找并替换一个正则表达式时,可以调用regex_replace。正则表达式替换操作如下:

    • m.format(dest, fmt, mft)或者m.format(fmt, mft)

    使用格式化字符串fmt生成格式化输出,匹配在m中,可选的match_flag_type标志在mft中。第一个版本西而入迭代器dest指向目的地位置并接受fmt参数,可以是一个string也可以用是表示字符数组中范围的一对指针。第二个版本返回一个string,也可以是指向一个空字符结尾的字符数组的指针。mft的默认值是format_default

    • regex_replace(dest, seq, r, fmt, mft)regexe_replace(seq, r, fmt, mft)

    遍历seq,用regex_search查找与regex对象r匹配的子串。使用格式字符串fmt和可选的match_flag_type标志来生成输出。

    string fmt = "$2.$5.$7";   // 将号码格式改成ddd.ddd.dddd
    regex r(phone);
    string number = "(908) 555-1800";
    cout << regex_replace(number, r, fmt) << endl;
    // 输出908.555.1800
    

    随机数

    在新标准出现之前,C或者C++都依赖于一个简单的C库函数rand来生成随机数。此函数生成均匀分布的伪随机整数,每个随机数的范围在0和一个系统相关的最大值(至少为32767)之间。

    使用rand库函数会带来一个问题:很多程序需要不同范围的随机数,一些与应用需要随机浮点数而另一些应用需要非均匀分布的数。程序员为了解决这些问题而试图转换rand生成的随机数的范围、类型或者分布时,常常会引入非随机性。

    1. 随机数引擎和分布

    我们可以调用一个随机数引擎对象来生成原始随机数:

    default_random_engine e;  // 生成随机无符号数
    for (size_t i = 0; i < 10; ++i) 
        // e() "调用"对象来生成下一个随机数
        cout << e() << " ";
    

    随机数引擎的操作如下:

    • Engine e:默认构造函数;使用该引擎类型默认的种子

    • Engine e(s):使用整形值s作为种子

    • e.seed(s):使用种子s重置引擎的状态

    • e.min()e.max():此引擎可生成的最小值和最大值

    • Engine::result_type:此引擎生成的unsigned整型类型

    • e.discard(u):将引擎推进u步,u的类型是unsigned long long

    使用分布:

    // 生成0~9之间(包含0和9)均匀分布的随机数
    uniform_int_distribution<unsigned> u(0,9);
    default_random_engine e;
    for (size_t i = 0; i < 10; ++i)
        // 将u作为随机数源
        // 每个调用返回在指定范围内并服从均匀分布的值
        cout << u(e) << endl;
    

    2. 序列不变性问题

    即使生成的数看起来是随机的,但是对于一个给定的发生器,每次运行程序它都会返回相同的数值序列。下面这种写法每次调用这个函数都会返回相同的100个数:

    // 几乎肯定是生成随机整数vector的错误方法
    // 每次调用都会生成相同的100个整数
    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;
    }
    

    正确的方法是将引擎和关联的分布对象定义为static的:

    // 返回一个vector, 包含100个均匀分布的随机数
    vector <unsigned> bad_randVec()
    {
        static default_random_engine e;
        static 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;
    }
    

    由于eustatic的,因此它们会在函数调用之间保持住状态,第一次调用会使用u(e)生成的序列的前100个随机数,第二次调用会获得接下来100个,从而不会完全相同。

    3. 使用种子

    default_random_engine e1(time(0));  // 稍微随机些的种子
    

    由于time返回以秒计的时间,因此这种方法只适用于生成种子的间隔为秒级或更长时间的应用。

    4. 分布类型

    分布类型的操作如下:

    • Dist d;:默认构造函数,使d准备好被使用
    • d(e):用相同的e连续调用d的话,会根据d的分布式类型生成一个随机数序列,e是一个随机数引擎对象
    • d.min()d.max():返回d(e)的最小值和最大值
    • d.reset():重置d的状态,使得随后对d的使用不依赖于d已经生成的值

    常用的分布类型:

    default_random_engine e;
    
    uniform_real_distribution<double> u(0,1);  // 0到1(包含0和1)的均匀分布
    normal_distribution<> n(4,1.5);            // 均值4, 标准差1.5的正态分布
    
    vector<unsigned> vals(9);  // 9个元素均为0
    for (size_t i = 0; i != 200; ++i) {
        unsigned v = lround(n(e));   // 舍入到最接近的整数
        if (v < vals.size())         // 如果结果在范围内
            ++vals[v];               // 统计每个结果出现的次数
    }
    
    // 用于统计0~9附近各出现了多少次, 结果呈现一个正态分布
    

    还有伯努利分布:

    default_random_engine e;
    bernoulli_distribution b;
    b(e);  // 50%的几率返回true, 50%几率返回false
    

    IO库再探

    1. 格式化输入和输出

    • endl:操作符的一种,输出一个换行符并刷新缓冲区

    • boolalphanoboolalpha:打印布尔值为true和false

    • octhexdec:控制整数的进制,不影响浮点值的表示

    • showbasenoshowbase:在输出中指出进制

    • precisionsetprecision:设置精度

    • showpoint:对浮点值总是显示小数点

    • showposnoshowpos:对非负数显示+

    • uppercasenouppercase:在十六进制中打印0X,在科学技术法中打印E

    • leftrightinernal:在值的右侧、左侧、符号和值之间添加填充字符

    • fixed:浮点值显示为定点十进制

    • scientific:浮点值显示为科学计数法

    • hexfloat:浮点值显示为十六进制(C++11新特性)

    • defaultfloat:重置浮点数格式为十进制(C++11新特性)

    • unitbuf:每次输出操作后都刷新缓冲区

    • nounitbuf:恢复正常的缓冲区刷新方式

    • skipwsnoskipws:输入运算符跳过/不跳过空白符

    • flush:刷新ostream缓冲区

    • ends:插入空字符,然后刷新ostream缓冲区

    • endl:插入换行,然后刷新ostream缓冲区

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

    前面我们提到的输入运算符忽略空白符,输出运算符应用补白、精度等规则。标准库还提供了一组低层操作,支持未格式化IO,这些操作允许我们将一个流当做一个无解释的字节序列来处理。

    2.1 单字节操作

    有几个未格式化操作每次一个字节地处理流,它们会读取而不是忽略空白符。例如我们使用未格式化IO操作get和put来读取和写入一个字符:

    char ch;
    while (cin.get(ch))
        cout.put(ch);
    

    具体操作包括:

    • is.get(ch):从istream is读取下一个字符存入字符ch中,返回is
    • os.put(ch):将字符ch输出到ostream os,返回os
    • is.get():将is的下一个字节作为int返回
    • is.putback(ch):将字符ch放回is,返回is
    • is.unget():将is向后移动一个字节,返回is
    • is.peek():将下一个字节作为int返回,但不从流中删除它
    2.2 多字节操作
    • is.get(sink, size, delim):从is中读取最多size个字节,并保存在字符数组中(sink是字符数组的起始地址),读取过程直到遇到字符delim或读取了size个字节或遇到文件尾时停止。如果遇到了delim,则将其留在输入流中,不读取出来存入sink
    • is.getline(sink, size, delim):与上面类似,但是会读取并丢弃delim
    • is.read(sink, size):至多读取size个字节,村融入字符数组sink中,返回is
    • is.gcount:返回上一个未格式化读取操作从is中读取的字节数
    • os.write(source, size):将字符数组sourcesize个字节写入os,返回os
    • is.ignore(size, delim):读取并忽略最多size个字符,包括delim

    3. 流随机访问

    标准库提供了一对函数,来定位seek到流中给定的位置,以及告诉tell我们当前位置。虽然标准库为所有流类型都定义了seektell函数,但是他们是否会做又有意义的事情依赖于流绑定到哪个设备。在大多数系统中,绑定到cincoutcerrclog的流不支持随机访问。对于这些流我们可以调用seektell函数,但在运行时会出错,将流置于一个无效状态。

    由于istreamostream通常不支持随机访问,因此本节内容只适用于fstreamsstream

    3.1 seek和tell函数
    • tellg()tellp():返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置
    • seekg(pos)seekp(pos):在一个输入流或输出流中将标记重定位到给定的绝对地址,pos通常是前一个tellgtellp返回的值
    • seekp(off, from)seekg(off, from):在一个输入流或者输出流中将标记定位到from之前或之后off个字符,from可以是下列值之一:
      • beg:偏移量相对于流开始位置
      • cur:偏移量相对于流当前位置
      • end:偏移量相对于流结束位置
    3.2 重定位标记

    seek函数有两个版本:一个移动到文件中的“绝对”地址,另一个移动到给定位置的指定偏移量

    // 将标记移动到一个固定位置
    seekg(new_position);  // 将读标记移动到指定的pos_type类型的位置
    seekp(new_position);  // 将写标记移动到指定的pos_type类型的位置
    
    // 移动到给定起始点之前或之后指定的偏移位置
    seekg(offset, from);  // 将度标记移动到距from偏移量为offset的位置
    seekp(offset, from);  // 将写标记移动到距from偏移量为offset的位置
    
    3.3 访问标记

    函数tellgtellp返回一个pos_type值,表示流的当前位置。tell函数通常用来记住一个位置,以便稍后再定位回来:

    // 记住当前写位置
    ostringstream writeStr;   // 删除stringstream
    ostringstream::pos_type mark = writeStr.tellp();
    
    // ...
    if (cancelEntry)
        // 回到刚才记住的位置
        writeStr.seekp(mark);
    
    3.4 实例

    给定一个文件:

    abcd
    efg
    hi
    j
    

    我们需要在文件的末尾写入一行,这一行包含文件中每行的相对起始位置,写完后为:

    abcd
    efg
    hi
    j
    5 9 12 14
    
    int main()
    {
        // 以读方式打开文件,并定位到文件尾
        fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out);
        if(!inOut) {
            cerr << "Unable to open file!" << endl;
            return EXIT_FAILURE;
        }
        // inOut以ate模式打开,因此一开始就定义到其文件尾
        auto end_mark = inOut.tellg();   // 记住原文件尾位置
        inOut.seekg(0, fstream::beg);    // 重定位到文件开始
        size_t cnt = 0;                  // 字节数累加器
        string line;                     // 保存输入中的每行
        // 继续读取的条件: 还未遇到错误且还在读取原数据
        while (inOut && inOut.tellg() != end_mark && getline(inOut, line)) {  // 且还可以获取一行输入
            cnt += line.size() + 1;         // +1表示换行符
            auto mark = inOut.tellg();      // 记住读取位置
            inOut.seekp(0, fstream::end);   // 将写标记拖动到文件末尾
            intOut << cnt;                  // 输出累计的长度
            // 如果不是最后一行,打印一个分隔符
            if (mark != end_mark) inOut << " ";
            inOut.seekg(mark);              // 恢复读位置
        }
        inOut.seekp(0, fstream::end);       // 定位到文件尾
        inOut << "\n";                      // 在文件末尾输出一个换行符
        return 0;
    }
    

    相关文章

      网友评论

          本文标题:《C++Primer》第十七章 标准库特殊设施

          本文链接:https://www.haomeiwen.com/subject/epzewktx.html