0%

关联容器支持普通容器操作,不支持,

  • 顺序容器位置相关的操作,push_back等;
  • 构造函数或插入操作接受一个元素值和一个数量值得操作。

定义

定义一个map时必须指定关键字类型和值类型,set只需关键字类型

初始化

可以用下面的方式来初始化,

  • 同类型容器的copy;
  • 指定值范围(begin和end);
  • 列表初始化。

有序关联容器关键字类型的要求

有序关联容器关键字类型必须定义元素比较的方法。默认情况下,使用<进行比较。

1
2
3
4
// list的iterator并无<
map<list<int>::iterator, int> ml; // 声明不会报错
list<int> li;
ml[li.begin()] = 0; // 报错

对于有序容器,有序容器的关键字必须是严格弱序的,可看做“小于等于”。

自定义比较操作

用于组织一个容器中元素的操作的类型也是容器类型的一部分,如果需要自定义操作,则在定义容器的时候就指明。

1
2
boo compare(...) { ... }
multiset<Sales_sata, decltype(compare) *> bookstore(compare);

创建对象时,提供的操作类型(函数指针)必须与尖括号中的类型吻合。规则与函数的const形参和实参的规则一致,忽略top const。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto comp = [](int a, int b) { return false; };
multiset<int, bool (*)(const int, const int)> m(comp);

auto comp1 = [](const int a, const int b) { return false; };
multiset<int, bool (*)(int, int)> m1(comp1);

auto comp2 = [](int* const a, int* const b) { return false; };
multiset<int, bool (*)(int*, int*)> m2(comp2);

auto comp3 = [](int* a, int* b) { return false; };
multiset<int, bool (*)(int* const, int* const)> m3(comp3);

auto comp4 = [](int* a, int* b) { return false; };
multiset<int, bool (*)(const int*, const int*)> m4(comp4); // 错误

pair

模板,接受两个类型名,pair的数据成员将有对应的类型,两个类型不要求一样。

创建对象时,pair的默认构造函数对数据成员进行值初始化(vector也可以)。

1
pair<string, size_t> word_count;

若函数返回pair,可对返回值进行列表初始化,不必显式构造返回值。

关联容器的操作

map中,每个元素就是一个pair对象,由于关键字不可变,因此pair的关键字部分是const。set的关键字也是const

1
2
3
map<string, int>::value_type v1; // pair<const string, int>
map<string, int>::key_type v2; // string
map<string, int>::map_type v3; // int

关联容器的迭代器

对关联容器迭代器解引用,可得到容器的value_type的引用。

对于set,虽然set定义了iteratorconst_iterator,但是都不能改变set中的元素。

当迭代器遍历一个map,multimap,set或multiset时,按关键字升序遍历。

关联容器和算法

由于关键字是const,因此不能用于修改或重排容器的算法(都需要向元素写入值)。只可用于读取元素的算法。

添加元素

insertemplace可以对关联容器进行插入,

1
2
3
4
5
6
7
8
9
10
11
c.insert(v)
c.emplace(args)
// map和set返回pair<iterator, bool>,iterator指向有此关键字的元素,bool说明是否元素是否已经存在,即是否插入成功。multimap和multiset总是进插入,只返回bool。

c.insert(b, e)
c.insert(li)
// 返回void

c.insert(p, v)
c.emplace(p, args)
// p指明了从哪里开始新元素的存储位置

删除元素

关联容器有三个版本的erase,

1
2
3
4
5
6
7
8
c.erase(k)
// 删除所有key为k的元素,返回size_type,表明删除的数量

c.erase(p)
// 返回被删除元素后的迭代器

c.erase(b, e)
// 返回e

map的下标

由于set并无关联值,下标操作对set无意义,故set不支持。multimap和multiset可能存在多个与某个key关联的值,故也不支持。

下标操作返回mapped_type,是左值。如果关键字不在map中,下标操作会,

  1. 创建一个元素并插入,关联值将进行值初始化
  2. 提取元素并赋值。

注意,

  1. 与vector和string不同,map的下标操作和解引用返回的类型(mapped_typevalue_type)不一样;
  2. 如果元素不存在,at并不会创建,而是抛出out_of_range
  3. 下标操作可能会改变map,对const的map无法使用;
  4. 对于const的map,只要at不修改元素,就可以用。
    1
    2
    3
    4
    const map<string, int> m{{"hello", 1}};
    cout << m.at("hello") << endl;
    cout << m["hello"] << endl; // 错误,即使是普通访问
    m.at("hello") = 1; // 错误

