目录

C++ Primer Plus 笔记 (14-18章)

C++中的代码重用

包含对象成员的类

使用 explicit 防止单参数构造函数的隐式转换

私有继承

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。

使用多个基类的继承被称为多重继承 (multiple inheritance,MI)。

在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。

通常,应使用包含来建立 has-a 关系; 如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们

各种继承方式如下表:

特征 共有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类接口访问 只能通过基类接口访问 只能通过基类接口访问
能否隐式向上转换 是(但只能在派生类中)

假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。另一种方法是,将函数调用包装在另一个函数调用中,即使用一个 using 声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过 Student 类能够使用 valarray 的方法 min()和 max(),可以在 studenti.h 的公有部分加入如下 using 声明 :

1
2
3
4
5
class Student : private std::string, private std::valarray<double>{
    public:
        using std::valarray<double>::min
        using std::valarray<double>::max;
}

注意,using 声明只使用成员名一没有圆括号、函数特征标和返回类型。

多重继承

问题1:从两个或更多相关基类那里继承同一个类的多个实例

虚基类使得从多个类(它们的基类相同) 派生出的对象只继承一个基类对象。

C++在基类是虚的时,禁止信息通过中间类自动传递给基类。

如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

问题2:从两个不同的基类继承同名方法

对于单继承,如果没有重新定义 Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的。

可以使用作用域解析运算符来澄清编程者的意图,更好的方法是在 派生类 SingingWaiter 中重新定义Show()

类模板

模板类以下面这样的代码开头,用于表明Type是一个通用的类型说明符: template <class Type>template <typename Type> ,后者较新。

可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同的模板声明打头。一个模板stack类形如

 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
26
27
28
29
30
31
32
// stacktp.h --a stack template
#ifndef STACKTP_H_
#define STACKTP_H_

template <class Type>

class Stack{
private:
    enum{MAX=101}; // constant specific to class
    Type items[MAX]; // holds stack items
    int top; // index for top stack item
public:
    Stack();
    bool isempty();
    bool isfull();
    bool push(const Type & item); // add item to stack
    bool pop(Type &item); // pop topintoitem
};

template <class Type>
Stack<Type>::Stack(){
    top=0;
}

template <class Type>
bool Stack<Type>::isempty(){
    return top==0;
}

//....

#endif

使用模板类

形如 Stack<int> kernels;

可以将内置类型或类对象用作类模板 Stack的类型,指针也可以。使用指针栈的方法之一是,让调用程序提供一个指针数组

模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。

类模板可以指定多个泛型,也可以有非类型参数: template <class T,class TT, int n>

类模板还可以包含本身就是模板的参数: template < template <typename T> class CL, typename U,int z>

类模板可以被部分具体化:

友元、异常和其他

友元

类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。

友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。

可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元。但需要前向声明。顺序为使用友元类函数的类的前向声明,友元类定义,使用友元类的类。

内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。

嵌套类

在 C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class)

嵌套类、结构和枚举的作用域特征

声明位置 包含它的类是否可以使用它 从包含它的类派生而来的类是否可以使用它 在外部是否可以使用
私有部分
保护部分
共有部分 是,通过类限定符来使用

异常

如果函数引发了异常,而没有 try 块或没有匹配的处理程序时,将会发生什么情况。在默认情况下下,程序最终将调用 abort()函数

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于 try 块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退

如果有一个异常类继承层次结构,应这样排列 catch 块: 将捕获位于层次结构最下面的异常类的 catch 语句放在最前面,将捕获基类异常的 catch 语句放在最后面。

对于使用 new 导致的内存分配问题,C++的最新处理方式是让 new 引发 bad alloc 异常。头文件 new包含 bad alloc 类的声明,它是从 exception 类公有派生而来的。但在以前,当无法分配请求的内存量时,new 返回一个空指针。

RTTI

RTTI 是运行阶段类型识别(Runtime Type Identification)的简称。这是新添加到 C++中的特性之一

只能将 RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象 的地址赋给基类指针。

C++有3个支持RTTI的元素

  • 如果可能的话,dynamic_cast 运算符将使用一个指向基类的指针来生成一个指向派生类的指针否则,该运算符返回0–空指针。
  • typeid 运算符返回一个指出对象的类型的值
  • typeinfo 结构存储了有关特定类型的信息。

