C++ Primer 学习笔记 第十七章 标准库特殊设施

C++ 11中引入了tuple类型,它类似pair,但它可以有任意多个成员。每个确定的tuple类型的成员数是固定的。我们可以在想将一些数据组合成单一对象,但又不想定义一个新的数据结构时使用。

tuple及其伴随类和函数都定义在头文件tuple中。tuple支持的操作:
在这里插入图片描述
与pair相同,tuple的默认构造函数成员也是将元素值初始化的。

定义tuple时,需要指出所有成员类型。

tuple只能直接初始化,它的构造函数是explicit的:

tuple<int> t = {1};    // 错误,但我测试时,也可以这样,应该是编译器优化或新版本特性
tuple<int> t{1};    // 正确

make_tuple:

auto item = make_tuple('a', 1, "sss");    // 自动类型推断,item类型为tuple<char, int, const char *>

获取上例tuple元素:

char c = get<0>(item);    // 返回item下标为0的成员,get为函数模板 

获取tuple成员的数量和类型:

size_t sz = tuple_size<decltype(item)>::value;    // sz为3
tuple_element<1, decltype(item)>::type cnt = get<1>(item);    // 获取item下标为1的元素的类型,cnt的类型为int,值为item的下标为1的元素的值

tuple_size的成员value是类型为public constexpr static size_t的数据成员。

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 < threeD);    // 错误,成员数量不同
tuple<size_t, size_t> origin(0, 0);
b = (origin < twoD);    // 正确,对每对元素使用<都是合法的

tuple定义了==和<,因此可以传递给算法,在无序容器中也可以将tuple作为关键字类型。当我测试时,直接使用tuple作为关键字类型会报错:

unordered_map<tuple<int, string>, int> um1;    // 错误
unordered_map<string, int> um2;    // 正确

只有给tuple类型自定义一个哈希函数传给无序容器才可以通过编译:

size_t hasher(const tuple<int, string>& tp) {
    return hash<int>()(get<0>(tp)) ^ hash<string>()(get<1>(tp));
}

int main() {
    unordered_map<tuple<int, string>, int, decltype(hasher) *> um(42, hasher);    // 正确,参数为桶大小和哈希函数指针
}

使用tuple的例子:用于返回多个值。假定一家连锁书店中每一家都用一个vector<Sales_data>保存销售记录,它将每本书的销售记录放在一起,之后再用vector<vector<Sales_data>>,保存每家书店的销售情况,我们想找到出售过某本书的书店,可以创建一个tuple保存这家店的索引和两个表示这家店卖出这本书范围的迭代器:

// 书店索引和迭代器范围
typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator, vector<Sales_data>::const_iterator> matches;

vector<matches> findBook(const vector<vector<Sales_data>> &files, const string &book) {
    vector<matches> ret;    
    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.second));
        }
    }      
    return ret;
}

equal_range功能与关联容器同名成员类似,前两个参数接受一个迭代器范围,第三个接受一个值,默认情况下,使用元素的<运算符,但Sales_data没有<运算符,因此第四个参数传递给它的是compareIsbn函数的指针用以代替<运算符。

打印汇总出售信息:

void reportResult(istream &in, ostream &os, const vector<vector<Salse_data>> &files) {
    string s;
    while (in >> s) {
        auto trans = findBook(files, s);
        if (trans.empty()) {
            cout << s << " is not find in any stores." << endl;
            continue;
        }
        for (const auto &store : trans) {
            os << "store " << get<0>(store) << " sales: " << accumulate(get<1>(store), get<2>(store), Sales_data(s)) << endl;
        }
    }
}

Sales_data定义了加法运算符,可以使用标准库的accumulate函数,求和起点为书名为s、卖出数为0的Sales_data对象。

标准库的bitset类使位运算的使用比内置int更容易,并且能处理超过最长整型大小的位集合。它定义在bitset头文件中。

初始化bitset:
在这里插入图片描述

bitset<32> bitvec(1U);    // 32位,低位为1,其他位为0,大小必须是一个常量表达式

bitvec下标是从0开始的,下标为0的是低位,下标为31的是高位。

用一个整型值初始化bitset时,此值被转换为unsigned long long,并被当做位模式处理。bitset中的二进制位是此模式的一个副本:

