《C++语言程序设计》学习笔记

发布于 2022-08-05  85 次阅读


已完结

Chap 3

  • 内联函数:并不是简单文本替换;定义必须在调用前。
  • 函数重载:以形参的个数、类型为区分,跟返回值、形参名无关。

Chap 4

  • 构造函数类名为函数名,可使用形如Clock::Clock(int newH,int newM):hour(newH),min(newM){}的形式来初始化成员变量,效率更高。
  • 可在类声明中使用Clock()=default来声明构造函数,以使其在需要时自动生成默认构造函数。
  • 委托构造函数可减少重复代码,形如:
Clock::Clock(int newH,int newM,int newS):hour(newH),minute(newM),second(newS){};
Clock::Clock():Clock(0,0,0){};
  • 复制构造函数,隐含生成的复制构造函数可以一一复制对应的成员变量,自定义的复制构造函数可以实现自定义特殊功能。参数表使用本类对象的常引用。
  • 如果不希望对象被复制构造,可在复制构造函数声明时使用=delete
  • 注意区分&在何时表示引用,在何时表示取地址。
  • 区分左值引用和右值引用及其表示。左值可使用move()转换为右值,从而可以被右值引用,此时被引用对象不该再被使用,只能销毁或重新赋值后使用。
  • 移动构造函数,以移动数据的方式构造新对象,不分配新内存。被移动的对象不能再使用,只能销毁或重新赋值使用。举例:
#include <utility>
class astring{
public
    std::string s;
    astring(astring&& o)noexcept:s(std:move(o.s)) //显式移动所有成员,理论上移动构造函数不会报错,故声明noexcept
    {函数体}
}
  • 使用移动构造函数,是为了清除不再被需要的对象,相比使用复制构造函数,更能节省空间,提高效率。
  • 析构函数在对象消亡前完成清理工作。未定义时将自动生成空的。以~类名为函数名,参数表必定为空。
  • 类组合时的构造函数声明形式:
类名::类名(对象成员需要的形参, 本类成员形参):对象1(参数), 对象2(参数), ……{
    //构造函数体
}
  • 类在使用前必须声明,可以是前向引用声明,可以是具体定义。如果是前向引用声明,则在具体定义前,不能声明该类对象,而做形参是可以的。

  • 枚举类型:

    • 枚举类型默认以0起始赋值,也可以指定值
    • 在未指定值之前,仍以0起始
    • 在指定值之后,顺次+1
    • 此外,枚举类型只能使用标识符,而不能是整型、字符型等
    • 与类定义类似,使用enum(就像class)定义后,需要“实例化”使用
    • 枚举定义中大括号内的标识符,可以赋给整型变量

Chap 5

  • 限定作用域的枚举类型,如:enum class color {},使用其中元素时需要前缀:color::

  • 嵌套作用域中,内外层存在同名标识符的情况下,内层中标识符在内层中起作用,即屏蔽了外层的同名标识符。

  • 静态变量(static)在重复进入函数时不会再次初始化,其生存期与整个程序相当。

  • 类的静态数据成员(static)在所有同类对象间共享,仅有一份。一般在类内声明,在类外初始化(使用::)。

  • 类的静态函数成员,与静态数据成员类似。可用类名::函数来调用。也可以通过对象调用。此函数一般用于处理静态数据成员。静态函数没有this指针。

  • 友元函数实际上不是成员函数,因此在类体中任何位置都可声明。友元函数可以访问该类的privateprotected成员。调用时直接使用函数名。定义时无需类名前缀。

  • 友元类,在一个类定义中将另一个类声明为友元类。如果在组合类中有打开成员封装的需求,则可使用友元类。

  • 如果既需要共享数据、为减小开销而使用引用,又不想降低数据的安全性,可使用常引用。

  • 常类型:

    • 常对象,const 类名 对象名必须被初始化,不能被更新。常对象只能调用常成员函数。
    • 常成员,包括常数据成员和常函数成员,类型说明符 函数名(参数表) const。常成员函数不能改变对象的数据成员。事实上const也可以参与函数重载。
    • 常数据成员必须在初始化列表中进行初始化(构造函数后跟:的部分)
    • 常引用,被引用的对象不能被更新。用常引用做形参,则不会发生对实参的改变。
    • 常数组,数组元素不能被更新
    • 常指针,指向常量的指针
  • 若只将类Y中的成员函数m声明为类X的友元函数,则m应在类X前声明。

  • 在定义成员函数时,在函数名后加const,可说明该函数不改变类的数据成员,若函数企图改变,则编译器会报错。形如:int getX() const {return x;}

  • 个人习惯

    • 将类中所有友元在类声明前声明。
    • 可在所有类声明前先做所有类的前向引用声明。

    Chap6 数组和指针

  • 数组名做参数,传送的是数组首地址,对形参数组的改变将直接影响到实参数组。

  • 数组中每一个元素对象被创建时,系统将调用类的构造函数对该对象进行初始化。

  • 若没有为对象指定显式初始值,则使用默认构造函数来初始化。

  • 当数组中对象被删除时,调用析构函数。

  • #ifndef的使用:

    • 为了避免头文件引用混乱而使用。
    • 例:
#ifndef _STDIO_H_ //头文件前后加下划线,点也变_,全大写
#define _STDIO_H_
......
#endif
  • 向指针变量赋的值必须是合法地址常量或变量。如&运算符取的地址,或动态分配内存返回的首地址。此外,整数0可以赋给指针,表示空指针。可以声明void类型指针,可以指向任何类型的对象地址。无类型指针不能直接使用存取,因为不知道类型,则无法确定取出几个字节等信息,在使用前需要做强制类型转换。
  • 安全起见,以nullptr表示空指针。
  • 指向常量的指针:以const在前声明的指针,不能通过该指针来改变所指对象的值(无论该对象是否为const),但可以更换指向的对象。
  • 指针常量:以const在后声明的指针,不能改变指针本身的值,即不能更换指向的对象。
  • 指针变量做形参时,实参使用变量地址。
  • 不要将非静态的局部变量地址作为返回值,这是非法的。
  • 返回的指针要确保在主调函数中是合法、有效的。
  • 内存的分配和释放最好保证在同一级。
  • 注意区别返回值为指针类型的函数(int* 函数名();),和指向函数的指针(int (*函数名)();)。
  • 注意区分指针数组int* account[]和指向数组的指针int (*account)[]
  • 函数回调:只定义一个框架,使得调用同一个函数可以实现不同的功能。
  • 对于对象的不同访问方式:ptr->getX()(*ptr).getX()
  • this指针,隐含于类的每一个非静态成员函数中。(静态成员函数全局只有一份,自然没有this的概念)
  • 虽然前向引用声明后,具体定义前,不能声明该类对象,但可以声明该类的指针来达到目的。
  • new对象时,要么根据括号中的参数初始化,要么调用默认构造函数初始化。
  • 动态申请数组空间:new 类型名T[表达式][常量表达式]……仅第一维可为变量。
  • char (*fp)[3];声明了一个指向一维数组的指针,该数组元素为字符型,数组中有三个元素。fp = new char[n][3]fp中存放该二维数组的首地址,或者说该二维数组第一行的首地址。fp + 1为该二维数组第二行的首地址。在定义指向动态多维数组的指针时,抹去多维数组的首维,则是指针应该指向的数据类型(指针是首地址,总比实际数组少一维)。可将指针名作为数组名使用,直接用方括号进行访问。
  • 释放指向的内存使用delete,释放指向的数组使用delete[]
  • 函数返回为“引用”类型,使得接收返回后可以操作该对象。如果返回“值”,则只是返回一个副本。
  • vector容器:vector<类型名> 数组对象名(长度),此处将类型名参数化了。vector数组对象名不表示数组首地址,首地址被封装起来了。注意,此时该对象类型为vector<类型名>
  • 指针本身存放对象地址,而寻址长度由其类型决定。比如int型指针是在对象地址向后4字节作为变量的存储单元。void型只有地址而没有长度,因此任何类型指针都可以直接赋给void指针,而反过来则需要做显式类型转换。

Chap7 继承

继承的类型

  • public 继承:
    • 基类public成员,在派生类的类内和对象皆可访问
    • 基类protected成员,在派生类的类内可以访问,对象不可访问
    • 基类private成员,在派生类的类内和对象都不能访问
  • private继承
    • 基类publicprotected成员以private身份出现在派生类中,可被类内成员函数访问,不能被对象访问
    • 基类private成员,在派生类的类内和对象都不能访问
  • protected继承
    • 基类publicprotected成员以protected身份出现在派生类中,可被类内成员函数访问,不能被对象访问
    • 基类private成员,在派生类的类内和对象都不能访问
  • 向上转型:指公有派生类的对象在使用上可以被当做基类的对象,反之不可。具体表现在:
    • 公有派生类的对象可以隐含转换为基类对象
    • 公有派生类的对象可以初始化基类的引用
    • 派生类的指针可以隐含转换为基类的指针
    • 另外,通过基类的对象名、指针,只能使用从基类继承的成员。如果派生类中定义了,与基类中继承来的成员函数同名的成员函数,在使用基类对象名、指针时,依然调用基类的成员函数。
    • 建议:绝对不要重新定义继承而来的非虚函数
  • 向上转型总是安全的,一般不用dynamic_cast动态转换(该操作是有开销的)
  • 向下转型则存在风险

派生类的构造函数

  • 默认情况下,基类构造函数不被继承。可以使用using Base::Base;来继承基类构造函数,使之成为派生类的构造函数,但只能初始化从基类继承来的成员
  • 派生类的构造函数,只需要初始化本类新增的成员,继承来的基类成员是自动调用基类构造函数进行初始化的。因此,派生类的构造函数需要给基类的构造函数传递参数。
  • 单一继承时的派生类构造函数定义:
派生类名::派生类名(基类所需形参,本类所需形参):基类名(基类初始化参数表), 本类成员初始化列表 //不论初始化列表顺序,一定是先调用基类构造函数
{
  //其他初始化
}
  • 多继承时的派生类构造函数定义:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表), ……, 基类名n(基类n初始化参数表), 本类成员初始化列表 //没有列入的基类,将自动调用对应的无参数的构造函数
{
  //其他初始化
}
  • 多继承,且有对象成员(组合)时的构造函数:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表), ……, 基类名n(基类n初始化参数表), 对象成员初始化列表, 基本类型成员初始化列表
{
  //其他初始化
}
  • 派生类构造函数执行顺序:
    1. 调用基类构造函数,顺序按照它们被继承时的顺序
    2. 初始化对象成员和基本类型成员,顺序按照它们在类中被声明的顺序。对象成员初始化是自动调用其所属类的构造函数完成的
    3. 执行派生类构造函数体中的内容

派生类的复制构造函数

  • 没有编写派生类的复制构造函数时,编译器生成隐含的复制构造函数,该函数先调用基类复制构造函数,再为派生类新增成员对象执行复制
  • 若编写派生类的复制构造函数,需要为基类的复制构造函数传递参数。显然,派生类的复制构造函数只能接收一个参数,该参数不仅用于初始化派生类定义的成员,也将被传递给基类的复制构造函数。此外,基类的复制构造函数的形参是基类对象的引用,而实参可以是派生类对象的引用,这也是向上转型的一个应用。形如:
C::C(const C &c1):B(c1){……}

派生类的析构函数

本节内容不完全正确,仅在一定范围内适用

  • 基类的析构函数不会被继承,如果派生类需要,要自行声明析构函数。声明方法与一般类(无继承的类)的析构函数相同
  • 不需要显式地调用基类的析构函数,系统会自动隐式调用
  • 析构函数的调用次序与构造函数相反

作用域限定

  • 当派生类与基类中有相同成员时:
    • 若未特别限定,则通过派生类对象使用的,是派生类中的同名成员
    • 若需要通过派生类对象访问基类中被隐藏的同名成员,应当使用基类名和作用域操作符::来限定,即:Base::成员名

二义性问题

派生类从不同基类继承了同名成员,而在派生类中没有定义同名成员,则通过派生类对象名or引用名.成员名派生类指针->成员名的形式访问成员,存在二义性问题。为了解决此问题,需要使用类名限定。

冗余问题

Base0中定义了成员var0Base1Base2都从Base0处继承得到了var0derived同时继承Base1Base2,则使用derived.var0以及derived.Base0::var0都存在二义性,且可能导致不一致性(var0有两个不一样的值)

  • 解决冗余问题:使用虚基类,在第一级继承时就声明为虚继承,即:
class Base1: virtual public Base0{};
class Base2: virtual public Base0{};

虚基类及其派生类的构造函数

  • 建立对象时指定的类称为最远派生类。
  • 虚基类的成员,是通过最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在其构造函数的成员初始化列表中,为虚基类的构造函数列出参数。否则,调用虚基类的默认构造函数。
  • 在建立对象时,只有最远派生类的构造函数会调用虚基类的构造函数,继承结构中间的其他类对虚基类的构造函数调用,将被忽略。
  • 虚基类并非一个完美方案。虚基类的使用应当限制在一个可控的范围内,否则在整个继承结构不可知的情况下,无法判断何时要给哪个基类传递参数。
  • 基于上一条,既然虚基类的适用范围是有限的,我们就应当仅在必要的情况下,才使用多继承,以此避免出现二义性和冗余问题。

Chap8 多态性

  • 虚函数都不能是内联函数,所以类体中只写声明,定义写在类体外。且virtual只能写在函数声明时,即类体内。因为调用虚函数需要动态绑定,而内联函数的处理是静态的。
  • 动态绑定的实现方式:虚表。虚表中存放了相应虚函数的代码入口地址。使用派生类对象地址初始化基类指针后,调用虚函数会先从派生类对象的虚表中寻找函数入口地址,从而实现动态绑定。
  • 如果派生类某成员函数并未显式声明virtual,但其与基类某虚函数有着相同的返回值类型(或可被隐含转换)、函数名、参数列表(包括数目和类型)、cv限定符(如const),则自动被确定为虚函数,并覆盖基类虚函数,隐藏基类中所有同名函数的重载形式。为了程序可读性,习惯于显式使用virtual关键字。
  • 一般非静态成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数
  • 派生类继承的虚函数可以不去再次定义覆盖。而继承的纯虚函数必须被再次定义覆盖,否则自身也将变为抽象类。
  • 含有纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类使用。
  • override(写在最右侧): 显式函数覆盖。声明该函数必须覆盖基类的虚函数,否则报错。(覆盖要求函数签名一致,包括函数名、参数列表、const、返回值相同或可隐含转换)
  • override在声明了但未覆盖时,或基类中不存在可以被覆盖的虚函数时,会报错。
  • final(写在最右侧):避免类被继承,或基类函数被覆盖

运算符重载

  • 对已有运算符赋予多重含义,使同一个运算符作用于不同类的数据时导致不同的行为
  • 重载之后,运算符的优先级和结合性都不会改变
  • 运算符可以重载为类的非静态函数成员,亦可以重载为类外友元函数
  • 形如:返回值类型 operator + (参数表);