类型转换运算符

更严格地限制允许的类型转换,并添加 4 个类型转换运算符,使转换过程更规范:

  • dynamic_cast; 安全转换,使得能够在类层次结构中对指针或引用进行向上或向下转换(派生类到基类),转换是执行运行时检查,若非法指针返回NULL,引用抛出异常。
  • const_cast; 用于执行只有一种用途的类型转换,去除 const 或 volatile
  • static_cast; 用于基本数据类型之间的转换,以及类层次结构中基类和派生类之间指针或引用的转换。在类层次结构中,它可以执行安全的上行转换(从派生类到基类),也可以执行下行转换(从基类到派生类),但下行不执行运行时检查以确保转换的安全性
  • reinterpret_cast 。用于低级别的类型转换,可以在任意指针(或引用)类型之间进行转换,也可以在指针和整数之间进行转换。它不执行运行时检查,因此使用时需要谨慎

string类和标准模板库

string类

string 类将 string::npos 定义为字符串的最大长度,是类的静态成员,通常为unsigned int 的最大值。另外,表格中使用缩写 NBTS (null-terminated string)

string 类的构造函数

构造函数 描述
string(const char * s) 将string对象初始化为s指向的NBTS
string(size_type n, char c) 创建一个包含n个元素的string对象,其中每个元素都被初始化为字符c
string(const string &str) 将个string对象初始化为string对象str (复制构造函数)
string() 创建一个默认的sting对象,长度为0(默认构造函数)
string(const char* s, size_type n) 将string对象初始化为s 指向的NBTS的前n个字符,即使超过了NBTS结尾
template <class Iter>
string(Iter begin, Iter end)
将string对象初始化为区间[begin,end)内的字符,其中 begin 和end 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括end
string(const string & str, string size_type pos = 0,size_type n = npos) 将一个string对象初始化为对象str 中从位置pos开始到结尾的字符或从位置pos开始的n个字符
string(string && str) noexcept 这是C++11新增的,它将一个string 对象初始化为string对象str,并可能修改str(移动构造函数)
string(initializer_list il) 这是C++11新增的,它将一个string对象初始化为初始化列表il中的字符

getline()有一个可选参数,用于指定使用哪个字符来确定输入的边界:

1
2
cin.getline(info,100,':'); // read up to :, discard :
getline(stuff, ':'); // read up to :, discard :

length()成员来自较早版本的string类,而 size()则是为提供STL兼容性而添加的。

重载的find()方法

方法原型 描述
size_type find(const string & str, size_type pos = 0)const 从字符串的 pos 位置开始,查找子字符串 str。如果找到,则返回该子字符首次出现时其首字符的索引;否则,返回string::npos
size_type find(const char *s, size_type pos = 0)const 从字符串的 pos 位置开始,查找子字符串 s。如果找到,则返回该子符首次出现时其首字符的索引;否则,返回string::npos
size_type find(const char *s, size_type pos = 0size type n) 从字符串的 pos 位置开始,查找 s 的前n个符组成的子符。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回string::npos
size_type find(char ch, size_type pos = 0)const 从字符串的 pos 位置开始,查找字符 ch。如果找到,则返回该符首次出现的位置;否则,返回 string::npos

string 库还提供了相关的方法: rfind( )、 find_first_of( )、find_last_of( )、 find_first_not_of( )和find_last_not_of(),它们的重载函数特征标都与 find()方法相同。rfind()方法查找字符或字符最后一次出现的位置; find_first_of( )方法在字符串中查找参数中任何一个字符首次出现的位置。

c_str()方法返回一个指向C-风格字符串的指针 其他一些常用方法,详见

1
2
3
4
5
6
7
string substr (size_t pos = 0, size_t len = npos) const;
void pop_back();
void push_back (char c);
string& erase (size_t pos = 0, size_t len = npos);
iterator erase (const_iterator p);
iterator erase (const_iterator first, const_iterator last);
void clear() noexcept;

智能指针模板类