// 以下注释中的位表示中,下标0在最右边
bitset<13> bitvec1(0xbeef);    // bitvec1比初始值位数小,高位被丢弃,因此bitvec1为1111011101111
bitset<20> bitvec2(0xbeef);    // bitvec2比初始值位数大,高位填充0,因此bitvec2为00001011111011101111
bitset<128> bitvec3(~0ULL);    // 64位机器中,unsigned long long长为64,因此bitvec3是0*64+1*64

从字符串初始化bitset,字符直接表示位模式:

// 以下注释中的位表示中,下标0在最右边
bitset<32> bitvec4("1100");    // bitvec4为0*28+1100,即超出字符串字符个数的高位被置0

string str("1111111000000011001101");
bitset<32> bitvec5(str, 5, 4);    // 从str的下标5开始,长度为4的str,即1100,放到bitset中翻转一下,即bitvec5为0*28+1100
bitset<32> bitvec6(str, str.size() - 4);    // 从str的大小-4下标开始,一直到str结束,即1101,bitvec6为0*28+1011

bitset操作:
在这里插入图片描述
此外,bitset还支持所有位运算符,作用与用在unsigned上含义相同。

置位意思是将该位值置为true。如将最高位size()-1置位,那么to_string后返回1+n个0。而复位的含义为将该位置为0。

C++ 11中bitset引进了成员函数all,所有位都是true时返回true。

bitset的count和size方法返回size_t类型值。

bitset的下标运算符是被const重载的,非const版本返回的是bitset定义的一个特殊类型的值,我们可以通过此值操纵指定位的值,而const版本根据是否置位返回true或false:

    bitset<4> bs("1011");
    auto bit = bs[0];
    bit = 0;
    cout << bs << endl;    // 输出1010

    const bitset<4> bs("1011");
    auto bit = bs[0];    // bit类型为bool

bitset的to_ulong和to_ullong方法返回的值与bitset对象有相同的位模式,只有bitset的大小小于等于对应类型大小时才能使用这两个操作,如bitset中值不能放入指定类型,会抛出overflow_error异常。

bitset的IO运算符从输入流读取字符,保存到一个临时string对象中,字符数达到bitset的最大大小或遇到不是01的字符时或遇到文件尾或输入错误时,读取结束,之后,用这个string对象初始化bitset。但我测试时,输入运算符会报错,输出运算符可以使用。

使用bitset实现评分程序:

bitset<30> quiz;    // 所有位初始化为0
quiz.set(27);    // 指出第27个学生通过了测验
status = quiz[27];    // 获取第27个学生是否通过测验
quiz.reset(27);    // 第27个学生未通过测验

正则表达式是一种描述字符序列的方法。C++ 11中有正则表达式库(RE库),它定义在头文件regex中。

regex库组件:
在这里插入图片描述
regex类表示一个正则表达式,它支持的一些操作:
在这里插入图片描述
我们在定义或赋值一个regex时,可以指定一些标志位影响regex如何匹配,上图中最后六个即为编写正则表达式时所用的语言,我们必须设置其中之一,默认为ECMAScript,即ECMA-262语法标准,这也是很多Web浏览器所使用的。

函数regex_match和regex_search确定一个给定字符序列与一个给定regex是否匹配。如果整个输入序列与表达式匹配,则regex_match返回true。如输入序列中一个子串与表达式匹配,regex_search返回true。

regex函数的参数:
在这里插入图片描述
上表中,m是一个match对象,match不是一个特定的类型,而是一些存放正则搜索结果的类型的总称,如smatch对象是match对象的一种。

使用正则表达式,查找条件为违反单词的拼写规则“单词中只能出现ie,不能出现ei,除非是前面有一个c:即cei”的单词:

string pattern("[^c]ei");    // 匹配不以c开头的ei
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";    // 包含上述pattern的整个单词
regex r(pattern);    // 用于模式查找的pattern
smatch results;    // 定义一个保存搜索结果的smatch类型对象
string test_str = "receipt freind theif receive";
if (regex_search(test_str, results, r)) {
    cout << results.str() << endl;    // 打印出搜索的结果,找到即停止,即只会输出第一个匹配的串
}

默认,regex使用的正则表达式语言是ECMAScript,其模式[[:alpha:]]匹配任意字母,符号*表示0个或多个,+表示一个或多个,因此pattern含义为零个或多个任意字母+我们的三字母模式+零个或多个字母。