无序关联容器

使用hash function和==来组织元素,用hash<key_type>类型的对象生成每个元素的hash值,有序关联容器的操作可以用于无序容器。

标准库为内置类型(包括指针类型)和部分标准库类型(包括string和智能指针)类型定义了hash。但不能直接把自定义类型作为key来定义无序容器,可以

  1. 提供自己的hash模板版本;
  2. 定义hash function和==运算符。
1
2
3
4
5
6
7
8
9
size_t hasher(const Sales_data& sd) {
return hash<string>() (sd.isbn());
}

bool eqOp(const Sales_data& lhs, const Sales_data&rhs) {
return lhs.isbn() == rhs.isbn();
}

unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*> set(42, hasher, eqOp);

首先介绍顺序容器操作基本相同的部分,然后分别是每种容器要注意的地方。

定义和初始化

  • C c 默认构造函数
  • C c1(c2)C c1 = c2 c1初始化为c2的copy。c1和c2必须是相同的容器类型,且保存相同类型的元素。
  • C c{a, b, c, ...}C c = {a, b, c, ...} 列表初始化
  • C c(b, e)

除array外,还有以下两种, * C seq(n) * C seq(n, t)

C c1(c2)C c(b, e)

C c1(c2)要求两个容器必须是相同的类型,且元素类型也是相同的;但C c(b, e)就只需要元素类型可以转换为要初始化的容器的元素类型即可。

赋值和swap

所有容器都支持赋值=,赋值后,左边容器的元素为右边容器元素的copy,且大小与右边容器相同。

swap

  • swap会交换两个容器的元素,两个容器必须有相同的类型;
  • 通常来说swap只是交换了容器内部的数据结构,但也有例外,对array进行操作时,会真正交换它们的元素;
  • swap完成以后,除string外,指向容器的迭代器、引用和指针都不会失效。

assign

  • seq.assign(b, e)
  • seq.assign(il)
  • seq.assign(n, t)

要注意的有,

  • 参数非常像初始化的;
  • assign不适用于关联容器的array;
  • 可以从一个不相同但相容的类型assign。

大小

除了forward_list,每个容器都支持size()empty()max_size();forward_list只支持empty()max_size()

运算符

每种容器都支持==!=

除了无序关联容器,所有容器都支持>>=<<=

操作

push...和insert

push_backinsertpush_front放入容器的是元素的copy。

emplace...

emplace_frontemplaceemplace_back分别对应push_backinsertpush_front。不支持push...或insert的容器,也不支持相应的emplace

push_backpush_frontinsert放入元素的copy不同,emplace会见参数传递给元素类型的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class C2 {
public:
C2() = default;
explicit C2(int a) : a_(a) {}
C2(int a, int b) : a_(a), b_(b) {}

private:
int a_ = 0;
int b_ = 0;
};

vector<C2> cs2;
// 由于是explicit,不存在从int到C2的隐式转换
// cs2.push_back(1);
// cs2.push_back(); // 错误
// cs2.push_back(1, 2); // 错误

cs2.push_back(C2());
cs2.push_back(C2(1));
cs2.push_back(C2(1, 2));

// 调用了C2的构造函数,对应以三个push_back
cs2.emplace_back();
cs2.emplace_back(1);
cs2.emplace_back(1, 2);

emplace在容器中直接构造元素,由于参数是传递给元素的构造函数,因此实参的类型必须和构造函数匹配。

访问元素

可用以下方法访问顺序容器的元素:

  • c.back()
  • c.front()
  • c[n]
  • c.at(n)

下标操作和at实际上就是进行随机访问,因而只能用于支持随机访问的顺序容器(string、vector、deque和array)。两个随机访问中,只有at能够保证安全的随机访问,下标越界时,会抛出out_of_range异常。

删除元素

  • c.pop_back()
  • c.pop_front()
  • c.erase(p)
  • c.erase(b, e)
  • c.clear()

pop_backpop_front返回voiderase返回一个迭代器,位置为最后一个被删除元素的下一个。

如果be相等(即使都为c.end()),那么不会删除任何元素。

容器适配器

适配器是一种机制,使得某种事物的行为看起来像另外一个事物。容器、迭代器和函数都有适配器。