将指针作为对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是 auto_ptrunique_ptrshared_ptr 背后的思想。模板 auto_ptr 是 C++98 提供的解决方案,C+11 已将其摒弃,并提供了另外两种解决方案。然而,虽然 auto_ptr 被摒弃,但它已使用了多年;同时,如果您的编译器不支持其他两种解决方案,auto_ptr 将是唯一的选择。

要创建智能指针对象,必须包含头文件 memory,该文件模板定义。然后使用通常的模板语法来实例化所需类型的指针。例如

1
2
3
4
5
6
unique_ptr<double> pdu(new double); // pdu an unique ptr to double
shared_ptr<string> pss(new string); // pss a shared ptr to string

std::string str="123";
std::string *ps = new std::string(str);
std::auto_ptr<std::string> ps (new std::string(str)); // use auto_tpr instead

所有智能指针类都一个explicit 构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象:

1
2
3
4
5
6
shared ptr<double>pd;
double* p_reg=new double;
pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);// allowed (explicit conversion
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);// allowed (explicit conversion)

不能将智能指针用于非堆内存。

为什么放弃auto_ptr

因为两个auto_ptr指向同一个对象,将导致释放错误。解决办法

  • 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。
  • 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。例如,赋值时,计数将加1,而指针过期时,计数将减 1。仅当最后一个指针过期时,才调用 delete。这是shared_ptr采用的策略。

unique_ptr 为什么优于 auto_ptr

避免了将一个智能指针赋给另一个时,留下危险的悬挂指针,更安全。模板 auto_ptr 使用 delete 而不是 delete[],因此只能与new 一起使用而不能与new[]一起使用。但unique_ptr 有使用 new[] 和 delete[] 的版本

1
std::unique ptr< double[]>pda(new double(5)); // will use delete []

如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr, 否则,一般用unique_ptr

标准模板类

基于范围的 for 循环可修改容器的内容,诀窍是指定一个引用参数for (auto & x : books) InflateReview(x);

泛型编程

为何使用迭代器。迭代器使算法独立于使用的容器类型,基于泛型可以使用同一个迭代器完成对容器的操作。

容器使用迭代器的示例

1
2
3
vector<double>::iterator pr;
for (pr = scores.begin();pr != scores.end(); pr++)
cout << *pr<<endl;

使用C++11新增的自动类型推断可进一步简化 auto pr=scores.begin()

要求能够直接跳到容器中的任何一个元素,这叫做随机访问

list<int>类的选代器类型为 list<int>::iterator。STL 实现了一个双向链表,它使用双向迭代器,因此不能使用基于随机访问迭代器的算法,但可以使用基于要求较低的迭代器的算法。 而vector可以随机访问,他使用的就是一段连续的线性内存空间。

以前的 11 个容器类型分别是 deque、list、queue、priority_queue,stack、vector、map、multimap、 set、 multiset 和 bitset (本章不讨论 bitset,它是在比特级处理数据的容器).C++11 新增了 forward_list、unordered_map、 unordered_multimap、 unordered_set 和 unordered_multiset

a[n]和 a.at(n)都返回一个指向容器中第 n个元素(从0开始编号)的引用。它们之间的差别在于,如果n落在容器的有效区间外,则a.at(n)将执行边界检查,并引发 out of range异常。

deque 双端队列,支持随机访问,从开始位置插入、删除的时间是固定的。设计比vector更复杂。

list 不支持数组表示法和随机访问,从容器插入或删除元素之后,链表迭代器指向元素不变。list有一些链表专用的成员函数。

list 成员函数

函数 说明
void merge(list<T,Alloc>& x) 将链表x与调用链表合并。两个链表必须已经排序。合并后的经过排序的链表保存在调用链表中,x为空。这个函数的复杂度为线性时间
void remove(const T & val) 从链表中删除 val 的所有实例。这个函数的复杂度为线性时间
void sort() 使用<运算符对链表进行排序:N个元素的复杂度为 NlogN
void splice(iterator pos, list<T,Alloc>x) 将链表x的内容插入到 pos 的前面,x将为空。这个函数的的复杂度为固定时间
void unique() 将连续的相同元素压缩为单个元素。这个函数的复杂度为线性时间

forward_list 单链表,只需要正向迭代器,不可反转。

priority_queue 优先队列,底层类是vector。最大或最小元素在队首。