测试字符串test_str中包含拼写错误(与模式匹配)的单词"freind"和"theif"和不含拼写错误的单词,接下来regex_search进行匹配,如找到匹配子串,返回true,之后用results的str方法来打印test_str中与模式匹配的部分,在找到第一个匹配子串后就会停止查找,因此程序输出freind。它会非字母字符为分隔符,因为我们的模式串中只含字母。

匹配文件名时忽略大小写:

// 模式为一个或多个字母或数字字符后接一个'.',再接"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;
    }
}

在模式中,点有特殊含义,通常代表匹配任意字符,于是我们需要使用反斜线使模式中的点去掉特殊含义,但C++语言中的反斜线也有特殊含义,于是需要另一个反斜线去除特殊含义,在正则表达式串中,\\.表示字符.

正则表达式的语法检查是在运行时解析的。如果我们编写的正则表达式有错误,运行时标准库会抛出regex_error类型的异常。regex_error也有一个说明异常情况的what成员,用以描述错误,它还有一个code成员,返回某个错误类型对应的数值编码,这是由具体实现定义的:

try {
    // alnum右括号少了一个
    regex r("[[:alnum:] + \\.(cpp | cxx | cc)$", regex::icase);
} catch (regex_error e) {
    cout << e.what() << "\ncode: " << e.code() << endl;
}

在某一系统上会输出:
在这里插入图片描述
以下为正则表达式全部错误类型,在该系统上,regex_error类的code成员返回下表从上到下,从0开始的错误编号:
在这里插入图片描述

正则表达式对象的创建和赋予新值是很慢的,尽量避免创建很多不必要的regex,如在循环外定义regex而不是循环内。

匹配的输入序列中的的字符可以是char或wchar_t,字符可保存在string(wstring)或char(wchar_t)数组中。regex类保存char编写的正则表达式,wregex类保存wchar_t编写的正则表达式,两者操作完全相同。

smatch保存string类型输入序列的结果;cmatch保存字符数组类型输入序列的结果;wsmatch保存wstring类型输入序列的结果;wcmatch保存宽字符数组类型输入序列的结果,match的类型必须与输入序列类型相匹配。

类型匹配关系:
在这里插入图片描述
上例匹配拼写错误的单词时,只找到了序列中第一个匹配的子串,可以用sregex_iterator来获得string中所有匹配。regex迭代器是一种迭代器适配器,被绑定到一个regex和一个输入序列上。

sregex_iterator操作:
在这里插入图片描述
在这里插入图片描述
上表中的解引用it和对it使用箭头运算符文本说明有问题,解引用返回的是smatch对象,而箭头运算符调用该smatch对象的成员。

将sregex_iterator绑定到string和regex对象时,迭代器自动定位到string中第一个匹配位置,即构造函数会调用一次regex_search。解引用会得到最近一次匹配的结果。递增迭代器会查找下一个匹配的结果。

找出所有不符合拼写规则的单词:

// file是一个string,保存了所有单词
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;    // 解引用获得smatch对象,再访问它的str成员函数
}

sregex_iterator的默认构造函数创建一个类似尾后迭代器的对象。

我们有时还需要获得匹配位置的上下文内容,一个(s|c|ws|wc)match类型对象还有两个名为prefix和suffix的成员函数,它们的返回类型为(s|c|ws|wc)sub_match,功能为保存上下文的相关内容。而sub_match对象也有两个成员,分别为str和length,表示上下文文本和文本长度,使用它:

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,则从前文字符串长度-40开始,取到末尾
	    << "\n\t\t>>> " << it->str() << " <<<\n"
	    << it.suffix().str().sub(0, 40)    // 取后缀的前40个字符
	    << endl;
}

smatch的操作:
在这里插入图片描述
正则表达式的模式中通常包含一个或多个子表达式,它是模式的一部分。语法上用括号表达子表达式:

regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase);

以上模式包含两个括号括起来的子表达式:
1.([[:alnum:]]+)匹配一个或多个字符的序列。
2.(cpp|cxx|cc)匹配文件扩展名。

我们就可以只打印文件名:

if (regex_search(filename, result, r)) {
    cout << results.str(1) << endl;    // 打印第一个子表达式
}

