1.OOP
1.1概述
- 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承、动态绑定。
数据抽象(类的基本思想之一,还包括封装)
(后续补充数据抽象)
- 数据抽象是一种依赖于 接口(interface) 和 实现(implementation) 分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
继承
- 通过 继承(inheritance) 联系在一起的类构成一种层次关系。通常在层次关系的根部有一个 基类(base class) ,其他类则直接或间接地从基类继承而来,这些继承得到的类称之为 派生类(derived class) 。基类负责定义在层次关系中所有类共有的成员,而每个派生类定义各自特有的成员。
基类将类型相关的函数与派生类不做任何改变直接继承的函数区分开来。基类将某些函数定义为 虚函数(virtual function) ,用于派生类各自定义适合自己的版本。
class Quote{ public: std::string isbn() const; virtual double net_price(std::size_t n) const; };
关键字virtual只能出现在类内部的声明语句之前而不能出现在类外部的函数定义中。
基类中的声明的virtual虚函数在派生类中也是隐式的虚函数。
派生类必须通过使用 类派生列表 指明它从哪些基类继承过来的。
class Bulk_quote :(冒号) public(访问说明符) Quote(基类名) { public: double net_price(std::size_t n) const override; };
函数参数列表后的 override 显示注明派生类将使用哪个成员函数 重写 基类的 virtual虚函数。除此之外还可以在const成员函数的关键字const后、或者在引用成员函数的引用限定符后添加一个关键字override注明同上的作用。
访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。(详细见 访问控制与继承)
动态绑定
- 当使用基类的引用(或指针)调用一个虚函数时将发生 动态绑定(dynamic binding) 。函数的运行版本由 实参 决定,即运行时选择函数版本,动态绑定又称运行时绑定。
1.2 类派生定义
基类定义
- 基类通常需要定义一个 虚析构函数 用于对析构函数的动态绑定。
virtual ~Quote()=default;
- 被用作基类的类 必须是已经定义的并非仅仅声明
… …(待补充?)
派生类定义
派生类构造函数
- 派生类必须使用基类的构造函数初始化它的基类部分,每个类必须初始化自己的成员。
//构造函数 Bulk_quote(const std::string& book , double p , std::size_t qty , double disc) : Quote(book , p) , min_qty(qty) , discount(disc) { }
其中的Quote(book , p) 注明派生类狗造函数想要使用基类的构造函数可以用 基类名(实参列表) 的形式提供初始值。
首先初始化基类部分,然后按声明顺序初始化派生类成员。
遵循基类的接口 : 每个类负责定义各自的接口。派生类应遵循基类的接口 Quote() 。
继承与静态成员
- 基类中定义一个static静态成员,则在整个继承体系中只存在该成员的唯一定义。静态成员遵循通用的访问控制规则。如基类中的静态成员是private,则派生类无权访问。
派生类的声明
- 派生类的声明中只包含类名但是不含有它的派生列表。一条声明语句的目的是令程序知晓某个名字的存在以及该名字表达一个什么样的实体,如一个类,一个函数,一个变量等。派生列表以及与他定义的其他细节必须与类的主体一起出现。
防止继承的发生final
-
在不想被当作基类的类类名后添加final关键字。
-
override和final说明符 出现在形参列表(任何const 和 引用修饰符)以及 尾置返回类型之后。
类型转换与继承
- 编译器隐性地执行派生类向基类的类型转换:派生类对象中含有其基类对象的组成部分,能把派生类对象当作基类对象使用,而且也能将基类的 引用/指针 绑定到派生类对象中的基类部分上(当使用基类的引用/指针时,实际上并不清楚该引用和指针所绑定对象的真实类型,可能是基类对象,也可能是派生类对象)。
静态类型与动态类型
- 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;
- 动态类型是变量或表达式表示的内存中对象的类型,直至运行时才可知。
C++OOP 的多态性: 表达式中基类的指针或引用的静态类型可能与其动态类型不一致。
编译器只能通过检查指针或引用的静态类型来判断是否合法。可以通过使用dynamic_cast运算符实现运行时类型识别(RTTI),假设已知基类向派生类转换安全可以使用static_cast来强制覆盖掉编译器的检查。
- 当使用一个派生类对象给一个基类对象赋值或初始化时吗,只用基类部分会被保留,其他派生部分将被切掉(sliced down)。
1.3虚函数
- 当且仅当指针或引用调用虚函数时,才会在运行时解析该调用,只用在该情况下对象的动态类型可能与动态类型不同。
派生类中的虚函数
- 关键词override覆盖虚函数规则:
1、一旦某函数被声明为虚函数,则在所有派生类中它都是虚函数。
2、派生类的函数覆盖某个继承而来的虚函数,则它的形参类型必须与基类函数完全一致。
3、派生类中虚函数的返回类型也必须一致。但是存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。
例如:
class D {
D* func(? , ?);
};
class B : public D {
B* func (? , ?);
};
//返回类型要求从D到B的类型转换是可访问的。
- 若派生类定义了一个函数与基类中虚函数的名字相同但参数列表不同,合法行为但编译器认为两者相互独立。但我们原本是希望派生类能覆盖掉基类中的虚函数的,可以使用override标记该派生类的新定义函数,若该被标记的新定义函数 没有覆盖已存在的虚函数,编译器将报错。
回避虚函数机制
- 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符:: 回避虚函数机制,该调用在编译时完成解析。
WARNING : 如果一个派生类函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用( [^] 已覆盖,调用顶层版本),导致无限递归。
1.4访问控制与继承
受(protected)保护的成员
- 一个类使用 protected关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。
- 与私有类似,受保护的成员对于类的用户来说是不可访问的。
- 与公有类似,受保护的成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或友元只能通过派生类对象(规避Protected提供的访问保护)来访问基类的受保护成员,派生类对于一个基类对象的受保护成员没有任何访问特权。
– 规定:派生类成员或友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。
公有、私有和受保护继承
成员访问说明符 公有:类的所有用户都可访问 ; 私有: 类的成员或友元可访问 ; 受保护的: 类的成员、友元和派生类…是可访问的
- 类对其继承而来的成员的访问权限受两个因素影响:一是在基类中该成员的访问说明符,而是在派生类的派生列表中的访问说明符。
- 派生说明符对于派生类的成员(及友元)能否访问直接基类的成员没影响。对基类成员的访问权限只与基类中的访问说明符有关。
- 派生说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生类向基类转换的可访问性
假定D继承自B:
- 只有当D公有的继承B时,用户代码才能使用派生类向基类的转换;
- 无论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;
- 如果D继承B的方式是公有或受保护的,则D的派生类 的成员或友元可以使用D向B的类型转换。(D私有地继承B,对于D的派生类来说D的基类成员是不可访问的)
友元和继承
- 不能继承友元关系;每个类控制各自成员的访问权限。
- 友元关系只对作出声明的类有效,对原类来说,其友元的基类或派生类不具有特殊的访问权限。
- 对基类的访问权限由基类本身控制,即使对于 派生类的基类部分 也是如此。
改变个别成员的可访问性
eg:
class D : private B { public: using B::size; protected: using B::n; };
派生类只能为那些它可访问的名字提供using声明。
默认的继承保护级别
- struct和class关键字的唯一差别是默认成员访问说明符和默认派生访问说明符。
class B {...}; // struct D1 : B {...}; //默认public继承 class D2 : B {...}; //默认private继承
1.5继承中的类作用域
- 派生类的作用域嵌套在基类作用域之内。若一个名字在派生类的作用域内无法正确解析,则编译器将继续在其外层基类作用域内寻找该名字的定义。
在编译时进行 名字查找
- 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
//D继承B,D有一个名为func的成员函数; D d; D *dp = &d; B *bp = &d; dp->func(); bp->func(); // 错误: bp的类型是 B* //dp的静态类型是D,D中的func成员函数是可见的(名字func在该派生类D的作用域内解析) ; bp的静态类型是B,名字func在该基类B的作用域内无法解析,且其外层作用域也无法解析。
名字冲突与继承
- 派生类成员将隐藏同名的基类成员。
- 可以用作用域运算符使用被隐藏的基类成员。(Base::name;)
- 声明在内层作用域的函数将隐藏而不会重载声明在外层作用域的同名实体。不同作用域将无法重载同名函数哪怕形参列表不一致;即派生类成员也只会隐藏基类中的同名成员而不会重载。
- Note:除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类的名字。
关键概念:名字查找与继承
理解函数调用的解析过程对理解C++继承重要,假定调用p->fcn()/obj.fcn(),则执行以下四个步骤:
- 首先确定P的静态类型。
- 在P/OBJ 的静态类型的类中查找名字fcn。(在继承链中向上查找至顶端基类,解析失败则编译器报错)
- 找到了名字fcn,则进行常规的类型检查以确认调用是否合法。(若调用非法,则报错)一旦找到名字 编译器就不会继续查找
- 调用合法,编译器则根据调用的是否是虚函数而作出不同行为:
– fcn是虚函数且通过指针或引用调用,则编译器将在运行时选择虚函数版本,依据对象的动态类型;
– fcn不是虚函数或通过对象调用,则编译器进行普通调用。
虚函数与作用域
- 基类和派生类中的虚函数必须有相同的形参列表(形参列表不同就无法覆盖同名虚函数而只会隐藏并拥有另一个同名函数),假如两者虚函数接受的实参不同,就无法通过基类的引用或指针调用派生类的虚函数。
class B { public: virtual int fcn(); }; class D1 : public B{ public: int fcn(int); virtual void f2(); }; class D2 : public D1{ public: int fcn(int); //隐藏D1的fcn(int) int fcn(); //覆盖B的虚函数fcn void f2(); //覆盖D1的虚函数f2 };
通过基类调用隐藏的虚函数
B bobj ; D1 dobj1 ; D2 dobj2 ; B *bp1 = &bobj , *bp2 = &dobj1 , *bp3 = &dobj2 bp1->fcn(); //调用B::fcn() bp2->fcn(); //调用B::fcn() ; 静态绑定类型D1--> 名字查找-->名字实体为虚函数-->动态绑定 bp3->fcn(); //调用D2::fcn()
覆盖重载的函数
- 派生类可以覆盖重载函数的0个或多个实例,这时为重载的成员提供一条using声明语句可以避免上述规则。
using声明语句指定一个名字而不指定形参列表,一条基类成员函数的using声明语句就可以把该函数目前所有的重载版本实例都添加到使用using声明语句的派生类的作用域中,但是每个实例在派生类中都必须是可访问的。
此时派生类只需要定义自己特定的版本函数,而无需为继承来的所有其他函数重新定义。
1.6构造函数与拷贝控制
虚析构函数
一个基类总是需要一个虚析构函数,在delete一个动态分配对象的指针或引用是执行正确的析构函数版本,之后会各自隐式销毁自己的对象。
3. 虚析构函数阻止合成移动操作(可自定义)
基类没有移动操作也会阻止派生类有自己的合成移动操作版本。
当需要移动操作时应该首先在基类中定义,即显式地定义default合成的版本。同时一旦基类定义了移动操作还必须显式地定义拷贝操作。派生类将自动合成移动操作在无排斥成员存在的情况下。
合成拷贝控制与继承
- 唯一要求是合成拷贝控制成员是可访问的,并且不是一个被删除的函数。
- 派生类隐式地使用合成的析构函数而基类通过将其虚析构函数定义成=default显式使用。
- 派生类的析构函数除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;直接接类又销毁它的直接基类,直至继承链顶端。
- 定义基类的方式可能导致有的派生类成员成为被删除函数:
- 基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或不可访问的,则派生类中对应成员是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的狗造、赋值或销毁操作。
- 基类中有一个不可访问的或删除的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器将无法销毁派生类的基类部分。
- 编译器不会合成一个删除的移动操作。当我们使用=default请求一个移动操作时,如果基类中对应操作时删除的或不可访问的,在派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
派生类的拷贝控制成员
派生类拷贝、移动和赋值操作时也必须为其基类部分的成员做这些。
不同的是析构函数只负责销毁派生类自己分配的资源。(基类部分自动销毁
WARNING:默认情况下,基类默认构造函数初始化派生类对象的基类部分。若是移动或赋值,则必须在派生类的构造函数初始值列表中显式地使用相关函数。D { B::operator=() };
在构造函数和析构函数中调用虚函数
已知:构造函数和析构函数的调用次序 和 在调用这两个函数时对象处于未完成类型。
–如果构造函数和析构函数调用了某个虚函数,则应该执行构造函数和析构函数所属类型相对应的虚函数版本。
继承的构造函数
派生类能重用其直接基类定义的构造函数。一个类只初始化其直接基类,也只重用直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。编译器将为派生类自动合成其未定义的构造函数。
–using声明语句作用于构造函数时令编译器在派生类中生成一个形参列表完全相同的构造函数:derived (parms) : base(args) { } // 将parms传递给args ,自己的数据成员将默认初始化。
特点:
- 不会改变构造函数的访问级别
- 不能指定explicit或constexpr
- 基类构造函数有默认实参时将不会被‘继承’,同时派生类将生成无默认实参的相同形参数量的构造函数和若干个依次去掉一个带有默认实参的形参的函数(数量实参数+1个)。
- 派生类可自定义自己的版本构造函数,当有相同参数列表时,派生类定义的构造函数将替换掉原本要继承的构造函数。
- 默认、拷贝和移动构造函数都不会被继承,按正常规则自己合成。
继承的构造函数将不会作为用户定义的构造函数使用,所以若类只含有继承来的构造函数,则他将拥有一个合成的默认构造函数。
1.7容器与继承
…
1.X 多重继承与虚继承
多重继承是多个直接基类产生派生类的能力。多重继承的派生类继承了所有父类属性。
多重继承
- 多重继承列表只能包含已经定义过的类,而这些类不能是final的。
- 构造一个类的对象将同时构造并初始化它的所有基类子对象。与基类进行派生一样,多重继承的构造函数初始值也只能初始化他的直接基类。
- 基类构造顺序与派生列表中的基类出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。次序:优先初始化最终基类,然后派生列表,最后本类。
- 允许派生类从他的一个或多个基类中继承 构造函数,但如果从多个基类中继承了相同的构造函数(参数列表完全相同),则程序产生错误。解决方法是这个类必须为该构造函数定义自己的版本。
- 派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的。
- 多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。只有当派生类使用的是合成版本的这些操作,才会自动对其基类部分执行这些操作。
- 类型转换
在只有一个基类的情况下,派生类指针或引用能自动转换成一个可访问的基类指针或引用。
编译器不会再派生类向基类的几种转换中进行比较和选择,在他看来都一样好。某些情况下会出现二义性错误,可以用 :: 限定符解决。 - 基于指针或引用类型的查找
与一个基类的继承一样,对象、指针或引用的静态类型决定我们能够使用哪些成员(参见名字查找) - 类作用域
派生类作用域嵌套在直接基类的和间接基类的作用域中。
多重继承下的名字查找是在所有直接基类中 并行 同时进行的。(易产生二义性)使用::限定符解决。
其他错误情况:派生类继承的函数参数列表不同也可能错误,一个函数在一个类中是私有在另一个类中是公有或受保护的同样可能错误。
先查找名字后进行类型检查当编译器在两个作用域同时发现同名,直接报告错误。
避免潜在二义性的最好办法是在派生类中为该函数定义一个新版本。
虚继承
虚继承(virtual inheritance) 的目的是令某个类作出声明,承诺愿意共享它的基类。共享的基类子对象成为 virtual虚基类,在该声明类类的派生列表中指定类前加关键字virtual。
必须在虚派生的真实需求出现前就完成虚派生的操作。
Note:虚派生只影响从指定了虚基类的派生类中进一步派生出的类,不会影响派生类本身。
虚基类成员的可见性
每个共享的虚基类中只有唯一一个共享的子对象,该基类的成员可以被直接访问且不会产生二义性。但是如果成员被多于一个(?虚)基类覆盖,则一般情况下,派生类必须为该成员自定义一个版本。// B定义x成员 D1、D2继承自B D继承自D1和D2 1、D1和D2中未定义x,则x被解析为B的成员; 2、x是D1和D2中某一个成员,派生类的x比共享虚基类B的x优先级更高。 2、D1 和 D2 中都定义x,则在D中直接访问x产生二义性,解决办法最好方法是在派生类中为成员自定义新的实例。或者使用 ::
构造函数与虚继承
- 在虚派生中,虚基类是由 最底层的派生类 初始化的。普通规则初始化,虚基类会在多条继承路径上重复初始化。
- 继承体系中每个类都可能在某时刻成为‘最底层的派生类’。只要我们创建虚基类的派生类对象,该派生类的狗造函数都必须初始化它的虚基类。(正常情况下,派生类初始化直接基类使用各自的构造函数)
- 构造顺序:首先,最底层派生类构造函数的初始值初始化该对象的虚基类子部分,其次,按派生列表次序初始化。(虚基类总是先于非虚基类的狗造)
–如果最底层派生类未显式地初始化虚基类,则调用虚基类的默认构造函数,没有则代码错误。 - 构造函数与析构函数的次序
一个类可以有多个虚基类。虚子对象按照其在派生列表中出现的顺序从左向右一次构造。编译器按照直接基类的声明顺序对其依次检查,以确认其中是否含有虚基类。
- 首先狗造派生列表中 直接基类的虚基类 或者 直接虚基类(派生列表中声明的virtual class), 然后狗造派生列表中的非虚基类按普通规则。
(未完待续…)