1
2
3
4
5
//完整版按大堆创建对象
priority_queue<int,vector<int>, less<int>> q;

//按小堆创建对象(按小堆创建时参数列表不可以省略)
priority_queue<int,vector<int>, greater<int>> q;

set 第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模less<>。

multimap 同一个键可能与多个值相关联。下面的代码演示了如何获取键为718的所有值。

1
2
3
4
auto range = codes.equal_range(718);
cout << "Cities with area code 718: \n";
for (auto it = range.first;it != range.second; ++it)
    cout <<  (*it).second<< endl;

unordered 几个开头的是无序关联容器,底层的差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表。

函数对象

很多STL 算法都使用函数对象——也叫函数符 (functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数 operator()()的类)。

对于所有内置的算术运算符、关系运算符和逻辑运算符,STL 都提供了等价的函数符。

运算符 相应的函数符
+ plus
- minus
* multiplies
/ divides
% modulus
- negate
== equal_to
!= not_equal_to
> greater
< less
>= greater_equal
<= less_equal
&& logical_and
! logical_not

例如使用 plus

1
2
3
4
#include <functional>
..
plus<double> add; // create aplus<double> object
double y = add(2.2, 3.4); // using plus<double>::operator()()

算法

有些算法有两个版本: 就地版本和复制版本。STL 的约定是,复制版本的名称将以_copy 结尾。复制版本将接受一个额外的输出迭代器参数,该参数指定结果的放置位置。

next_permutation() 算法将区间内容转换为下一种排列方式

count() 函数。它将一个区间和一个值作为参数,并返回这个值在区间中出现的次数。

其他库

vector、valarray和array 的区别

vector 模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜索、将数据转移到其他容器中等。而 valarray 类模板是面向数值计算的,不是 STL的一部分。例如,它没有 push_back()和 nsert()方法,但为很多数学运算提供了一个简单、直观的接口。最后 array 是为替代内置数组而设计的,它通过提供更好、更安全的接口,让数组更紧凑,效率更高。Array 表示长度固定的数组,因此不支持 push_back()和 insert(),但提供了多个STL 方法,包括 begin()、end()、rbegin()和rend(),这使得很容易将STL算法用于array对象。

valarray 可以使用 apply() 方法,apply()不修改调用对象,而是返回一个包含结果的新对象。另外提供了 sum(), max(), min()等方法。valarray类确实有一个 resize( )方法,但不能像使用 vector 的 push_back 时那样自动调整大小。没有支持插入、排序、搜索等操作的方法。

模板 initializer_list 是 C++11 新增的。您可使用初始化列表语法将 STL容器初始化为一系列值:

1
2
3
4
std::vector<int> vi{10}; //??
// 这将调用哪个构造函数呢?
std::vector<int> vi(10); // case A: 10 uninitialized elements
std::vector<int> vi([10]); // case B: ielement set to 10

如果类有接受 initializer_list 作为参数的构造函数,则使用语法将调用该构造函数。因此在 这个示例中,对应的是情形 B。 所有 initializer_list 元素的类型都必须相同,但编译器将进行必要的转换

输入、输出和文件

C++输入和输出概述

C++程序把输入和输出看作字节流。

输出时,程序首先填满缓冲区,然后把整块数据传输给硬盘,并清空缓冲区,以备下一批输出使用。这被称为刷新缓冲区 (flushing the buffer)。

键盘输入每次提供一个字符,因此在这种情况下,程序无需缓冲区来帮助匹配不同的数据传输速率。然而,对键盘输入进行缓冲可以让用户在将输入传输给程序之前返回并更正。C++程序通常在用户按下回车键时刷新输入缓冲区。

流、缓冲区和 iostream 文件

  • streambuf 类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法:
  • ios_base 类表示流的一般特征,如是否可读取、是二进制流还是文本流等:
  • ios 类基于ios_base,其中包括了一个指向 streambuf 对象的指针成员;
  • ostream 类是从 ios 类派生而来的,提供了输出方法;
  • istream 类也是从 ios 类派生而来的,提供了输入方法;
  • iostream 类是基于 istream 和 ostream 类的,因此继承了输入方法和输出方法。