match对象的str成员函数的参数为0时,表示整个模式串的匹配结果。而1表示第一个子表达式。

使用子表达式例子:美国电话号码有10位,包含3位区号和7位本地号,区号通常放在括号里,但这并不是必须的。剩余7位数字可以用短横线’-’、一个点’.‘或一个空格’ ‘分隔成3个和4个数字,但也可以完全不用分隔符。区号和本地号之间也可以用短横线’-’、一个点’.‘或一个空格’ '分隔成3个和4个数字,但也可以完全不用分隔符。我们希望识别出这种格式的数据,通过分两步实现,第一步先找出可能是电话号码的序列,第二步再调用一个函数完成验证。

ECMAScript正则表达式语言的一些语法:
1.\d表示单个数字,而\d{n}表示一个n个数字的序列。
2.方括号中的字符集表示匹配其中的任一个。[-. ]匹配短横线’-’、一个点’.‘或一个空格’ ‘。
3.后接?的组件是可选的。[-. ]?表示可以有短横线’-’、一个点’.‘或一个空格’ ',也可以什么都没有。
4.ECMAScript使用反斜线\表示一个字符本身而非它的特殊含义。

于是电话号码匹配模式串为:

"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})"

1.(\\()?表示可选的左括号。
2.(\\d{3})表示区号。
3.(\\))?表示可选的右括号。
4.([-. ])?表示区号后可选的分隔符。
5.(\\d{3})表示本地号的前3个数字。
6.([-. ]?)表示可选的本地号中间的分隔符。
7.(\\d{4})表示本地号最后四个数字。

但最后可能区号的括号只有一半,这时候需要第二步检查可能的号码是否合法。

string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
regex r(phone);
string s;
while (getline(cin, s)) {
    for (sregex_iterator it(s.begin(), s.end(), r), end_it; it != end_it; ++it) {
        if (valid(*it)) {    // valid为自定义函数,检查通过匹配后的号码是否合法
            cout << "valid: " << it->str() << endl;
        } else {
            cout << "not valid: " << it->str() << endl;
        }
    }
}

子匹配操作:
在这里插入图片描述
我们的电话号码匹配程序的模式有7个子表达式,因此,smatch对象包含8个ssub_match元素,0为整个匹配,1~7表示对应的子表达式。

我们调用valid时,已经知道有一个完整的匹配,但不知道每个可选的子表达式是否是匹配的一部分,如果是,则该子表达式的matched成员为true。

valid函数:

bool valid(const smatch &m) {
    if (m[1].matched) {    // 如果有一个左括号
        return m[3].matched && (m[4].matched == false || m[4].str() == " ");    // 那么必须有右括号,此种情况下区号和本地号之间要么有空格,要么什么都没有
    } else {
        return !m[3].matched && m[4].str() == m[6].str();    // 区号和本地号的连接符必须与本地号中间的连接符相同
    }
}

注:match对象的str(n)是直接获取第n个子表达式的文本,而match[n]获取的是第n个子表达式对应的sub_match对象。

regex_replace函数可以将匹配到的内容替换为其他内容:
在这里插入图片描述
将号码生成为特定形式:

string fmt = "$2.$5.$7";    // 将号码格式改为ddd.ddd.ddd,只获取第2、5、7个子表达式而忽略其他的
string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
regex r(phone);
string number = "(908) 555-1800";
cout << regex_replace(number, r, fmt) << endl;    // 输出908.555.1800

标准库定义了在替换过程中控制匹配或格式的标志,它可以传给函数的mft参数:
在这里插入图片描述
上表这些控制标志的类型为match_flag_type,这些值定义在regex_contents命名空间中,此命名空间在std命名空间中,为使用regex_constants中的名字:

using std::regex_constants::format_no_copy;    // 可以直接使用format_no_copy
using namespace std::regex_constants;    // 可以直接使用所有此命名空间中的名字

默认,regex_replace输出整个输入序列,未与正则表达式匹配的部分会原样输出,匹配的部分按fmt指定的格式输出,我们可以通过使用格式标志改变它:

string fmt2 = "$2.$5.$7 ";    // 在号码最后放置一个空格做分隔符
cout << regex_replace(s, r, fmt2, format_no_copy) << endl;    // 只输出匹配到的部分,若s中有多处匹配到,则全部输出,相邻两个输出不用任何分隔符分隔