重载为成员函数

  • +-重载为类内成员函数后,两个操作数一个是对象自己,一个由形参列表提供(除自增自减操作符外,形参列表应为操作数-1)
  • 使用重载运算符,实际上是函数调用,如:·oprd1.operator + (oprd2)
  • 对于单目运算符重载函数,前置时没有形参,后置时有一个int型形参(只有类型,无参数名,只是作为区分,实际上并不使用)
  • 前置++Clock& operator ++ (),后置++Clock operator ++ (int),两者区别在于参数列表和返回值类型
  • 实例:
Clock & Clock::operator ++ ()
{
    second++;
    if (second >= 60)
    {
        second -= 60;
        minute++;
        if (minute >= 60)
        {
            minute -= 60;
            hour = (hour + 1) % 24;
        }
        return *this;
    }
}

Clock Clock::operator ++ (int)
{
    //注意形参表中的整型参数
    Clock old = *this;
    ++(*this); //调用前置“++”运算符
    return old;
}
  • 无法重载基本数据类型的单目运算符

重载为非成员函数

  • 当左操作数不是类的对象,或者不是我们编写的类的对象,则应重载为非成员函数(类外),需要时可以声明友元
  • 重载插入操作符<<,用于如下输出:cout << a << b;,实际调用的是:operator << (operator << (cout,a),b);operator <<的返回是std::ostream的引用(即cout)。
  • 重载<<