C++的 iostream 类库管理了很多细节。例如,在程序中包含 stream 文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)。cin 对象对应于标准输入流。cout 对象与标准输出流相对应。wcout 对象与此类似,但处理的是wchar_t 类型。cerr 对象与标准错误流相对应,可用于显示错误消息。clog 对象也对应着标准错误流。

使用cout进行输出

ostream类重新定义了«运算符,方法是将其重载为输出。对于C++中所有的基本类型该类都提供了operator«() 函数的定义。

对于char* 类型,使用字符串中的终止空字符来确定何时停止显示字符。

插入运算符的所有化身的返回类型都是 ostream &。也就是说,原型的格式如下: ostream & operator<<(type); 以实现拼接输出

ostream 类还提供了 put()方法和 write()方法,前者用于显示字符,后者用于显示字符串。

控制符 flush 刷新缓冲区,而控制符 endl 刷新缓冲区,并插入一个换行符。这两个控制符的使用方式与变量名相同

ostream插入运算符将值转换为文本格式。在默认情况下,格式化值的方式如下。

  • 对于 char 值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中
  • 对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中。字符串被显示在宽度等于该字符串长度的字段中。

浮点数的默认行为有变化。下面详细说明了老式实现和新实现之间的区别。

  • 新式:浮点类型被显示为6位,末尾的0不显示(注意,显示的数字位数与数字被存储时精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示(参见第3 章),取决于它的值。具体来说,当指数大于等于6 或小于等于-5 时,将使用科学计数法表示。另外,字段宽度恰好容纳数字和负号(如果有的话)。默认的行为对应于带%g 说明符的标准C库函数 fprintf()。
  • 老式:浮点类型显示为带6位小数,末尾的0不显示(注意,显示的数字位数与数字被存储时的精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示(参见第3 章),取决于它的值。另外,字段宽度恰好容纳数字和负号(如果有的话)。

要控制整数以十进制、十六进制还是八进制显示,可以使用 dec、hex 和 oct 控制符。 cout<<hex;

使用width 成员函数将长度不同的数字放到宽度相同的字段中,width()方法只影响接下来显示的一个项目,然后字段宽度将恢复为默认值。 cout.width(12);

在默认情况下,cout 用空格填充字段中未被使用的部分,可以用 fil()成员函数来改变填充字符 cout.fill('*');

使用precision()成员函数设置浮点数显示精度。和 width()的情况不同,但与 ill( )类似,新的精度设置将一直有效,直到被重新设置。 cout.precision(2);

setf( )函数(用于 set 标记),能够控制多种格式化特性。 使 cout 显示末尾小数点: cout.setf(ios_base::showpoint);

常量 含义
ios_base ::boolalpha 输入和输出bool值,可以为true或false
ios_base ::showbase 对于输出,使用 C++基数前缀(0, 0x)
ios_base ::showpoint 显示末尾的小数点
ios_base ::uppercase 对于16进制输出,使用大写字母,E表示法
ios_base ::showpos 在正数前面加上+

为简化工作,C++在头文件 iomanip中提供了其他一些控制符 setprecision()控制符接受一个指定精度的整数参数; setfill()控制符按受一个指定填充字符的 char 参数; setw()控制符接受一个指定字段宽度的整数参数。

使用cin进行输入

istream类(在iostream头文件中定义)重载了抽取运算符»,使之能够识别c++这些基本类型

可以将 hex、oct 和 dec 控制符与 cin 一起使用,来指定将整数输入解释为十六进制、八进制还是十进制格式。

cin 或 cout 对象包含一个描述流状态(stream state)的数据成员(从ios base类那里继承的)。流状态(被定义为ostate 类型,而ostate 是一种 bitmask 类型)由3个ios_base 元素组成:eofbit、badbit 或 failbit,其中每个元素都是一位,可以是1(设置)或0(清除)。当 cin 操作到达文件末尾时,它将设置 eofbit;当 cin 操作未能读取到预期的字符时,它将设置 failbit。I/0 失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将failbit 设置为1。在一些无法诊断的失败破坏流时,badbit 元素将被设置(实现没有必要就哪些情况下设置 failbit,哪些情况下设置 badbit 达成一致)。当全部 3 个状态位都设置为0时,说明一切顺利。程序可以检查流状态,并使用这种信息来决定下一步做什么。

假设某个输入函数设置了 eofbit,在默认情况下,这不会导致异常被引发。但可以使用 exceptions()方法来控制异常如何被处理。

只有在流状态良好 (所有的位都被清除) 的情况下,这个测试才返回 true: while (cin >> input)

  • 方法get(char&)和get(void)提供不跳过空白的单字符输入功能;
  • 函数 get(char*,int,char)和 getline(char*,int,char)在默认情况下读取整行而不是一个单词。

peek( )函数返回输入中的下一个字符,但不抽取输入流中的字符

putback( )函数将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句读取的第一个字符。

文件输入和输出

要让程序写入文件,必须这样做:

  1. 创建一个ofstream 对象来管理输出流
  2. 将该对象与特定的文件关联起来;
  3. 以使用 cout 的方式使用该对象,唯一的区别是输出将进入文件,而不是屏幕
1
2
3
4
5
6
ofstream fout;
fout.open("jar.txt");

ofstream fout("jar.txt"); // 作用与上面两步相同

fout << "data";

读取文件的要求与写入文件相似:

  1. 创建一个ifstream对象来管理输入流
  2. 将该对象与特定的文件关联起来;
  3. 以使用cin的方式使用该对象。
1
2
3
4
5
6
ifstream fin;
fin.open("jar.txt");

ofstream fis("jar.txt"); // 作用与上面两步相同

fin >> "data";

当输入和输出流对象过期(如程序终止) 时,到文件的连接将自动关闭。另外,也可以使用 close()方法来显式地关闭到文件的连接:

1
2
fout.close(); // close output connection to file
fin.close(); // close input connection to file

关闭这样的连接并不会删除流,而只是断开流到文件的连接。然而,流管理装置仍被保留,可以打开新文件。

较新的 C++实现提供了一种更好的检查文件是否被打开的方法—— is_open() 方法:

1
2
3
4
if (!fin.is_open()) // open attempt failed
{
    
}

is_open()fin.fail()fin.good()!fin 更好

对于 int main(int argc,char *argv[]) argc 为命令行中的参数个数,其中包括命令名本身。argv 变量为一个指针,它指向一个指向 char 的指针

open 可以接受两个参数,第二个控制打开模式。

常量 含义
ios_base::in 打开文件,以便读取
ios_base::out 打开文件,以便写入
ios_base::ate 打开文件,并移到文件尾
ios_base::app 追加到文件尾
ios_base::trunc 如果文件存在,则截短文件
ios_base::binary 二进制文件

ifstream open()方法和构造函数用 ios_base::in (打开文件以读取)作为模式参数的默认值,而ofstream open()方法和构造函数用 ios_base::out | ios_base::trunc(打开文件,以读取并截短文件)作为默值。

c++ 使用 ifstream fin(filename, c++mode); 而 c 使用 fopen(filename,cmode);

c++模式 c模式 含义
ios_base::in “r” 打开以读取
ios_base::out “w” 等价于ios_base::out | ios_base::trunc
ios_base::out | ios_base::trunc “w” 打开以写入,如果已经存在,则截短文件
ios_base::out | ios_base::app “a” 打开以写入,只追加
ios_base::out | ios_base::out “r+” 打开以读写,在文件允许的位置写入
ios_base::out | ios_base::out ios_base::binary “w+”
c++mode | ios_base::binary “cmodeb” 以C++mode(或相应的cmode)和二进制模式打开;
c++mode | ios_base::ate “cmode” 以指定的模式打开,并移到文件尾。C 使用一个独立的函数调用,而不是模式编码。

c++mode 是一个 openmode 值,如 ios_base::in; 而 cmode 是相应的 C 模式字符串,如“r”。

  • 二进制格式对于数字来说比较精确,因为它存储的是值的内部表示,因此不会有转换误差或舍入误差。
  • 以二进制格式保存数据的速度更快,因为不需要转换,并可以大块地存储数据。
  • 二进制格式通常占用的空间较小,这取决于数据的特征。
  • 然而,如果另一个系统使用另一种内部表示,则可能无法将数据传输给该系统。

二进制格式可以用 read()write() 读写

随机存取 使用 seekg()seekp() ,前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置

内核格式化

C++库还提供了sstream族,它们使用相同的接口提供程序和string对象之间的IO。这些类使得能够使用istream 和ostream 方法来抽取字符串中的信息,并对要放入到字符串中的信息进行格式化。

探讨C++新标准

复习前面介绍过的C++11功能

  • C++11新增了类型long long 和 unsigned longlong,以支持 64 位(或更宽)的整型;新增了类型 char16_t和 char32_t,以支持16位和 32位的字符表示:还新增了“原始”字符串。
  • 扩大了大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型。
  • 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。
  • auto 自动类型推断。
  • decltype 将变量的类型声明为表达式指定的类型。
  • 在函数名和参数列表后面(而不是前面)指定返回类型,配合使用 decltype 来指定模板函数的返回类型
  • using = 创建别名,相比于 typedef,可以支持模板部分具体化。
    template<typename T>
    using arr12 = std::array<T,12>;
  • 新增了关键字 nullptr ,用于表示空指针:它是指针类型,不能转换为整型类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式 nullptr==0 为 true
  • C++11 摒弃了 auto_ptr 并新增了三种智能指针: unique_ptr、shared_ptr 和weak_ptr
  • C++11 摒弃了遗产规范。添加了关键字 noexcept
  • 使用 class 或 struct 定义的枚举。新枚举要求进行显式限定,以免发生名称冲突。
  • 引入了关键词 explicit , 以禁止单参数构造函数导致的自动转换。
  • 支持类内成员初始化,可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。效果与构造函数提供成员初始化列表相同。
  • 基于范围的 for 循环
  • 新增了 STL容器forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset,新增了模板 array
  • 新增了 STL 方法cbegin()和 cend()。这些新方法将元素视为 const。
  • valarray 升级,支持 begin() 和 end()
  • 保留 export 关键字, 但终止了在C++98中的用法
  • 无需再避免 多层嵌套的 > 和运算符 >> 混淆
  • 新增右值引用 &&

移动语义和右值引用

移动语义:将一个对象中的资源移动到另外一个对象中。

右值引用 && 的主要应用就是重载了移动构造函数,利用了将亡值,将将亡值的空间内容交换到要拷贝的对象中。减少了深拷贝。

右值引用可以绑定到临时对象(右值),通过将资源的所有权从一个对象转移到另一个对象,避免了不必要的复制和销毁操作,提高程序效率。移动语义在大规模数据结构中尤为重要,例如std::vector、std::string等。

右值引用还可以用于函数模板的完美转发,即将参数以原始的形式传递给下一个函数,避免了不必要的复制和类型转换,提高了程序效率。

右值引用可以使用std::move()函数将对象强制转换为右值,使得该对象的所有权可以被移交,从而避免了内存泄漏的问题。

新的类功能

C++11 新增了两个:移动构造函数和移动赋值运算符

1
2
Someclass::Someclass(Someclass &&); // defaulted move constructor
Someclass & Someclass::operator(Someclass &&);// defaulted move assignment

默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似: 执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数和移动赋值运算符,这将调用它们: 否则将调用复制构造函数和复制赋值运算符

默认的方法,例如,您提供了移动构造函数,因此编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数在这些情况下,您可使用关键字 default 显式地声明这些方法的默认版本 Someclass & operator=(const Someclass &) = default; 禁用的方法,关键字 delete 可用于禁止编译器使用特定方法,类似于 default

C++11 允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种

使用 using 继承基类构造函数,让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数

假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本。在C++11 中,可使用虚说明符 override 指出您要覆盖一个虚函数: 将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。您可能想禁止派生类覆盖特定的虚方法,为此可在参数列表后面加上 final

Lambda函数

使用整个 lambda 表达式替换函数指针或函数符构造函数,让您能够使用匿名函数,即无需给函数命名。在 C++11 中,对于接受函数指针或函数符的函数,可使用匿名函数定义 (lambda)作为其参数。

使用 [] 替代了函数名(这就是匿名的由来); 没有声明返回类型。返回类型相当于使用decltyp 根据返回值推断得到的,这里为 bool。如果 lamda 不包含返回语,推断出的返回类型将为 void

1
2
bool f3(int x) {return x%3 == 0;}
[](int x) {return x%3 == 0;} // 其 lambda 表达式

可给 lambda 指定一个名称,并使用该名称多次

1
2
3
auto mod3= [](int x){return x%3 ==0;}
countl = std::count_if(nl.begin(), nl.end(), mod3);
count2 = std::count_if(n2.begin(), n2.end(), mod3);

lambad 可访问作用域内的任何动态变量,要捕获要使用的变量,可将其名称放在中括号内。如果只指定了变量名,如 [z] ,将按值访问变量;如果在名称前加上&,如 [&count] ,将按引用访问变量。

包装器

C++11 提供了其他的包装器,包括模板 bind、men_fm和reference_wrapper 以及包装器function 。

模板 function 是在头文件 functional 中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或 lambda 表达式。可以用来缩小可执行代码的规模。

可变参数模板

可变参数模板(variadic template) 让您能够创建这样的模板函数和模板类,即可接受可变数量的参数。 要创建可变参数模板,需要理解几个要点:

  • 模板参数包(parameter pack);
  • 函数参数包
  • 展开(unpack)参数包;
  • 递归。

C++11 提供了一个用省略号表示的元运算符 (meta-operator),让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还让您能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。其语法如下:

1
2
3
4
5
template<typename... Args> // Args is a template parameter pack
void show_list1(Args... args) // args is a function parameter pack
{
    ...
}

其中,Args 是一个模板参数包,而 args 是一个函数参数包。与其他参数名一样,可将这些参数包的名称指定为任何符合 C++标识符规则的名称。Args 和T的差别在于,T 与一种类型匹配,而Args 与任意数量(包括零)的类型匹配。

展开参数包,将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。

1
2
3
4
5
template<typename T, typename... Args>
void show_list3( T value, Args... args){
    ...
    show_list3(args...);
}

可进一步改进,单独写0,1个参数的情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// definition for 0 parameters
void show list(){}
// definition for l parameter
template<typename T>
void show list(const T& value)
{
    std::cout << value << '\n';
}
//definition for 2 or more parameters
template<typename T, typename... Args>
void show_list(const T& value, Args&... args){
    std::cout << value << '\n';
    show_list(args...);
}

C++11新增的其他功能

为解决并行性问题,C++定义了一个支持线程化执行的内存模型,添加了关键字 thread_local,提供了相关的库支持。关键字 thread_local 将变量声明为静态存储,其持续性与特定线程相关即定义这种变量的线程过期时,变量也将过期。 库支持由原子操作 (atomic operation)库和线程支持库组成,其中原子操作库提供了头文件 atomic.而线程支持库提供了头文件thread、mutex、condition_variable和future。

C++11 添加了多个专用库

  • 头文件 random 支持的可扩展随机数库提供了大量比 rand( )复杂的随机数工具。
  • 头文件chrono 提供了处理时间间隔的途径。
  • 头文件 tuple 支持模板 tuple。tuple 对象是广义的 pair 对象。pair 对象可存储两个类型不同的值,而 tuple对象可存储任意多个类型不同的值。
  • 头文件 ratio 支持的编译阶段有理数算术库让您能够准确地表示任何有理数,其分子和分母可用最宽的整型表示。它还支持对这些有理数进行算术运算。
  • 头文件 regex 支持的正则表达式库。

低级编程 低级编程中的“低级”指的是抽象程度,而不是编程质量,低级意味着接近于计算机硬件和机器语言使用的比特和字节。

  • 放松了 POD (Plain Old Data) 的要求
  • 允许共用体的成员有构造函数和析构函数
  • 解决了内存对齐问题
  • constexpr 机制让编译器能够在编译阶段计算结果为常量的表达式,让 const 变量可存储在只读内存中

杂项

  • 提供了一种创建用户自定义字面量的机制: 字面量运算符 (literal operator)。使用这种机制可定义二进制字面量,如1001001b,相应的字面量运算符将把它转换为整数值。
  • 提供了调试工具 assert。
  • 加强了对元编程 (metaprogramming)的支持。元编程指的是编写这样的程序,它创建或修改其程序,甚至修改自身。

语言变化

主要指 Boost 项目和TR1

接下来的任务