在C++ 11前,依赖C库的rand函数来生成随机数,它生成均匀分布的伪随机整数,范围为0~一个系统相关的最大值(至少为32767)之间,它不能生成任意范围的随机数,不能生成随机浮点数,也不能生成非均匀分布的随机数,为解决这些不足所作的一些操作可能会引入非随机性。

C++ 11引入了头文件random,其中包含随机数引擎类和随机数分布类:
在这里插入图片描述

default_random_engine e;    // 随机数引擎
for (size_t i = 0; i < 10; ++i) {
    cout << e() << " ";    // 调用e来生成原始随机数
}

标准库定义了多个随机数引擎,区别在于性能和随机性质量不同,每个编译器都会指定一个随机数引擎为default_random_engine类型,它具有最常用的特性。

随机数引擎操作:
在这里插入图片描述
通常,随机数引擎的输出不能直接使用,因为生成的随机数的值范围与我们需要的通常不符,而正确转换随机数范围是极其困难的,因此称之为原始随机数。

为得到指定范围内的数,使用分布类型对象:

uniform_int_distribution<unsigned> u(0, 9);    // 分布类型对象,可生成均匀分布的unsigned值,生成的值域包含边界值0和9
default_random_engine e;    // 引擎类型对象
for (size_t i = 0; i < 10; ++i) {
    cout << u(e) << " ";    // 将u作为随机数源,每个调用返回指定范围内并服从均匀分布的值
}

随机数发生器指分布对象和引擎的组合。

default_random_engine引擎的输出范围是e.min()和e.max(),而rand函数的输出范围为0~RAND_MAX。

随机数发生器每次运行都会返回相同的数值序列。但我们可以通过将一个函数中的分布和引擎都定义为static的,这样在函数调用之间都会保持住状态,从而第二次调用时的输出与第一次调用的输出是连续的。

随机数发生器每次运行都生成相同随机数列的特性在调试程序时比较有用。

我们可以利用种子来生成不同的随机数列。种子是一个数值,引擎可以每次调用它从序列中一个新位置重新开始生成随机数:

default_random_engine e1;    // 使用默认种子
default_random_engine e2(23545412);    // 使用给定的种子
default_random_engine e3(23545412);     // 会与e2的序列相同,因为使用了相同的种子

最常用的种子是系统函数time,它定义在ctime头文件中,返回从一个特定时刻(1970-01-01 00:00:00)到现在过了多少秒(UNIX时间戳)。time函数接受一个指针参数,指出用于写入时间的数据结构,如指针为空,则只返回UNIX时间戳:

default_random_engine e1(time(0));

time返回以秒计的时间,因此不适用于生成种子的间隔小于一秒的应用。

程序常需要不同类型和分布的随机数列,通过定义不同的分布对象来满足这两方面要求。

如要生成0~1内的随机浮点数,常用但不正确的方法是rand的结果除RAND_MAX,由于随机整数除RAND_MAX的精度通常低于随机浮点数,因此一些浮点数永远不会出现。用新标准库可以实现:

default_random_engine e;
uniform_real_distribution<double> u(0, 1);    // 0~1浮点数的均匀分布
for (size_t i = 0; i < 10; ++i) {
    cout << u(e) << " ";
}

分布类型操作:
在这里插入图片描述
生成浮点值的分布类型默认生成double值,生成整型值的分布默认生成int值。它们通过模板类型参数来实现不同的生成类型,当我们想用默认的模板实参时:

uniform_real_distribution<> u(0, 1);    // 默认生成double值

生成正态分布随机数:

default_random_engine e;
normal_distribution<> n(4, 1.5);    // 默认生成double值,均值为4,标准差为1.5,因此生成的数99%都在0~8之间
vector<unsigned> vals(9);
for (size_t i = 0; i != 200; ++i) {
    unsigned v = lround(n(e));    // 四舍五入,负数作为参数时忽略负号进行四舍五入
    if (v < vals.size()) {
        ++vals[v];
    }
}

for (size_t j = 0; j != vals.size(); ++j) {
    cout << j << ": " << string(vals[j], '*') << endl;
}

只有一个不接受模板参数的分布类,它是bernoulli_distribution,它是一个普通类,返回bool值,返回true和false的概率都是常数0.5。使用它:

default_random_engine e;    // e应保持状态,要在循环外定义
bernoulli_distribution b;
do {
    bool first = b(e);
    cout << (first ? "We go first" : "You get to go first") << endl;
}

调整伯努利分布的true的概率:

bernoulli_distribution b(.55);    // b有55%的概率返回true

iostream对象还维护一个格式状态来控制IO如何格式化,如整型值是几进制、浮点数的精度、一个输出元素的宽度等。标准库定义了一组操纵符修改流的格式状态,一个操纵符是一个函数或对象,会影响流的状态,并能用作有输入或输出运算符的运算对象,并且操纵符和输入输出运算符一样返回它所处理的流对象,因此我们能在一条语句中组合数据和操纵符。

endl是一个操纵符,我们将它“写入”输出流,就像它是一个值一样,但它不是值,而是一个操作,它输出换行符并刷新缓冲区。

大多改变格式状态的操纵符都是设置/复原成对的。操纵符改变状态后,对后续所有IO都生效。

打印布尔值时,会打印出1和0,我们可以用boolalpha操纵符打印出true和false:

cout << true << " " << false << " " << boolalpha << true << " " << false << endl;    // 输出1 0 true false
cout << noboolalpha;    // 将内部状态恢复为默认格式

IO整型值时使用十进制,我们可以用hex、oct和dec改变它:

cout << oct << 20 << endl;    // 输出八进制24
cout << hex << 20 << endl;    // 输出十六进制14
cout << dec << 20 << endl;    // 输出十进制20

如上,但不会影响浮点值。

我们在输出中不知道打印的是几进制,可以使用showbase操纵符输出进制信息:

cout << showbase;
cout << oct << 20 << endl;    // 输出八进制024
cout << hex << 20 << endl;    // 输出十六进制0x14
cout << dec << 20 << endl;    // 输出十进制20
cout << noshowbase;    //恢复cout状态

默认,十六进制以0和小写x开头,我们也可以以0大写X开头,这样做的话,也会输出大写A~F:

cout << uppercase << showbase << hex << 20 << endl << nouppercase << noshowbase << dec;    // 输出0X14,并复原格式状态

默认,浮点数以六位有效数字打印,但位数不够六位时后边不会补0,如3.14后面不会补3个0,而是直接输出3.14;如浮点值没有小数部分,则不打印小数点和小数部分;根据浮点数的值会自动选择打印成十进制或科学计数法(非常大(大于六位数时)或非常小时,为了可读性)形式。

默认,浮点值超出六位有效数字的部分会四舍五入而非截断,四舍五入后最后的0不会输出,如3.333398会输出3.3334,最后的0不会输出。

precision和setprecision操纵符会改变浮点数精度。precision是重载的,一个版本接受int值,将精度设为此值,另一个版本不接受参数,返回当前精度值;setprecision操纵符接受一个参数,用来设置精度。

setprecisin和其他接受参数的操纵符定义在头文件iomanip中。

cout << cout.precision() << endl;    // 输出当前浮点数精度,默认为6
cout.precision(10);    // 将精度设为10
cout.setprecision(12);    // 将精度设为12

sqrt函数定义在头文件cmath中,它是重载的,分别接受float、double、long double值,返回实参的平方根。

在这里插入图片描述
在这里插入图片描述
ends输出的空字符看起来像空格。

执行scientific、fixed、hexfloat后,精度值控制的是小数点后面的数字位数,而默认情况下指定的是有效数字的位数。

cout << 100 * sqrt(2) << endl;    // 输出141.421
cout << scientific << 100 * sqrt(2) << endl;    // 输出1.414214e+002
cout << fixed << 100 * sqrt(2) << endl;    // 输出141.421356
cout << hexfloat << 100 * sqrt(2) << endl;    // 输出0x1.1ad7bcp+7,p代表十进制科学计数法中的e,因为十六进制中e表示14,因此使用p代替
cout << defaultfloat << 100 * sqrt(2) << endl;    // 输出141.421
cout << 1.0 << endl;    // 输出1
cout << showpoint << 1.0 << noshowpoint << endl;    // 输出1.00000
cout << showpoint << 1 << noshowpoint << endl;    // 输出1
int i = -16;
double d = 3.14159;