每个适配器有两个构造函数,

  • A a
  • A a(c),拷贝容器c来初始化a

顺序容器适配器有,

  • stack,默认情况下基于queue实现;
  • queue,默认情况下基于queue实现;
  • priority_queue,默认情况下基于vector实现;

一般来说,用于构造适配器的容器是有限制的,

  • 能添加和删除元素;
  • 访问尾元素;

具体到每个适配器,

  • stack push_backpop_backback
  • queue push_backpush_frontbackfront
  • priority_queue frontpush_backpop_back,随机访问

vector

capacity和size

vector支持快速随机访问,元素是连续存储的。这意味着添加元素时,需要移动已有的元素,以保证连续的存储。如果没有空间容纳新元素,则需要分配新的内存,并把已有元素从旧的位置移动到新的位置。为了减少内存的分配和释放的代价,分配策略一般为实在是没法存时,才获取新的内存。只要没有操作使得vector的capacity不够,就不会重新分配内存。

以下几个操作是管理容器大小的,

  • c.shrink_to_fit() 请求把capacity减少为size。这里仅仅是一个请求,shrink_to_fit不保证退还内存。
  • c.capacity() 在不重新分配内存的情况下,最多能存储的元素数目。
  • c.reserve(n) 分配至少能容纳n个元素的内存。n如果小于等于当前capacity,那么什么都不发生。

要注意的是,reserve和resize不同,reserve改变(至少是变大)的是capacity,size并未变化;而resize改变了size的同时,capacity(如果n大于当前的capacity)也有可能改变。

1
2
3
4
5
6
void t23() {
vector<int> v1(10, 10); // size: 10 capacity: 10
vector<int> v2(10, 10); // size: 10 capacity: 10
v1.resize(15); // size: 15 capacity: 20
v2.reserve(15); // size: 10 capacity: 15
}

迭代器

添加元素的情况下, * 如果添加前capacit = size,那么会导致内存重新分配,从而vector相关的迭代器、引用或指针都会失效; 如果不发生内存分配,有可能会发生元素移动(插入中间位置),插入位置之后的迭代器、引用或指针都会失效。

对于删除元素,不会发生内存重新分配,被删除元素前的迭代器、引用或指针都还有效。

deque

迭代器

添加元素的情况下, 如果插入到首尾之外的位置,都会导致迭代器、引用或指针失效;

删除元素的情况下, * 如果在首尾之外的位置删除,都会导致迭代器、引用或指针失效; * 如果删除了尾元素尾后迭代器也会失效,但其他的迭代器、引用或指针不受影响。

string

操作

string也支持上述提及的大部分操作,但同样有例外,且某些操作有特殊局限,

  • swap会导致相关的迭代器、引用或指针失效;
  • 与vector一样,front相关的操作不支持;
    • 不支持push_frontemplace_front
    • 不支持pop_front;
  • inserteraseassignreplace重载函数,p323和p324;
    • 额外的inserteraseassign
    • append在末尾插入;
    • replace,等价于erase+insert
  • 搜索,p325和p326;
    • 返回值均为string::size_type(),是一个unsigned类型,如果找不到,这返回string::npos
    • s.find(args),返回第一个匹配args的下标;
    • s.rfind(args),返回最后一个匹配args的下标;
    • s.find_first_of(args),返回args中任何一个字符首次出现在args中的下标;
    • s.find_last_of(args)
    • s.find_first_not_of(args),返回首个不在args中字符的下标;
    • s.find_last_not_of()
  • compare同样有多个重载,p327;
  • 数值转换,p328;
    • to_string(val)
    • sto...
      • string中的第一个非空白字符必须是数值中可能出现的字符,即+或-,或数字,数字可以是0x或0X开头表示的十六进制数;
      • 如果是转换为浮点数的,开头可以为.,且包含e或E表示指数;
      • 如果是转换为整型的,根据不同的基数,可以有字母;
      • 如果不能转换,则抛出invallid_argument
      • 如果得到的数值无法用任何类型表示,则抛出out_of_range

泛型算法

  • 一般泛型算法不直接操作容器,而是运行于迭代器之上,由迭代器来进行操作;
  • 迭代器令算法不依赖于容器,但是某些算法使用的操作需要元素的类型;
  • 算法不会直接改变底层容器的大小;
  • 算法可能改变元素的值或移动元素,但不会直接添加或删除元素。

按使用元素的方式分类

只读算法

accumulate

