第十七章 标准库特殊设施
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)
:返回一个用给定初始值初始化的tuple
,tuple
的类型从初始值的类型推断 -
t1 == t2, t1 != t2
:两个tuple
具有相同数量的成员且成员对应相等时则两个tuple
相等 -
t1 relop t2
:两个tuple
必须具有相同数量的成员,用<
运算符比较t1
和t2
对应的成员 -
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)
即可返回tuple
第i
个成员的引用,如果我们不知道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)
:b
是unsigned long long
值u
的低n
位的拷贝,如果n
大于unsigned long long
的大小,那么超过的高位被置为0 -
bitset<n> b(s, pos, m, zero, one)
:b
是string s
从位置pos
开始m
个字符的拷贝。s
只能包含字符zero
和one
,如果包含其他字符会抛出invalid_argument
的错误。zero
默认为0
而one
默认为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
处的位设置为bool
值v
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
,当下一个字符不是1
或0
,或者已经读入b.size()
个位时停止
正则表达式
正则表达式的组件包括:
-
regex
:正则表达式的类 -
regex_match
:将一个字符序列与一个正则表达式匹配 -
regex_search
:寻找第一个与正则表达式匹配的子序列 -
regex_replace
:使用给定格式替换一个正则表达式 -
sregex_interator
:迭代器适配器,调用regex_search
来遍历一个string
中所有匹配的子串 -
smatch
:容器类,保存在string
中搜索的结果 -
ssub_match
:string
中匹配的子表达式的结果
其中regex_search
和regex_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_iterator
,wsregex_iterator
和wcregex_iterator
:
-
sregex_iterator it(b, e, r)
:一个``sregex_iterator,遍历迭代器
b和
e表示的
string,它调用
sregex_search(b, e, r)将
it`定位到输入中第一个匹配的位置 - ``sregex_iterator end;
:
sregex_iterator`的尾后迭代器 -
*it
,it->
:根据最后一个调用regex_search
的结果,返回一个smatch
对象的引用或一个指向smatch
对象的指针 -
++it
,it++
:从输入序列当前匹配位置开始调用regex_search
,前置版本返回递增后迭代器,后置版本返回旧值 -
itt1 == it2
,it1 != 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
操作包括,下面这些操作也适用于cmatch
,wsmatch
,wcmatch
和对应的csub_match
,wssub_match
和wcsub_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()
:表示m
中sub_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})"
-
(\\()?
:表示区号部分可选的左括号 -
(\\d{3}
):表示区号 -
(\\))?
:表示区号部分可选的右括号 -
([-. ])?
:表示区号部分可选的分隔符,横线、点或者空格 -
(\\d{3})
:表示号码的下三位数字 -
([-. ])?
:可选的分隔符 -
(\\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;
}
由于e
和u
是static
的,因此它们会在函数调用之间保持住状态,第一次调用会使用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
:操作符的一种,输出一个换行符并刷新缓冲区boolalpha
和noboolalpha
:打印布尔值为true和falseoct
、hex
和dec
:控制整数的进制,不影响浮点值的表示showbase
和noshowbase
:在输出中指出进制precision
和setprecision
:设置精度showpoint
:对浮点值总是显示小数点showpos
和noshowpos
:对非负数显示+
uppercase
和nouppercase
:在十六进制中打印0X
,在科学技术法中打印E
left
、right
和inernal
:在值的右侧、左侧、符号和值之间添加填充字符fixed
:浮点值显示为定点十进制scientific
:浮点值显示为科学计数法hexfloat
:浮点值显示为十六进制(C++11新特性)defaultfloat
:重置浮点数格式为十进制(C++11新特性)unitbuf
:每次输出操作后都刷新缓冲区nounitbuf
:恢复正常的缓冲区刷新方式skipws
和noskipws
:输入运算符跳过/不跳过空白符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)
:将字符数组source
的size
个字节写入os
,返回os
-
is.ignore(size, delim)
:读取并忽略最多size
个字符,包括delim
。
3. 流随机访问
标准库提供了一对函数,来定位seek
到流中给定的位置,以及告诉tell
我们当前位置。虽然标准库为所有流类型都定义了seek
和tell
函数,但是他们是否会做又有意义的事情依赖于流绑定到哪个设备。在大多数系统中,绑定到cin
,cout
,cerr
和clog
的流不支持随机访问。对于这些流我们可以调用seek
和tell
函数,但在运行时会出错,将流置于一个无效状态。
由于
istream
和ostream
通常不支持随机访问,因此本节内容只适用于fstream
和sstream
。
3.1 seek和tell函数
-
tellg()
和tellp()
:返回一个输入流中(tellg
)或输出流中(tellp
)标记的当前位置 -
seekg(pos)
和seekp(pos)
:在一个输入流或输出流中将标记重定位到给定的绝对地址,pos
通常是前一个tellg
或tellp
返回的值 -
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 访问标记
函数tellg
和tellp
返回一个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;
}