cout << setw(12) << i << endl;    // 输出      -16,宽度为12,默认右对齐
cout << setw(12) << left << d << endl;    // 输出3.14159       ,宽度为12,左对齐,此处必须再使用setw,setw只对下一次输出有效
cout << internal << setw(12) << i << endl;    // 输出-          16,宽度为12,右对齐,internal覆盖left或right,表示正负号左对齐,数字右对齐
cout << setw(12) << setfill('#') << i << endl;    // 输出-#########16,使用#填充空白

以上输出中setw为12,正负号和点也占位。
在这里插入图片描述
输入运算符会忽略空白符(空格符、制表符、换行符、换纸符、回车符),获取空白符:

char ch;
while (cin >> noskipws >> ch) {
    cout << ch;
}
cin >> skipws;    // 恢复

格式化IO:根据读取或写入的数据类型来格式化它们。

标准库还提供了未格式化IO,它允许我们将一个流当作一个无解释的字符序列来处理。

每次一个字节地处理流:
在这里插入图片描述

// 功能与使用noskipws的代码相同
int ch;
while (cin.get(ch)) {
    cout.put(ch);
}

将获取到的字符重新放回流中:
1.peek方法返回输入流中下一个字符的副本,但不会将它从流中删除。
2.unget方法使得输入流向后移动,从而使最后读取的值又回到流中。
3.putback方法与unget功能相同,它退回流中读取的最后一个值,但它接受一个参数,此参数必须与最后读取的值相同。

一般,标准库最多保证我们可以回退一个值。

以下代码会无限输出第一个输入字符:

    int ch;
    while (cin.get(ch)) {
        cout.put(ch);
        cin.unget();
    }

peek方法和无参的get方法以int类型从输入流返回一个字符,这是因为如果以char返回的话,每一个char值都代表一个真实的字符,这样就没有字符能表示文件尾。返回时,会将char先转换为unsigned char,然后再将结果提升为int,这样负值表示的char也会被映射为正值,之后就可以用负值表示文件尾了。头文件cstdio定义了一个名为EOF的const值,可以用它检测get的返回值是否是文件尾,而不必记忆表示文件尾的实际数值:

int ch;    // 使用int来保存get()的返回值
while ((ch = cin.get()) != EOF) {
    cout.put(ch);
}

如上代码如使用char存放get返回值,若运行在char被实现为unsigned char的机器上时,循环永远也不会停止,因为get返回的文件尾负值在赋予ch时会被转换为无符号值,之后与EOF相比较时,unsigned char会提升为int,最终文件尾值会变为正值,它与EOF表示的负int值不再相等,而且其他字符都是正值,也不会与EOF表示的负值相等。

但如果在char被实现为signed char的机器上,不能确定以上循环的行为,因为EOF值为-1,如输入一个值为-1的字符时,会提前终止循环。
在这里插入图片描述
get方法遇到分隔符后停止读取,但分隔符还在流中。getline方法同样遇到分隔符后停止读取,但分隔符会读取并丢弃。

调用了peek、unget、putback等将单个字符放回流中的方法后,再调用gcount返回0。

i(o)stream类型通常不支持随机访问,以下随机访问内容针对fstream和sstream。因为随机IO本质上是依赖于系统的,随机访问是否会成功取决于流绑定到哪个设备。
在这里插入图片描述
g结尾的用于输入流get,p结尾的用于输出流put。

对于读写都可以的流,它们只有单一的标记表示缓冲区中的当前位置,标准库将p和g版本的读写位置都映射到这个单一的标记,因此,只要我们在读写操作间切换,就必须进行seek操作来重定位标记。

seek的绝对地址类型为pos_type,相对地址类型为off_type,它们是机器相关的,定义在头文件istream和ostream中。

tell函数可以返回一个pos_type的绝对地址值,以便之后再回来。

在文件尾部添加以空格分隔的每一行末尾的累计长度(包括换行符):

int main() {
    fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out);    // 读写方式打开文件并定位到文件尾
    if (!inOut) {
        cerr << "Unable to open file!" << endl;
        return EXIT_FAILURE;    // EXIT_FAILURE为常量,一般为1,表示程序未成功执行
    }
    
    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);
        inOut << cnt;    // 输出累计长度
        if (mark != end_mark) {
            inOut << " ";
        }
        inOut.seekg(mark);
    }
    inOut.seekp(0, fstream::end);
    cout << "\n";    // 文件尾输出一个换行符
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值