accumulate的第三个参数类型决定了函数中使用那个加法运算符以及返回值的类型。

  • 如果这个类型不支持+运算符,则会发生编译错误;
  • 如果元素类型与这个类型不匹配,且能够类型转换,无论哪个类型宽窄,只会发生元素类型->第三个参数类型

equal

按元素比较,第二个序列至少与第一个序列一样长

如果两个序列类型分别为,vector<const char*>list<const char*>,则比较的是地址,不是字符串的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<const char *> vcc{"ab", "bc", "cd"};
list<const char *> lcc{"ab", "bc", "cd"};

// 由于编译器优化,vcc和lcc实际上共享了字面值常量,故下面的地址相同。
cout << equal(vcc.begin(), vcc.end(), lcc.begin()) << endl;
// 1
cout << static_cast<void *>(const_cast<char *>(vcc.front())) << endl;
// 0x40c951
cout << static_cast<void *>(const_cast<char *>(lcc.front())) << endl;
// 0x40c951

const char a[3][3] = {"ab", "bc", "cd"};
const char b[3][3] = {"ab", "bc", "cd"};
vector<const char *> vcc1{begin(a), end(a)};
list<const char *> lcc1{begin(b), end(b)};

cout << equal(vcc1.begin(), vcc1.end(), lcc1.begin()) << endl;
// 0
cout << static_cast<void *>(const_cast<char *>(vcc1.front())) << endl;
// 0x40c908
cout << static_cast<void *>(const_cast<char *>(lcc1.front())) << endl;
// 0x40c911

写容器元素的算法

这类算法并不检查写操作。由于算法不会改变底层容器的大小,因此必须保证目的位置迭代器开始序列足够容纳要写入的元素。

copy返回的是目的位置迭代器递增后的值。

重排容器元素的算法

unique“移除”了相邻重复元素,把不重复的元素移动到了序列前面,并非删除。返回不重复元素范围末尾的下一个迭代器。

定制操作

谓词

某些算法需要进行元素间的比较,如果需要使用与定义行为不同的比较,或者元素类型未定义<运算符,则需要通过提供谓词,重载算法的默认行为。

谓词是可调用的表达式,返回结果是一个能用着条件的值,分为,

  • 一元谓词;
  • 二元谓词。

序列中的元素作为实参传入谓词,因此需要满足函数匹配规则。

lambda表达式

是可调用对象,callable object有,

  • 函数
  • 函数指针
  • 重载了函数调用运算符的类
  • lambda表达式
  • bind创建的对象

使用lambda,要注意的是,

  • lambda必须使用尾置返回,可以忽略参数列表和返回类型(忽略时,从代码中推断);

    1
    auto f = []{ return 1; };

  • lambda不能有默认参数;

  • 对于lambda所在函数体的非static局部变量,只能使用在捕获列表中捕获后,才能使用;

  • 对于局部static变量和lambda所在函数体之外声明的名字,可以直接使用

如果lambda捕获列表为空,那么lambda可以转换为函数指针。

1
2
3
4
auto f = [](int a, int b) { return a + b; };
// f自动转换为pointer
int (*pf)(int, int) = f;
cout << pf(1, 2) << endl;

lambda捕获和返回

定义lambda时,编译器生成一个与lambda对应的未命名类类型。用auto定义一个用lambda初始化的变量时,就定义了一个相应的未命名类类型的对象。lambda的数据成员在lambda对象创建时被初始化。

类似函数的参数传递,捕获方式有,

每种方式都可以进行,

  • 值捕获

    被捕或的变量的值是在lambda创建时拷贝,而不是像函数调用时才拷贝。能使用值捕获的前提是变量可拷贝

  • 引用捕获

    &表示以引用的方式捕获。 引用捕获与返回引用有相同的问题和限制,必须保证引用的对象在lambda执行时存在,且在执行时是所期望的(可能在被捕或后和执行前,引用的对象的值改变了)。如果函数返回lambda,则不能包含局部非static变量的捕获。