ostream & operator << (ostream &out, const 类名 &c)
{
  out << "(" << c.real << "," << c.image << ")";
  return out;
}
  • 将操作符重载为非成员函数后,所有操作数都要形参列表提供(后置++--除外,要多一个int

Chap9 模板与群体数据

  • 重载=与下标运算符[]需要返回引用(使其可以作为左值被修改)
  • 要实现深层复制,必须自定义复制构造函数。而一般自定义了复制构造函数,也需要重载=
  • 模板类函数定义:template <class T> Array <T>::Array(){}
  • 复制时两边一定一样,赋值时可能不一样
  • 重载指针转换运算符,实现将对象名转换为对应类型指针。不能有返回值类型(void都不能有)和参数表
  • 数组
  • 链表
  • 队列
  • 类模板定义容器时,容器大小最好放在模板参数里(与类型一起),这样可读性、一致性更好。
  • 几种简单的排序算法

Chap10 泛型程序设计与STL

  • 泛型程序设计与面向对象程序设计是不同的概念,是不同角度的抽象
  • 术语:concepts。STL库中,用concepts来界定类型,如可比较的Comparable;具有公有复制构造函数、可进行赋值的Assignable
  • 如果两个concepts间具有从属关系,例如SortableComparableAssignable为前提,后两者需求的所有功能,是前者所需求功能的一部分,则称Sortable为后两者的子概念。
  • 术语:model。符合一个concepts的数据类型,被称为model。例如,intComparable的一个模型;静态数组(不是static的静态)不是Assignable的模型。
  • 使用STL中的模板时,类型既可以是已存在的,也可以是自定义的,只要它们是模板所要求conceptsmodel
  • STL的基本组件:
    • container:容器
    • iterator:迭代器
    • function object:函数对象
    • algorithms:算法
  • 迭代器是容器和算法之间的桥梁,算法并不直接以容器作为参数,而是将迭代器作为参数,通过迭代器访问元素,且通过迭代器处理算法的输出结果(存入新的容器或者存入原先的容器)。如此,实现算法的通用性,不仅对数据类型不敏感(模板实现),且对数据存储形式不敏感(容器+迭代器实现)
  • 函数也以对象形式传递给算法作为参数,而不是将函数作为算法的一部分。

container

分类

  • 指容纳、包含一组元素的对象
  • 按容器中元素的组织方式分:顺序容器与关联容器。
  • 按与容器关联的迭代器类型划分:可逆容器,随机访问容器(属于可逆容器)。
  • 基本容器类模板:
    • 顺序容器:array, vector, deque(双端队列), forward_list(单链表)
    • (有序)关联容器:set, multiset(多重集合), map(映射), multimap(多重映射)
    • 无序关联容器:unordered_set, unordered_multiset(无序多重集合), unordered_map(无序映射), unordered_multimap(无序多重映射)
  • 容器适配器:stack, queue, priority_queue(优先队列)。容器适配器是对基本容器附加限制或扩展功能而来的。
  • 使用容器,需要包含对应的头文件

通用功能

基本

  • 用默认构造函数构造空容器。
  • 支持关系运算符:==!=<<=>>=
  • 使用begin()end()获得容器首、尾迭代器。
  • 使用cbegin()cend()获得容器首、尾常迭代器,不需改变容器内容时更安全。
  • clear():清空容器。
  • empty():判断容器是否为空。
  • size():得到容器元素个数。
  • s1.swap(s2):将s1s2两容器内容交换。

相关数据类型(S表示容器类型)

  • S::iterator:指向容器元素的迭代器类型。
  • S::const_iterator:指向容器元素的常迭代器类型

以上获取到的迭代器类型可用于定义、声明迭代器。如:list<int>::iterator iter = l.begin();

对可逆容器的访问

STL为每个可逆容器都提供了专门的逆向迭代器:

  • rbegin():指向容器尾的逆向迭代器。
  • rend():指向容器首的逆向迭代器。

逆向迭代器的类型名表示如下:

  • S::reverse_iterator
  • S::const_reverse_iterator

顺序容器

  • 包括vectordequelistforward_listarray
  • 元素线性排列,可随时在指定位置插入、删除元素。
  • 符合Assignable这一概念。
  • array大小固定,forward_list有特殊的添加和删除操作。

构造函数

  • 默认构造函数
  • S s(n,t);,构造包含nt的容器实例s
  • S s(n);,构造包含n个元素的容器实例s,每个元素都是T(),即T型默认初始化。
  • S s(q1,12);,将[q1,q2)区间内的数据作为s的元素构造s。(比如传入数组名数组名+n

赋值函数

与构造函数形式一一对应

  • s.assign(n,t)
  • s.assign(n)
  • s.assign(q1,q2)

插入函数

关键字insertemplace

插入一个或多个元素,也可插入一个迭代器区间内的元素。

若干形式:

都是插入到p1p1 - 1之间

  • s.insert(p1,t)
  • s.insert(p1,n,t)
  • s.insert(p1,q1,q2)
  • s.emplace(p1,args):将参数args传递给T的构造函数来构造新元素t,然后插入。

Vector

  • 实际上是一个可扩展的动态数组
  • 随机访问,在尾部插入、删除操作快。
  • 在头部插入、删除慢。
  • Vector容量是实际为其分配的容量,使用s.capacity()获取。
  • s.reserve(n),在s容量小于n时扩展容量到n,否则不操作。
  • s.shrink_to_fit(),回收未使用的元素空间,使得sizecapacity相等。

deque 双端队列

  • 在两端插入、删除快,中间慢。
  • 随机访问快,但比vector慢。

list

  • 在任意位置插入、删除都快
  • 无法随机访问(链表实现)
  • splice接合操作,s1.splice(p,s2,q1,q2),将s2[q1,q2)移动到s1p所指元素之前。
  • 接合类似剪切

forward_list

  • 单向链表每个结点只有指向下一个结点的指针,没有简单方法来获取一个结点的前驱。
  • 没有定义insertemplaceerase,而定义了insert_afteremplace_aftererase_after
  • 不支持size操作。

array

  • 是对内置数组的封装,提供更安全、方便使用数组的方式。
  • 对象大小固定,定义时除了指定类型,还要指定大小。
  • 不能动态地改变容器大小。

比较

随机访问 插入删除
vector 大量 仅在尾部
deque 不算太多 两端都要
list 不需要 中间需要
array 相比内置数组是更好的选择

iterator

  • 迭代器是泛化的指针。实际上指针本身就具有同样的特性,所以指针本身就是一种简单的迭代器。
  • 迭代器提供了顺序访问容器中每一个元素的方法。
  • 可以使用++运算符来获得指向下一个元素的迭代器。
  • 有些迭代器还能够使用--运算符获得指向上一个元素的迭代器。
  • 可以使用*运算符访问一个迭代器指向的元素,如果元素类型是类或者结构体,还可以使用->运算符直接访问该元素的一个成员。
  • 要使用独立于STL容器的迭代器,需要包含头文件<iterator>
  • 迭代器是“封装起来的指针”,无法通过迭代器获取元素的真实地址,只是从逻辑上去访问。
  • 每个容器都有责任和义务,去生成一个能够遍历自己各个元素的迭代器,将这个迭代器送给算法去处理元素中的数据。
  • 迭代器是算法和容器之间的桥梁。用于访问容器内的元素,算法不直接操作容器中的数据,而是通过迭代器间接操作。
  • 实现了算法和容器的独立。增加新的算法,无需更改容器的实现;增加新的容器,原有的算法也能适用。

输入、输出流迭代器

  • 输入流迭代器:
    • istream_iterator<T>
    • 以输入流,如cin,为参数构造。
    • 可用*(p++)获得下一个输入元素。
    • 构造无参数的输入流迭代器,该迭代器指向输入流的结束。(不同系统的输入流结束标记不同,win中为Ctrl+z
  • 输出流迭代器:
    • ostream_iterator<T>
    • 以输出流,如cout,为参数构造。
    • 可用*(p++)=x,将x输出到输出流。

迭代器的区间

  • 两个迭代器表示一个区间[p1,p2)
  • STL算法常以迭代器的区间作为输入,传递输入数据。
  • 合法区间应满足:p1经过n次(n>0)自增(++)操作后,满足p1 == p2
  • 区间左闭右开,含左不含右。

迭代器辅助函数

  • advance(p,n),对p执行n次自增操作。
  • distance(first,last),计算两迭代器间的距离,即对first执行多少次++操作后能够使得first == last

顺序容器的插入迭代器

  • 用于向容器头部、尾部、中间指定位置插入元素的迭代器
  • 包括front_inserter前插,back_inserter后插和inserter任意插入迭代器。
  • 实例:
list<int> s;
back_inserter iter(s);
*(iter++) = 5;

顺序容器的适配器

以顺序容器为基础,构建一些常用数据结构,是对顺序容器的封装

  • stack
  • queue队列
  • priority_queue优先级队列:最“大”的元素最先被弹出
  • 对于栈模板,除了给出元素类型,还需给定任一顺序容器作为基础容器,否则默认使用双端队列。
  • 队列模板类似,但要给定的是允许前插的顺序容器(双端队列或列表),默认也使用双端队列。
  • 优先级队列的基础容器必须支持随机访问,默认使用vecotr
  • 栈、队列、优先级队列共同支持的操作:
    • s1 op s2,对两个容器适配器之间的元素按字典序进行比较。(优先级队列不支持)
    • s.size()
    • s.empty()
    • s.push()
    • s.pop()
  • 栈还支持s.top()返回栈顶元素的引用。仅返回,不删除。
  • 队列支持s.front()s.back()返回队列头尾元素的引用。仅返回,不删除。
  • 优先级队列也提供top()函数,返回下一个要被弹出的元素的引用。仅返回,不删除。

函数对象

  • 一个行为类似函数的对象,对它可以像调用函数一样调用。
  • 函数对象是泛化了的函数,任何普通函数和任何重载了()运算符的类的对象,都可以作为函数对象使用。
  • 使用STL的函数对象,需要包含头文件<functional>

算法

  • 可以广泛用于不同的对象和内置的数据类型
  • STL包含七十多种算法,若需使用,需要包含头文件<algorithm>
  • 特点:
    • 通过迭代器获得输入数据
    • 通过函数对象对数据进行处理
    • 通过迭代器将结果输出
    • STL算法本身就是一种函数模板,是通用的,独立于具体的数据类型、容器类型。
  • 分类:
    • 不可变序列算法:不改变原序列
    • 可变序列算法:改变序列
    • 排序和搜索算法
    • 数值算法:加减乘除等

Chap 11 流类库与输入输出

将“数据在不同对象间的流动”抽象为“流”。

从流中读取——提取,写——插入。

输出流类 输入流类
ostream istream
ofstream ifstream
ostringstream istringstream

输出流

ostream类对象:

  • cout标准输出流
  • cerr标准错误输出流,没有缓冲,发送给它的内容被立即输出。
  • clog类似cerr,但有缓冲,在缓冲区满时被输出。

通过coutcerrclog输出的内容,在默认情况下都会输出到屏幕,标准输出和标准错误输出的区别在发生输出重定向时会显露出来。执行程序时可以在命令行使用>对标准输出进行重定向,这会使得通过cout输出的内容写到重定向的文件中,而通过cerr输出的内容仍然输出到屏幕;使用2>可以对标准错误输出重定向,而不会影响标准输出。

构造文件输出流

  1. 使用默认构造函数,然后调用open成员函数,如:
ofstream myFile; //定义对象
myFile.open("filename"); //打开文件,使流对象与文件建立联系
  1. 也可以在定义时调用构造函数指定文件名,如:
ofstream myFile("filename");
  1. 或者使用一个流先后打开不同文件,但在同一时刻只有一个打开的文件,如:
ofstream file;
file.open("file1");
//……输出操作
file.close();
file.open("file2");
//……输出操作
file.close();

使用插入运算符和操纵符

使用操纵符,要包含头文件<iomanip>

  • 输出宽度:可在流中放入stew()操纵符,或调用width()成员函数(可使用fill()成员函数指定引导填充字符),如:cout.width(10);cout << content << endl;cout << setw(6) << content << endl;。两种方法都只影响紧随其后的域,在一个域输出完后域宽度恢复默认。而其他流格式选项一般保持有效,直到发生改变。
  • 对齐方式进制:使用操纵符setiosflags()(定义在头文件iomanip中),搭配参数实现自定义对齐方式。它的影响一直保持有效,直到使用resetiosflags()(参数与set对应相同)重新恢复默认值为止。
  • 精度:使用操纵符setprecision()。此外,setiosflags()中也有ios_base::fixedios_base::scientific指定科学格式和定点格式。浮点数默认输出精度是6位。如不指定fixedscientific,默认六位有效数字,如指定任一一项,则为小数点后六位。
  • 注意:操纵符可以放在插入运算符间使用,故不仅可以在标准输出流中使用,文件输出流、字符串输出流亦可。

文件输出流成员函数

共有三种类型:

  • 与操纵符等价的成员函数
  • 执行非格式化写操作的成员函数
  • 其他修改流状态且不同于操纵符或插入运算符的成员函数

对于顺序的格式化输出,可以仅使用插入运算符和操纵符。对于随机访问二进制磁盘输出,使用其他成员函数,可以使用或不使用插人运算符。

  1. open()函数可以指定打开模式,如:ofstream file("filename", ios_base::out | ios_base::binary);ofstream file;file.open("filename", ios_base::out | ios_base::binary),其他参数自行查阅。
  2. close()函数最好在文件不用后调用一次。
  3. put()函数把一个字符写到输出流中,与cout相比,它不受流的格式化参量影响。
  4. write()函数将内存中内容写到文件输出流中,长度参数指出写的字节数。如:
#include<fstream>
using namespace std;
struct Date {
int monday, day, year;
};
int main() {
  Date dt=[6, 10, 92];
  ofstream file("date.dat", ios_base::binary);
  file.write(reinterpret_cast<char * >(&dt), sizeof(dt));
  //指定从dt起始地址开始,长度为dt的长度
  //此方法写入二进制数据。如要写入文本数据,请使用插入运算符。
  file.close();
  return O;
}
  1. seekptellp
  2. 错误处理函数

文件流输出默认为文本模式,而win和linux下文本文件的行分隔符不太一样。linux下是一个换行符\n,而win下是换行符加一个回车符\n\r。所以,win中在以文本模式输出一个换行符,会追加输出一个\r。如果想避免这个问题,需要在打开文件时使用二进制输出模式。

注意:如果某些内容只是用于中转,而非以阅读为目的输出到文件中,那么使用二进制模式最好,可以缩短存盘时间,节省性能。

C++不支持对象的序列化,即将整个对象不转换格式,直接以二进制方式写入到文件中去,这样写入的数据再读出来是没用的。但结构体是可以的,所以一般用结构体来存储简单数据。

字符串输出流

输出流不仅可以用于输出信息,也可以用于生成字符串,如:

#include <iostream>
#include <sstream>
#include <string>
using namespace std;
template <class T>
inline string tostring(const T &v) {
  ostringstream os;
  //创建字符串输出流
  os << v;
  //将变量 ⅴ的值写人字符申流
  return os.str();
  //返回输出流生成的字符申
}

输入流

  • 构造输入流对象的方式与构造输出流对象基本一样。
  • 提取(extraction)运算符>>用于格式化文本输入,以空白符为分隔。所以如果输入文本包含空格,应该使用非格式化输入成员函数getline,再进行分析。同样,输入流也有错误处理函数。
  • 定义在ios_base类和头文件iomanip中的操纵符可用于输入流,但只有少数操纵符有效,如比较重要的decocthex
  • 相关函数:
    • open
    • close
    • 非格式化get函数与提取运算符类似,但可以正常读入包含空白字符的数据。如:cin.get()
    • 成员函数getline允许指定输入终止字符,默认换行符,读取完成后会从内容中删去该终止字符。但该函数将输入结果存在字符数组中,数组大小无法自动扩展,使用上不方便。而非成员函数getline将内容存在string对象中,更方便。其有三个参数,第一个是输入流,第二个是保存结果的对象,第三个是终止字符,默认换行符。非成员的getline在头文件string中。如:`getline(cin,myLine,'t');,需要注意,getline每次只能读入一行,且得到的字符串中不含换行符。可通过while(getline())来逐行读取文件。
    • read成员函数从文件读字节到指定存储器区域,长度参数指定要读的字节数。如:ifstream myInFile("filename");myInFile.read(接收变量,sizeof(接收变量));
    • seekgtellg

字符串输入流

与字符串输出流类似,字符串输入流从字符串读取数据。istringstream类有两个构造函数,比较常用的一个接收两个参数,分依次是要输入的string对象和流的打开模式,默认为ios_base::in模式。如:string str=abc;istringstream is(str);

除了openclose外,ifstream类具有的成员函数,istringstream类基本都有。

字符串输入流的一个典型用法,是将字符串转换为数值:

template <class T>
inline T fromstring(const string &str) {
  istringstream is(str);
  //创建字符串输人流
  T v;
  is>>V;
  //从字符串输入流中读取变量 v
  return v;
  //返回变量v
}

输入输出流

一个iostream对象可以是数据的源或目的,由iostream派生出fstreamstringstream,这两个类继承了istreamostream类的功能。

fstream类支持磁盘文件的输入和输出,如果需要在同一个程序里在一个磁盘文件中读写数据,可以构造fstream对象。一个fstream对象有输入、输出两个逻辑子流。

stringstream也类似。

Chap 12 异常处理

为什么要引发异常?因为发现错误的函数一般处理不了错误,所以引发异常,让它的调用者捕获异常并处理。即便上一层调用者处理不了,还可以继续引发异常,让更高层调用者捕获和处理。

这一机制让底层函数集中于解决问题,而不必考虑对异常的处理。

在C++中,使用try(代码保护段)、throw(抛出异常)和catch(异常处理)语句作为异常处理机制。

异常处理过程

  1. 程序正常执行到try
  2. 如果try内没有引起异常,则try后的catch不执行。
  3. 如果执行到throw表达式,此处即为异常抛出点,一个异常对象会被创建。如果异常抛出点在try内,则按序执行try后的catch,匹配异常类型并处理。如果异常抛出点不在try内,或catch没能匹配到,则结束当前函数的执行,将当前函数的调用点作为异常抛出点,重复过程,直到异常被成功捕获。
  4. 如果异常始终未被成功捕获,结束main函数执行,自动调用库函数terminate,其默认功能是终止程序。
  5. 如果匹配到catch,则执行其中的处理部分。执行完毕后,该部分的trycatch块执行完毕。

当以下条件之一成立,则异常类型匹配成功:

  • catch中声明的异常类型,就是抛出异常对象的类型或其引用
  • catch中声明的异常类型,就是抛出异常对象的类型的公共基类或其引用
  • catch中声明的异常类型和抛出异常类型都是指针类型,且后者可以隐含转换为前者

此外,catch(...)可以处理任何类型的异常,在顺序中应放在最后一个,否则其后的catch都不会被检查。

异常接口声明

为了增强程序可读性,可以在函数声明时列出该函数可能抛出的所有异常类型,如:void func() throw(A,B,C);

一旦如此声明,则函数func()能且只能抛出A,B,C三种类型的异常。

如没有异常接口声明,则可以抛出任何类型的异常。

如声明了异常接口,但列表为空,则该函数不能抛出任何类型的异常。

如果函数抛出了它的异常接口声明所不允许的异常,函数unexpected将被调用,其默认功能是调用库函数terminate终止程序。用户亦可定义自己的unexpected函数。

异常处理中的构造和析构

catch中的异常声明有值参数和引用参数两种,前者复制被抛出的异常对象,后者使引用指向异常对象。

C++异常处理的真正能力,不仅在于处理不同类型的异常,还在于为异常抛出前构造的所有局部对象自动调用析构函数的能力。

异常被抛出后,栈的展开过程就开始了。从进入try到异常被抛出前,这期间在栈上构造的,且尚未析构的所有对象,都会被自动析构,其顺序与构造顺序相反。这一过程称之为unwinding(解旋)。

标准程序库异常处理

C++标准库提供了一组标准异常类,这些类都派生自基类Exception

虽然C++不禁止抛出Exception派生类外的异常对象,但习惯上还是这样做,即直接使用标准库中的异常类,或从其派生出的类。标准库异常类请自行查阅。

logic_errorruntime_error两个类及其派生类,都有一个接收const string &型参数的构造函数,在构造异常对象时要将错误信息传递给该函数。并且,可以调用该对象的what函数,得到构造该异常对象时提供的错误信息。

最后,C++标准库对异常处理做出如下保证:C++标准库在面对异常时,保证不会发生资源泄露,也不会破坏容器的不变特性。

异常安全

具有异常安全性的函数,在异常发生时,不应泄露任何资源,也不能使任何对象陷入非法状态。

在编写异常安全函数时,要明确哪些操作绝对不会抛出异常。不会抛出异常的操作,是异常安全函数的基石。

一种通用技巧是,先计算,再使用swap交换。因为在函数中只有swap是对最终对象进行修改的操作,而STL中的各种swap函数和swap函数模板都不会抛出异常。所以,用户在特化swap模板,或编写自己的交换函数时,也要确保不抛出异常,为编写其他异常安全函数提供良好基础。

另外,还要确保析构函数不抛出异常。因为在异常抛出到进入捕获异常的cath前,会有一些栈内的对象被析构,如果此时析构函数也抛出新的异常,异常处理机制就会无法工作,自动调用terminate终止程序。一般而言,deletedelete[]是不会抛出异常的,所以应该避免在析构函数中出现逻辑错误。

智能指针

要避免发生资源泄露,就最好将一切资源包装成对象,使得其资源可以通过自动调用析构函数来释放。而如果用new,则需要考虑各种情况,在不同位置手动使用delete释放。

但是有时候,我们不可避免地要使用new(要求灵活性时),此时可以使用C++标准库中的智能指针auto_ptr(头文件memory)。简单来讲,智能指针是一个封装好的指针对象,调用时要关联到一个普通指针,否则默认为空。要注意,智能指针只能关联到指向堆对象的指针,不能关联到指向其他对象的指针,也不能关联到new动态分配的数组,否则在析构时会出问题。

  • 可以使用get成员函数得到智能指针关联的指针,但一般用不到。由于*->运算符已经被重载,智能指针可以像普通指针一样被使用。
  • 可以使用成员函数reset改变智能指针关联的指针。前一个指针会被自动delete
  • 可以使用成员函数release解除智能指针的关联,并返回原来关联的指针,但不会对普通指针进行delete操作。
  • 智能指针之间可以赋值,也可以用一个智能指针构造另一个智能指针。但需要注意,这两种操作都会使得旧智能指针解除与当前指针的关联。因为同一时间至多能有一个智能指针负责一个普通指针的删除。

noexcept 异常说明

有的函数不会抛出异常,为了避免不必要的开销,可以为这类函数添加noexcept说明,标识函数不会抛出异常。如:void func() noexcept;

需要注意,对于使用了noexcept说明的函数,即使函数定义中包含了throw语句,或其他可能抛出异常的函数,编译也不会报错,而执行中一旦遇到此类异常抛出,程序将会异常终止。所以该说明需要谨慎使用。

为了保证函数调用间异常处理的一致性,可以使用noexcept()运算符,其作用是判断函数是否使用了noexcept说明。

void func() noexcept(true) //等价于 void func() noexcept; 不会抛出异常
void func() noexcept() //等价于 void func(); 可能会抛出异常
noexcept(f()); //f函数如果有 noexcept 声明,f本身不包含 throw 语句
               //且f调用的其他函数均有 noexcept 说明,则返回 true,否则返回 false

通过noexcept()的使用,保证了函数间异常说明的一致性:

int func1() noexcept(noexcept(func2()));
//func1 异常说明与 func2一致