这是本博客有史以来最长的一篇文章,总结了我从大一至今,遇到的几乎所有不易理解或是容易忘记的C++知识,可作为手册查阅,内容参考自清华大学郑莉教授的C++课程。
内联函数
声明时使用关键字 inline
编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销
注意:
内联函数体内不能有循环语句和switch语句
内联函数的定义必须出现在内联函数第一次被调用之前
对内联函数不能进行异常接口声明
定义内联函数,可以显式用inline声明,也可以直接在类内定义好实现
编译器并不一定遵从我们的inline
constexpr函数
constexpr修饰的函数在其所有参数都是constexpr时,一定返回constexpr
函数体中必须有且仅有一条return语句
constexpr的变量的值必须是编译器在编译的时候就可以确定的
|
|
重载函数
通过形参的个数不同或者类型不同进行区分
无法通过返回值区分
构造函数
默认构造函数
|
|
隐含生成的构造函数
如果程序中未定义构造函数,编译器将在需要时自动生成一个默认构造函数
参数列表为空,不为数据成员设置初始值
如果类内定义了成员的初始值,则使用类内定义的初始值
如果没有定义类内的初始值,则以默认方式初始化
基本类型的数据默认初始化的值是不确定的
=default
如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用=default
|
|
委托构造函数
类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的,这时,为了避免代码重复,可以使用委托构造函数
不使用委托构造函数:
|
|
使用委托构造函数:
|
|
复制构造函数
复制构造函数是一种特殊的构造函数,其形参为本类的对象引用,作用是用一个已存在的对象去初始化同类型的新对象
定义一个对象时,以本类另一个对象作为初始值,发生复制构造
如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造
如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造
隐含的复制构造函数
如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个隐含的复制构造函数
这个构造函数执行的功能是:用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员(浅拷贝)
=delete
C++11做法:用=delete
指示编译器不生成默认复制构造函数。
|
|
类的组合
构造组合类对象时的初始化次序
首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序
成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造
初始化列表中未出现的成员对象:调用用默认构造函数(即无形参的)初始化
处理完初始化列表之后,再执行构造函数的函数体
前向引用声明
如果需要在某个类的声明之前,引用该类,则应进行前向引用声明
前向引用声明只为程序引入一个标识符,但具体声明在其他地方
|
|
使用前向引用声明虽然可以解决一些问题,但它并不是万能的
在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象
当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节
|
|
联合体
成员共用同一组内存单元
任何两个成员不会同时有效
|
|
枚举类
|
|
枚举类的优势
强作用域,其作用域限制在枚举类中
转换限制,枚举类对象不可以与整型隐式地互相转换。
可以指定底层类型
|
|
类的友元
友元是C++提供的一种破坏数据封装和数据隐藏的机制
通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息
为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元
友元函数
友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和protected成员
作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择
访问对象中的成员必须通过对象名
友元类
若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员
声明语法:将友元类名在另一个类中使用friend修饰说明
类的友元关系是单向的
如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据
常类型
对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)
const关键字可以被用于参与对重载函数的区分
通过常对象只能调用它的常成员函数
|
|
常成员函数可以被非常对象调用,但常对象不可调用非常成员函数
多文件结构
外部变量
如果一个变量除了在定义它的源文件中可以使用外,还能被其它文件使用,那么就称这个变量是外部变量
文件作用域中定义的变量,默认情况下都是外部变量,但在其它文件中如果需要使用这一变量,需要用extern关键字加以声明
外部函数
在所有类之外声明的函数(也就是非成员函数),都是具有文件作用域的
这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的
编译预处理指令
预处理在编译前进行
每条预处理指令必须单独占用一行
预处理指令可以出现在程序的任何位置
指针
空值nullptr
以往用0或者NULL去表达空指针的问题:
C/C++的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C++的时代,这就会引发很多问题
C++11使用nullptr关键字,是表达更准确,类型安全的空指针
指向常量的指针
不能通过指向常量的指针改变所指对象的值,但指针本身可以改变,可以指向另外的对象。
|
|
指针类型的常量
若声明指针常量,则指针本身的值不能被改变。
|
|
函数指针
|
|
智能指针
显式管理内存在是能上有优势,但容易出错
C++11提供智能指针的数据类型,对垃圾回收技术提供了一些支持,实现一定程度的内存管理
- unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针
- shared_ptr :多个指针共享资源
- weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响
移动构造
移动构造可以减少不必要的复制,带来性能上的提升
C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。在某些情况下,我们没有必要复制对象——只需要移动它们
有可被利用的临时对象时,触发移动构造
|
|
左值和右值
左值和右值都是针对表达式而言的
左值是指表达式结束后依然存在的持久对象
右值指表达式结束时就不再存在的临时对象——显然右值不可以被取地址
读入字符串
用cin的>>操作符输入字符串,会以空格作为分隔符,空格后的内容会在下一回输入时被读取
getline可以输入整行字符串(要包string头文件),例如:getline(cin, s2);
输入字符串时,可以使用其它分隔符作为字符串结束的标志(例如逗号、分号),将分隔符作为getline的第3个参数即可,例如:getline(cin, s2, ',');
|
|
继承
公有继承(public)
继承的访问控制
- 基类的public和protected成员:访问属性在派生类中保持不变
- 基类的private成员:不可直接访问
访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
- 通过派生类的对象:只能访问public成员
私有继承(private)
继承的访问控制
- 基类的public和protected成员:都以private身份出现在派生类中
- 基类的private成员:不可直接访问
访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
- 通过派生类的对象:不能直接访问从基类继承的任何成员
保护继承(protected)
继承的访问控制
- 基类的public和protected成员:都以protected身份出现在派生类中
- 基类的private成员:不可直接访问
访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
- 通过派生类的对象:不能直接访问从基类继承的任何成员
protected 成员的特点与作用
- 对建立其所在类对象的模块来说,它与 private 成员的性质相同
- 对于其派生类来说,它与 public 成员的性质相同
- 既实现了数据隐藏,又方便继承,实现代码重用
|
|
派生类的构造函数
默认情况
- 基类的构造函数不被继承
- 派生类需要定义自己的构造函数
C++11规定
可用using语句继承基类构造函数
但是只能初始化从基类继承的成员
派生类新增成员可以通过类内初始值进行初始化
语法形式:
using B::B;
多继承且有对象成员时派生的构造函数定义语法
|
|
构造函数的执行顺序
调用基类构造函数
顺序按照它们被继承时声明的顺序(从左向右)
对初始化列表中的成员进行初始化
顺序按照它们在类中定义的顺序
对象成员初始化时自动调用其所属类的构造函数,由初始化列表提供参数
执行派生类的构造函数体中的内容
派生类复制构造函数
派生类未定义复制构造函数的情况
编译器会在需要时生成一个隐含的复制构造函数
先调用基类的复制构造函数
再为派生类新增的成员执行复制
派生类定义了复制构造函数的情况
一般都要为基类的复制构造函数传递参数
复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数
基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
例如: C::C(const C &c1): B(c1) {…}
派生类的析构函数
析构函数不被继承,派生类如果需要,要自行声明析构函数
声明方法与无继承关系时类的析构函数相同
不需要显式地调用基类的析构函数,系统会自动隐式调用
先执行派生类析构函数的函数体,再调用基类的析构函数
访问从基类继承的成员
当派生类与基类中有相同成员时:
- 若未特别限定,则通过派生类对象使用的是派生类中的同名成员
- 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定
如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,“派生类对象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题
- 解决方式:用类名限定
虚基类
需要解决的问题
- 当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性
虚基类声明
- 以virtual说明基类继承方式
- 例:
class B1:virtual public B
作用
- 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
- 为最远的派生类提供唯一的基类成员,而不重复产生多次复制
注意:
- 在第一级继承时就要将共同基类设计为虚基类
虚基类及其派生类构造函数
建立对象时所指定的类称为最远派生类
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的
在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数
在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略
|
|
运算符重载
双目运算符重载规则
如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型
经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operator B(oprd2)
|
|
前置单目运算符重载规则
如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。
经重载后,表达式 U oprd 相当于 oprd.operator U()
后置单目运算符 ++和–重载规则
如果要重载 ++或–为类成员函数,使之能够实现表达式 oprd++ 或 oprd– ,其中 oprd 为A类对象,则 ++或– 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
|
|
运算符重载为非成员函数
有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象
运算符重载为非成员函数的规则
- 函数的形参代表依自左至右次序排列的各操作数
- 参数个数=原操作数个数(后置++、–除外)
- 至少应该有一个自定义类型的参数
- 后置单目运算符 ++和–的重载函数,形参列表中要增加一个int,但不必写形参名
- 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元
- 双目运算符 B重载后,表达式oprd1 B oprd2等同于operator B(oprd1,oprd2 )
- 前置单目运算符 B重载后,表达式 B oprd等同于operator B(oprd )
- 后置单目运算符 ++和–重载后,表达式 oprd B等同于operator B(oprd,0 )
|
|
虚函数
- 用virtual关键字说明的函数
- 虚函数是实现运行时多态性基础
- C++中的虚函数是动态绑定的函数
- 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态
- 一般成员函数可以是虚函数
- 构造函数不能是虚函数
- 析构函数可以是虚函数
- 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候
- 在派生类中可以对基类中的成员函数进行覆盖
- 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的
virtual 关键字
- 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
- 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型
- 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值
- 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
- 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式
- 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性
虚析构函数
为什么需要虚析构函数? - 可能通过基类指针删除派生类对象; - 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的
|
|
虚表与动态绑定
虚表
- 每个多态类有一个虚表(virtual table)
- 虚表中有当前类的各个虚函数的入口地址
- 每个对象有一个指向当前类的虚表的指针(虚指针vptr)
动态绑定的实现
- 构造函数中为对象的虚指针赋值
- 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
- 通过该入口地址调用虚函数
|
|
抽象类和纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表) = 0;
带有纯虚函数的类称为抽象类
抽象类作用
- 抽象类为抽象和设计的目的而声明
- 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为
- 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现
注意:
- 抽象类只能作为基类来使用。
- 不能定义抽象类的对象。
|
|
override
C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。 - 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误
- 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
- 覆盖要求: 函数签名(signatture)完全一致
- 函数签名包括:函数名 参数列表 const
final
C++11提供final,用来避免类被继承,或是基类的函数被改写 例:
|
|
模板
函数模板
语法形式:
template <模板参数表>
模板参数表的内容:
- 类型参数:class(或typename) 标识符
- 常量参数:类型说明符 标识符
- 模板参数:template <参数表> class标识符
注意:
- 一个函数模板并非自动可以处理所有类型的数据
- 只有能够进行函数模板中运算的类型,可以作为类型实参
- 自定义的类,需要重载模板中的运算符,才能作为类型实参
|
|
类模板
使用类模板使用户可以为类声明一种模式,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型的和用户自定义类型)
类模板 template <模板参数表> class 类名 {类成员声明};
如果需要在类模板以外定义其成员函数,则要采用以下的形式: template <模板参数表> 类型名 类名<模板参数标识符列表>::函数名(参数表)
|
|
数组类模板
自己实现一个动态数组
|
|
泛型程序设计与STL
迭代器
迭代器是算法和容器的桥梁
- 迭代器用作访问容器中的元素
- 算法不直接操作容器中的数据,而是通过迭代器间接操作
算法和容器独立
- 增加新的算法,无需影响容器的实现
- 增加新的容器,原有的算法也能适用
|
|
逆向迭代器
- rbegin() :指向容器尾的逆向迭代器
- rend():指向容器首的逆向迭代器
逆向迭代器的类型名的表示方式如下:
- S::reverse_iterator:逆向迭代器类型
- S::const_reverse_iterator:逆向常迭代器类型
函数对象
一个行为类似函数的对象
可以没有参数,也可以带有若干参数
其功能是获取一个值,或者改变操作的状态
普通函数就是函数对象
重载了“()”运算符的类的实例是函数对象
|
|
|
|
I/O流
操纵符(manipulator)
|
|
|
|
|
|
|
|
|
|
|
|
二进制文件流
使用ofstream构造函数中的模式参量指定二进制输出模式或以通常方式构造一个流,然后使用setmode成员函数,在文件打开后改变模式
|
|
字符串输出流( ostringstream )
将字符串作为输出流的目标,可以实现将其他数据类型转换为字符串的功能
|
|
输入流
重要的输入流类
- istream类最适合用于顺序文本模式输入,cin是其实例
- ifstream类支持磁盘文件输入
- istringstream类支持从内存中的字符串输入
|
|
|
|
|
|
|
|
|
|
字符串输入流( istringstream)
将字符串作为文本输入流的源,可以将字符串转换为其他数据类型
|
|
输入/输出流
两个重要的输入/输出流
- 一个iostream对象可以是数据的源或目的
- 两个重要的I/O流类都是从iostream派生的,它们是fstream和stringstream。这些类继承了前面描述的istream和ostream类的功能
fstream类
- fstream类支持磁盘文件输入和输出
- 如果需要在同一个程序中从一个特定磁盘文件读并写到该磁盘文件,可以构造一个fstream对象
- 一个fstream对象是有两个逻辑子流的单个流,两个子流一个用于输入,另一个用于输出
stringstream类
- stringstream类支持面向字符串的输入和输出
- 可以用于对同一个字符串的内容交替读写,同样是由两个逻辑子流构成
异常处理
异常接口声明
一个函数显式声明可能抛出的异常,有利于函数的调用者为异常处理做好准备
可以在函数的声明中列出这个函数可能抛掷的所有异常类型
|
|
若无异常接口声明,则此函数可以抛掷任何类型的异常
不抛掷任何类型异常的函数声明如下:
|
|
|
|
自动的析构
找到一个匹配的catch异常处理后
- 初始化异常参数
- 将从对应的try块开始到异常被抛掷处之间构造(且尚未析构)的所有自动对象进行析构
从最后一个catch处理之后开始恢复执行
|
|