按是否显示列出希望使用的变量,可分为,

  • 显式捕获

    [v1, ...][&v1, ...]

  • 隐式捕获

    • 值捕获,[&]
    • 引用捕获,[=]
  • 混合使用显式和隐式捕获

    当混合使用显式和隐式捕获时,显示捕获的变量必须使用与隐式捕获不同的方式。

    • [&, identifier_list]

      任何隐式捕获的变量都采用引用捕获identifier_list采用值捕获的方式,且identifier_list中的名字不能使用=

    • [=, identifier_list]

      任何隐式捕获的变量都采用值捕获identifier_list采用引用捕获的方式,且identifier_list中的名字必须使用&

      1
      2
      3
      4
      5
      6
      int a = 1;
      int b = 2;
      int c = 3;

      auto l = [=, &c](){};
      // auto l = [=, c](){}; 错误

可变lambda

默认情况下,值捕获的变量,lambda不会改变被捕获变量的值(并非改变原始变量),如果需要改变,加入mutable

1
2
3
4
5
6
7
8
int a = 1;

auto l2 = [a]() mutable {
++a; // 如果无mutable,则错误
return a;
};
cout << a << endl; // 1, 并非改变原始变量
cout << l2() << endl; // 2

引用捕获无此限制,能够更改依赖于被引用变量是否为const

lambda的返回值

默认情况下,如果一个lambda包含return之外的任何语句,编译器假定此lambda返回void。

bind

由于find_if接受的是一个一元谓词,因此含有两个形参的函数是不可用的。

bind生成一个新的可调用对象,可看做一个通用的函数适配器。

1
auto newCallable = bind(callable, arg_list)

arg_list可包含_n,表示newCallable的参数,参数的类型就是callable中_n处的类型。其中n代表了占位符_nnewCallable的位置,调用newCallable时,_n处的参数最终会传递到callable中_n相应的位置。

_n定义在placeholders的namespace中,使用时需,

1
2
3
4
using std::placeholders::_1;
...
// 或
using namespace std::placeholders;

默认情况下,不是占位符的参数是被拷贝到bind返回的可调用对象中的。如果想传递引用,就必须显式的指明,使用refref返回的对象包含给定的引用,是可拷贝的。类似的还有cref,返回const引用。

额外的迭代器

除每个容器的定义的迭代器外,还有以下几种,

插入迭代器

是一种迭代器适配器,对插入迭代器赋值时,该迭代器调用容器的操作来进行插入。*和前置后置++会直接返回迭代器,并不会修改

插入迭代器有以下几种,

  • back_inserter

    创建使用push_back的迭代器,始终在尾部插入。

  • front_inserter

    创建使用push_front的迭代器,始终在首部插入。

  • inserter

    创建使用insert的迭代器,始终在迭代器it指定位置前插入。插入后it还是指向一开始指定的位置。

流迭代器

istream_iterator

读取输入流,必须指定迭代器将要读写的对象类型,且要读取的类型必须支持>>(由于是调用>>来读取)。

1
2
3
4
5
6
7
8
istream_iterator<int> int_it(cin);
istream_iterator<int> int_eof; // 默认初始化,尾后迭代器

while (int_it != eof) {
...
}

vector<int> v(int_it, eof);

当绑定到流时,标准库并不保证迭代器立即从流中读取数据,而是保证在首次解引用前,完成了数据的读取

ostream_iterator

类似istream_iterator,使用<<写入输出流(类型必须支持<<)。必须绑定到一个输入流,可指定一个每次写入时都输出的字符。*和前置后置++会直接返回迭代器,并不会修改

1
2
3
4
ostream_iterator<int> out(cout);
// ostream_iterator<int> out(cout, " "); 字面值常量或指向C风格的字符串

out = 5; // 类型必须与out定义的类型兼容

反向迭代器

也是一种迭代器适配器。只能从一个支持++和--的迭代器来定义反向迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
    cbegin()                                     cend()
| |
[], [], [], [], [], [], [], [], [], [], [], []
| |
crend() crbegin()

cbegin() rit.base() cend()
| | |
[], [], [], [], [], [], [], [], [], [], [], []
| |
rit crbegin()

[crbegin(), rit)和[rit.base(), cend())指向相同的范围。

可以对反向迭代器调用base()来得到普通的迭代器。base()得到的是相邻的位置。

移动迭代器

一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。对移动迭代器解引用生成的是一个右值

1
auto newe = uninitialized_copy(make_move_iterator(elements), make_move_iterator(cap), newb);

通过调用make_move_iterator可将一个普通迭代器转换为一个移动迭代器。上面的代码中,传递给uninitialized_copy的是一个移动迭代器,解引用后得到的是右值,因此uninitialized_copy将使用移动构造函数来构造元素。

泛型算法的结构

迭代器分类

