cpp
简单记录一下
《C++ Primer Plus》
第十二章 类和动态内存分配
特殊成员函数
C++ 编译器自动生成下面这些成员函数的定义(如果没有定义):
默认构造函数
默认析构函数
复制构造函数
赋值运算符
地址运算符
移动构造函数(C++ 11)
移动赋值运算符(C++ 11)
默认构造函数
指不接受参数的构造函数,有了默认构造函数后可以使用类似于初始化自动变量的方式来初始化一个对象
1 | Klunk lunk; |
如果定义一个类时没有提供任何构造函数,编译器将提供以下默认构造函数
1 | Klunk::Klunk() {} |
也可以显式定义默认构造函数,常用于设置成员变量
1 | Klunk::Klunk() |
在所有参数都有默认值的情况下,带有参数的构造函数也是默认构造函数
1 | Klunk(int n=0) |
但只能有一个默认构造函数。
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中(通常是初始化过程,包括按值传递参数),而不是常规的赋值过程。
类的复制构造函数原型通常如下
1 | Class_name(const Class_name &); |
新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用,假设 motto 是一个 StringBad 对象,以下声明将调用复制构造函数
1 | StringBad ditto(motto); |
每当程序生成了对象副本,编译器都将使用复制构造函数。当函数按值传递对象或者返回对象时都将使用复制构造函数。
生成临时对象时也将使用复制构造函数。
默认的复制构造函数逐个复制非静态成员,复制的是成员的值(浅复制)。以下两种方式等效
1 | StringBad sailor = sports; |
解决方案为定义一个显式的复制构造函数
1 | StringBad::StringBad(const StringBad & st) |
赋值运算符
将已有的对象赋值给另一个对象时将使用重载的赋值运算符。
类似于复制构造函数,赋值运算符的隐式实现也对成员进行逐个复制(浅复制)。
提供赋值运算符进行深度复制的定义时,实现与复制构造函数类似。但有一些不同:
- 由于目标对象可能引用了之前分配的数据,函数应使用
delete[]
释放这些数据 - 函数应当避免赋值给自身,否则释放内存时可能删除对象的内存
- 函数返回一个指向调用对象的引用
1 | StringBad & StringBad::operator=(const StringBad & st) |
其中 delete []
是由于稍后将把一个新的字符串地址赋给 str,如果不释放,则这个字符串将一直保留在内存中。
子类赋值运算符中需要调用父类的赋值运算符时
1 | baseDMA::operator=(hs); |
定位 new 运算符
对于使用定位 new 运算符分配的内存空间,应以与创建时相反的顺序显式调用析构函数来释放空间,而不是使用 delete。
初始化列表
仅可以在构造函数中使用初始化列表
1 | TTP:TTP (const string& fn, const string& ln) : firstname(fn), lastname(ln) |
第十三章 类继承
构造函数
子类的构造函数应总是使用初始化列表显式调用合适的父类构造函数,否则将调用默认构造函数
1 | // 声明 |
指针
指向父类的指针可以用于指向子类,但只能使用父类的数据成员和公有方法。
虚方法
virtual 有一个含义为 实际上的,事实上的,理解起来更容易。
被声明为 virtual 的方法在通过指针或引用调用时,将根据指针指向的对象的实际类型(而不是指针的类型,因为子类对象可以使用指向父类对象的指针)调用相应的方法。
virtual 关键字只用于类声明的方法原型中,不用在定义中。
虚析构函数
如果子类的析构函数中包含了某些操作,那么父类应显式声明一个虚的析构函数。否则当使用父类指针的子类对象销毁时将只调用父类的析构函数。
用作基类的函数应总是声明一个虚析构函数。
重新定义方法
如果在子类中重新定义了父类的方法,但与父类的特征标不通,子类的方法将覆盖父类的方法,而不是重载。
经验法则:
- 如果重新定义继承的方法,应确保与原来的原型完全想通。例外:如果返回基类的指针或引用,则可以修改为派生类的指针或引用
- 如果基类的有重载的方法需要重写,则在派生类中应重新定义所有的重载版本
纯虚函数
类似于接口,声明一个类方法时可以在声明后加 =0
使其成为纯虚方法,有纯虚方法的类不能实例化,基类中不必提供纯虚方法的定义,而子类必须覆盖纯虚方法。
第十四章 代码重用
包含
一个类中可以包含另一个类
1 | class Studnet |
构造函数定义
1 | Studnet(const char* str): name(str) {} |
多数情况下应使用包含,除非需要使用私有继承的特性如使用 protected 成员或者需要重写虚方法。
模板类 valarray
声明
1 | valarray<type> classname; |
初始化
1 | double gpa[3] = {3.1, 3.5, 3.8}; |
私有继承
基类的公有成员将成为派生类的私有成员,基类的接口在培盛磊中可用
1 | class Student :private std::string |
构造函数定义
1 | Student(const char* str) :std::string(str){} |
由于是继承,所以使用类名调用基类的构造函数。
使用类名和作用域解析符调用基类的方法,使用强制类型转换将子类转换为基类来访问基类对象本身
1 | double Student::Average() const |
保护继承
基类的公有成员和保护成员都成为派生类的保护成员,继承层次较多时依然可用
多重继承
必须使用访问控制符修饰每一个基类,否则默认为 private。
不使用虚基类的 MI 不会引入新的语法规则。
虚基类
假设 Singer 和 Waiter 继承自 Worker,SingingWaiter 继承自 Singer 和 Waiter
使用虚基类使 SingingWaiter 只包含一个 Worker(通常将包含两个,分别来自 Singer 和 Waiter)
1 | class Singer : virtual public Worker ; |
此时 SingingWaiter 只包含 Worker 对象的一个副本,本质上时 Singer 和 Waiter 共享一个 Waiter 对象
构造函数
如果 Worker 是虚基类,则在以下 MI 构造函数中
1 | SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other) |
初始化了 panache 和 voice,但不会将 wk 的信息传递给子对象 Waiter 和 Singer(否则会由于使用两个 Worker 的构造函数导致冲突)。但创建派生类对象前必须调用基类构造函数,因此会使用 Worker 的默认构造函数。
如果不希望使用默认构造函数创建虚基类对象,则需要显式调用所需的构造函数
1 | SingingWaiter(const Worker& wk, int p =0, int v = Singer::other) |
只能对虚基类这样做
成员方法
应在 SingingWaiter 中重写的 Show
方法指定使用 Singer 或是 Waiter 的版本
1 | void SingingWaiter::Show() |
类模板
使用模板定义替换类声明,使用模板成员函数替换类成员函数,模板类也以template <typename Type>
开头,Type 为泛型名,常用 Type 和 T,模板被调用时,Type 将被具体类型取代。
模板的具体实现和声明应放置在同一文件中。
1 | // stack.h |
使用时用具体类型替代泛型名进行实例化
1 | Stack<int> is; |
必须显式提供所需类型(不同于函数模板)。
与函数模板类似,有隐式实例化,显式实例化,和显式具体化,统称为具体化(specialization)
友元
使用友元函数时,应使用具体化的对象,声明中
1 | template <typename T> |
而且由于 report 不是成员函数,所以必须为其提供定义显式具体化
1 | void report(HasFriend<short> &) ; |
另一种解决方案时使友元函数本身成为模板
首先在类定义前面声明每个模板函数
1 | template <typename T> void counts(); |
然后在函数中再次将模板声明为友元
1 | template <typename T> |
最后要为友元提供模板定义
模板别名
可以使用 typedef 为模板具体化指定别名
1 | typedef std::array<double, 12> arrd; |
也可以使用模板提供一系列别名
1 | template<typename T> |
using = 语法用于非模板时等价于 typedef
1 | typedef const char *pc1; |
第十五章 友元、异常和其他
友元类
被声明为友元的类可以访问 private 和 protected 成员
1 | class Tv |
可以只将另外一个类的某个成员函数作为友元,此时需要重排声明顺序
1 | class Tv; |
也可以使两个类是彼此的友元
1 | class Tv |
嵌套类
可以在类中定义类
1 | class Queue |
可以在嵌套类中使用模板
异常
cstdlib 中包含 abort 函数,它向 stderr 中发送消息,然后终止程序。
也可以使用返回值指出出现了错误。
也可以使用 try catch
1 | int a = 3; |
通常将对象用作异常类型,可以获得更多信息
1 | class err |
try catch 中将总会为异常创建一个拷贝,即使声明了引用。此时声明引用的意义在于基类的引用可以用于子类。
exception 类
C++ 中提供了异常类的基类 exception
类,在 exception
头文件中,它有 what
虚方法,返回一个字符串。
头文件 stdexcept
定义了 logic_error
和 runtime_error
等异常类,它们都是 exception 的公有派生。构造函数接受一个 string 对象为参数,为 what 方法提供数据。两个类都有一些派生类
1 | // logic_error 系列 |
新标准中,当 new 无法分配请求的内存时会引发 bad_alloc
异常,它时 exception 的公有派生。以前则是返回空指针。新标准中也提供了失败时返回空指针的 new
1 | int * pi = new (std::nothrow) int; |
异常处理失败
throw 一个异常后,如果在带有异常规范的函数中引发,则必须与异常规范列表中的某种一场匹配,否则称为意外异常,导致程序异常终止。如果异常不是在函数中引发的(或函数没有异常规范),则必须 catch ,否则称为未捕获的异常,默认情况下导致程序异常终止,但可以修改默认行为。
RTTI
运行阶段类型识别(Runtime Type Identification),只适用于包含虚函数的类层次结构,因为只有这种结构才应该将派生对象的地址赋给基类指针
C++ 有3个支持 RTTI 的元素:
- 如果可能的话,
dynamic_cast
运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则返回0(空指针) typeid
运算符返回一个指出对象类型的值type_info
结构存储了有关特定类型的信息
类型转换运算符
新增4个类型转换运算符,规范转换过程
1 | dynamic_cast |
第十六章 string 类和标准模板库
string 类
string 类在头文件 string 中,实际上是模板具体化basic_string<char>
的一个 typedef,同时省略了有关内存管理的参数。
构造函数
size_type
是一个依赖于实现的整型,npos
为字符串的最大长度,通常为unsigned int
的最大值,NBTS
(null-terminated string)表示 C字符串。
1 | string(const char* s); |
输入
C字符串的输入
1 | char info[100]; |
string 对象的输入
1 | string stuff; |
getline 都有一个可选参数用于指定确定输入边界的字符
1 | cin.getline(info, 100, ':'); // discard : |
string 的 getline 函数读取字符存入 string 对象,直到三者之一发生:
- 到达文件尾,此时会设置输入流的 eofbit,fail 和 eof 函数都将返回 true
- 遇到分界字符(默认为换行),将从输入流中删除分界字符,但不存储它
- 读取的字符达到最大允许值,(string::npos 和可供分配的内存字节数中较小的一个),将设置 failbit,fail 方法返回 true
输入流对象有统计系统,遇到文件尾设置 eofbit,检测到输入错误设置 failbit,出现无法识别的故障设置 badbit,顺利时设置 goodbit。
string 的 operater>> 函数类似,但是遇到空白字符时将把该空白字符留在输入队列中。空白字符包括空格、换行和制表符,使用isspace()
会返回 true。
方法
string 对象重载了全部 6 个关系运算符,按 ascii 码排列。
可以使用 +=
拼接字符串
size 和 length 成员函数返回字符串中的字符数。
find 方法可以寻找子字符串或字符,可以指定开始位置,也可以指定将参数的前 n 个字符作为待查找的子字符串。除此之外,还有 rfind,find_first_of,find_last_of,find_first_not_of,find_last_not_of 等方法。
c_str 方法返回一个指向 C 字符串的指针
字符串种类
string 库实际上基于一个模板类
1 | template<class charT, class traits = char_traits<charT>, |
模板 basic_string 有四个具体化,每个都有一个 typedef 名称
1 | typedef basic_string<char> string; |
智能指针模板类
智能指针时行为类似于指针的类对象,但具有其他功能。对象过期时,对象的析构函数可以删除指向的内存。
有三个智能指针对象,auto_ptr(C++11 废弃),unique_ptr 和 shared_ptr(后面两个为C++11新增)。
使用 new 将返回的地址赋给这些对象,无需记住使用 delete 释放内存,智能指针过期时,将由析构函数自动释放内存。
智能指针不能用于非堆内存。
使用智能指针对象需要包含头文件 memory,在命名空间 std 中使用,然后使用通常的模板语法实例化所需类型的指针。
1 | // 声明 |
在多次使用同一个指针初始化多个 auto_ptr 时,会对同一个内存块多次调用 delete 导致崩溃。
多个 unique_ptr 不能指向同一个内存块,否则会引发编译错误。
多个 shared_ptr 指向同一个内存块时,存在一个计数器,记录 shared_ptr 的数量,只有减到 0 时才会释放内存。
如果程序要使用多个指向同一个对象的指针应使用 shared_ptr,否则使用 unique_ptr。
标准模板库(STL)
STL 提供了一组表示容器、迭代器、函数对象和算法的模板。STL 不是面向对象的编程,而是泛型编程(generic programming)。
STL 从广泛角度定义了一些非成员函数执行一些操作。
容器模板类 vector
1 |
|
与 string 类相似,各种 STL 容器模板都提供一个可选的模板参数用来指定使用哪个分配器对象管理内存。
STL 的容器模板类都提供了一些基本方法,包括:
- size,返回容器中元素的数目
- swap,交换两个容器的内容
- begin,返回一个指向容器中第一个元素的迭代器
- end,返回一个表示超过容器尾的迭代器
vector 还有一些只有部分容器才有的方法:
- push_back(element),将元素添加到 vector 末尾
- erase(begin, end),接受两个迭代器参数,删除区间内的元素,包括 begin 但不包括 end
- insert(pos, begin, end),接受三个迭代器参数,第一个指定新元素的插入位置,第二个和第三个定义插入的区间
还有一些 STL 函数,它们是非成员函数:
- for_each,接受三个参数,前两个是定义容器区间的迭代器,最后一个是指向函数的指针(或者说函数对象),for_each 函数将被指向的函数应用于区间中的每个元素
- random_shuffle 函数接受两个指定区间的迭代器参数,并随机排列区间中的元素,此函数要求容器类允许随机访问
- sort,有两个版本,第一个版本接受两个定义区间的迭代器参数,使用 operator< 比较,用于用户定义的类型时必须提供 operator< 定义。第二个版本接受第三个参数,是想要使用的函数指针(函数对象),这个函数需要返回能转化为 bool 的值,返回 false 代表顺序错误。同样要求容器允许随机访问
泛型编程
迭代器
迭代器使算法独立于使用的容器类型,具有以下特征
- 能够解除引用
- 能将一个迭代器赋值给另一个
- 迭代器之间能够比较
- 能使用迭代器遍历容器中的元素,通过为迭代器 p 定义 ++p 和 p++ 实现
有5种迭代器,分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。算法原型指出了需要的迭代器
1 | template<class InputIterator, class T> |
算法会尽量使用低等级的迭代器。
输入迭代器
“输入”是从程序的角度来说,即来自容器的信息被视为输入。因此,输入迭代器可被容器用来读取容器中的信息。
对输入迭代器解除引用使程序能够读取容器中的值,但不一定能够修改,所以需要输入迭代器的算法不会改变容器中的值。
输入迭代器必须能够访问容器中所有的值,通过支持 ++ 运算符(前缀和后缀格式)实现。
输入迭代器使单项迭代器,可以递增不能倒退。基于输入迭代器的算法应该是 single-pass 的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。
输出迭代器
与输入迭代器相反。
正向迭代器
正向迭代器只能使用++运算符便利容器,但是它总是按相同顺序便利一系列值,迭代器递增后依然可以对前面的迭代器值解出引用并得到相同的值,因此可以用于 多次通行算法。
即可以只读,也可以读写。
双向迭代器
相比于正向迭代器,同时支持 – 运算符。
随机访问迭代器
有些算法要求能够直接跳到容器中的任何一个元素(随机访问),除了双向迭代器的特性意外,同时需要支持随机访问。
在容器区间内,需要支持一系列运算,包括:+(迭代器和数字相加,顺序不限),-(迭代器减数字),+=,-=,[],<,>,>=,<=。
概念改进和模型——无需关联容器
函数对象——其他库
第十七章 输入、输出和文件
输入与输出
流和缓冲区和 iostream 文件
C++ 程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。
iostream 文件包含了一些类:
- streambuf 类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法
- ios_base 表示流的一般特征,如是否可读取,时二进制还是文本流等
- ios 类基于 ios_base ,其中包含了一个指向 streambuf 对象的指针成员
- ostream 和 istream 是从 ios 类派生而来的,分别提供了输出和输入方法
- iostream 类是基于 istream 和 ostream 类的,继承了输入和输出方法。
在程序中包含 iostream 文件将自动创建8个流对象:cin,wcin,cout,wcout,cerr,wcerr,clog,wclog。流对象存储了有关输出的数据成员,如显式数据时使用的字符宽度,小数位数,显示整数时采用的技术方法及 streambuf 对象的地址。
重定向
对标准输出的重定向不影响 cerr 和 clog
使用 cout 输出
使用 ostream 对象和 << 运算符输出,<<运算符应返回 ostream &来连续输出
ostream 类还提供了 put(显示字符)和 write(显示字符串)方法
刷新输出缓冲区
使用 cout 将字节发送到标准输出时,字节被存储在缓冲区中。缓冲区被填满时,程序将刷新缓冲区,把内容发送出去,并清空缓冲区。输出到屏幕时,多数实现会在输入发生时刷新缓冲区,也可以使用控制符刷新缓冲区
1 | cout << 'a' << flush; |
也可以使用函数刷新
1 | flush(cout) |
用 cout 进行格式化
ostream 插入运算将值转换为文本格式。
ios_base 类存储了描述格式状态的信息,通过使用 ios_base 的成员函数,可以控制字段宽度和小数位数。