按算法所要求的迭代器操作,可将迭代器分为下面几类,除了输出迭代器外,高层类别的迭代器支持低层类别迭代器的所有操作。C++标准指明了泛型和数值算法的每个迭代器参数的最小类别

  1. 输入迭代器

    只读,不写;单遍扫描,只能递增

    支持,

    • ==!=
    • 前置后置++,可能导致所有其他指向流的迭代器失效,不能保证输入迭代器的状态可以保存下来,即只能单遍扫描
    • *,只能在赋值运算的右侧
    • ->

    例子:findaccumulate

  2. 输出迭代器

    只写,不读;单遍扫描,只能递增

    支持,

    • 前置后置++
    • *,只能在赋值运算的左侧

    例子:用作目的位置的迭代器,如copy

  3. 前向迭代器

    可读写;多遍扫描,只能递增

    支持,

    • 所有输入和输出迭代器的操作

    例子:replaceforward_list的迭代器。

  4. 双向迭代器

    可读写;多遍扫描,可递增递减

    支持,

    • 所有前向迭代器的操作
    • 前置后置--

    例子:reverse,除了forward_list的迭代器,其他标准库容器的迭代器都符合双向迭代器。

  5. 随机访问迭代器

    可读写;多遍扫描,支持全部迭代器的运算

    支持,

    • <<=>>=
    • 和整数的+-+=-=
    • 两个迭代器的-
    • iter[n],等价于*(iter[n])

    例子:sortarraydequestringvector的迭代器。

算法形参模式

  • alg(beg, end, other, args);
  • alg(beg, end, dest, other, args);
  • alg(beg, end, beg2, other, args);
  • alg(beg, end, beg2, end2, other, args);

向输出迭代器dest写入数据的算法都假设,目标位置有足够的空间。 接受单独beg2的算法假定从beg2开始的序列与[beg, end)所表示的范围至少一样大。

列表初始化(list initialization)

注:对于内置类型的变量,如果使用列表初始化且初始化存在丢失信息的风险,则编译器将报错。

1
2
double d = 3.14;
int i{d}; // 错误

默认初始化(default initialization)

如果定义变量时没有指定初值,则变量被默认初始化,具体值由变量类型决定。对象被默认初始化时自动执行默认构造函数。

何时发生

  • 当在块作用域中不使用任何初始值定义一个非静态变量或者数组时;
    1
    2
    3
    4
    int main() {
    int a;
    double b[10];
    }
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时;
    1
    2
    3
    4
    5
    6
    7
    8
    class A {
    public:
    int a;
    };
    class B {
    public:
    A a;
    };
  • 当类类型的成员没有在构造函数初始值列表中显示地初始化时。
    1
    2
    3
    4
    5
    6
    7
    class A {
    public:
    A(int a) : a_(a) {}
    private:
    int a_;
    int b_;
    };

值初始化

对于内置类型,则元素初始值自动设为0;如果是类类型,则由类来默认初始化。

何时发生

  • 在数组初始化时,提供的初始值的数量少于数组的大小;
    1
    int arr[5] = {1, 2, 3}; // a[] = {1, 2, 3, 0, 0}
  • 不使用初始值定义一个局部静态变量时;
    1
    2
    3
    4
    int func() {
    static int s; // s值为0
    return 0;
    }
  • 使用T()来显示地请求值初始化时;
  • 使用列表初始化来初始化聚合类,列表中元素数量少于类的成员数量时,后续成员被值初始化。
    1
    2
    3
    4
    5
    6
    7
    8
    class Aggre {
    public:
    int i;
    double d;
    char c;
    };

    Aggre aggre = {1} // d和c都是0

References

  1. C++值初始化,默认初始化,以及其他初始化类型的区别

iostream

条件状态

strm::iostate机器相关的位类型,用来标识流错误状态,有4个值,strm::badbitstrm::failbitstrm::eofbitstrm::goodbit。可用ios::bad()ios::fail()ios::eof()ios::good()来得知各个flag是否置位;用ios::rdstate()来获取流的当前状态。

1
2
3
4
badbit: 0001
failbit: 0100
eofbit: 0010
goodbit: 0000

不同的错误会导致不同的flag被置位,

  • eof -> eofbit,failbit
  • fail -> failbit
  • bad -> badbit,且fail()返回true

ios::clear()ios::setstate()可以对flag进行置位,两者区别在于,ios::clear()会先将原先的所有flag先复位,而后者不会,

1
2
3
void ios::setstate (iostate state) {
clear(rdstate() | state);
}

输出缓冲

每个输出流都管理一个缓冲区。

缓冲刷新

即数据真正写到输出设备或文件,会有以下原因,

  • 程序正常结束,main函数return;
  • 缓冲区满,需要刷新才能写入后续数据;
  • 使用操纵符;
    • endl:换行并刷新缓冲区;
    • flush:刷新缓冲区,不附加任何额外字符;
    • ends:附加一个空字符,刷新缓冲区。
  • unitbufnounitbuf; 使用unitbuf后,接下来的每次写操作都进行一次flush;使用nounitbuf返回正常的缓冲方式。
  • 关联输入流和输出流。 关联一个输入流到输出流时,从输入流读取数据,会先刷新关联的输出流,cincout是被关联的。使用tie进行关联。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tie的两个重载版本
// 两个版本都返回已经tied ostream,如果没有,则返回nullptr
ostream* os = cin.tie();
(*os) << "tie version 1" << endl;

ostream* old_os = cin.tie(nullptr);
ostream* null_os = cin.tie();
(*old_os) << "tie version 2" << endl;
cout << boolalpha << (old_os == &cout) << "\n" << (null_os == nullptr) << "\n";
// true true

char ch;
// 此时上面的两个true并不会输出
cin >> ch;
cout << ch << endl;

fstream

1
2
3
4
op1=>operation: iostream
op2=>operation: fstream

op2(right)->op1

继承自iostream

  • 接受一个iostream类型引用或指针参数的函数,可用一个对应的fstream类型来调用;
  • 支持iostream的所有操作。

fstream也有自己特有的行为和操作,

  • fstream::fstream(s):创建对象并打开文件s
  • fstream::open():打开文件,并与fstream对象关联;
  • fstream::close():关闭与fstream对象关联的文件;
  • fstream::is_open():与fstream对象关联的文件是否成功打开,且未关闭;
  • 对于已打开的fstream对象,再次open()会失败;
  • open()失败->failbit;
    1
    2
    3
    4
    ifstream ifs(...);
    if (ifs) {
    ...
    }
  • fstream对象被销毁,close()会被自动调用。

文件模式

对于ofstream

  • 未指定打开模式时,以out打开;
  • 通常out意味着同时使用trunc
  • app不能与trunc同时设置;
  • appate
    • app,所有的输出操作都在文件末尾,不能seek around;
    • ate,初始位置在文件末尾,可以seed around。

sstream

1
2
3
4
op1=>operation: iostream
op2=>operation: sstream

op2(right)->op1

类似fstreamsstream也包含特有的操作,

  • sstream::sstream(s):创建对象,并保存s的copy;
  • sstream::str():返回保存的string的copy;
  • sstream::str(s):copys到对象中。

缓冲区(bufer)

一个存储区域,用于保存数据。IO设施通常将输入(或输出)数据保存在一个缓冲区中,读写缓冲区的动作与程序中的动作是无关的。我们可以显式地刷新输出缓冲,以便强制将缓冲区中的数据写入输出设备。默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。

cerr

一个ostream对象,关联到标准错误,通常写入到与标准输出相同的设备。默认情况下,写到cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容。

类(class)

一种定义自己的数据结构及其相关操作的机制。

类类型(class type)

类定义的类型。类名即为类型名。

clog

一个ostream对象,关联到标准错误。默认情况下,写到clog的数据是被缓冲的。clog通常用于报告称程序的执行信息,存入一个日志文件中。

未初始化的变量(uninitialized variable)

未赋予初值的变量。类类型的变量如果未指定初值,则按类定义指定的方式进行初始化。定义在函数内部的内置类型变量默认是不初始化的,除非有显式的初始化语句。

变量(variable)

具名对象。

()运算符(()opeartor)

调用运算符。跟随在函数名字之后的一对括号“()”,起到调用函数的效果,传递给函数的实参放置在括号内。

.运算符(.operator)

点运算符。左侧运算符必须是一个类类型对象,右侧运算对象必须是此对象的一个成员的名字。运算结果即为该对象的这个成员。

::运算符(::operator)

作用域运算符。其用处之一是访问命名空间中的名字。

<<和>>运算符(<<operator, >>operator)

输出和输入运算符。它们的计算结果均是其左侧的运算对象。