C++动态绑定的实现
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这是动态绑定的关键。 用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指向对象的实际类型所定义的。
虚函数: 除了构造函数外,任意非static 成员函数都可以是虚函数。保留字virtual 只在类内部的成员函数声明中出现,不能用在类定义体外部出现的的函数体上。 基类通常应将派生类需要重定义的任意函数定义为虚函数。
访问控制和继承:
public: 无限制
private: 只能是基类的成员和友元可以访问。
protected: 可以被派生类对象访问但不能被该类型的普通用户访问。
protected 成员重要性质:
派生类只能通过派生类对象访问其基类的protected 成员,派生类对基类类型对象的protected成员没有特殊访问权限。
举个栗子:假设 class A_base ; class A ; A_base 是基类, A是 派生类, A 中定义一个成员函数,接受一个A对象的引用 和 一个A_base 对象的引用,该函数可以访问自己的protected 成员以及A 形参的protected成员,但是,不能访问A_base 形参的protected 成员。
派生类
派生类列表: 为了定义派生类,使用派生类列表指定基类。 派生类列表指定了一个或多个基类。
格式如下: class classname : access-label base class
access-label: 是public、protected或private
每个派生类对象包含两部分: 从基类继承的成员 和 自己定义的成员。
派生类与虚函数: 如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类中 虚函数的声明必须与基类中的定义方式
完全匹配
,
返回类型可以不同:
返回对基类型的引用(或指针) 的虚函数,派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)
[后面的clone函数就会说明为什么返回类型如果是指针的话 可以为基类或派生类]
一旦函数在基类中声明为虚函数,就一直是虚函数,派生类无法改变该函数为虚函数的命运。派生类重定义虚函数时, virtual 保留字可用可不用。
派生类对象 包含基类对象 作为子对象。
派生类对象由多个部分组成: 派生类本身定义的(非static)成员加上由基类(非static)成员组成的子对象。
派生类对象 的组成结构:
基类成员 |
派生类成员 |
每个派生类对象都有基类部分,类可以访问其基类的public 和 protected 成员 ,就想访问自己的成员一样
用作基类的类必须是已定义的 (实际中就是派生类的定义文件(头文件) 必须include 基类的定义文件(头文件))
最底层的派生类 对象包含其每个直接基类或间接基类的子对象
派生类的声明:
如果需要声明(并不实现) 一个派生类,则声明包含类名但不包含派生列表。
EX : 如果声明class A :
class A;
class A_base;
virtual:
触发动态绑定的两个条件:
1) 只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数。
2)必须通过基类类型的引用或指针进行函数调用。
1 从派生类到基类的转换:因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到 派生类的基类部分,也可以用指向基类的指针指向派生类对象。
无论实际对象具有哪种类型,编译器都将它当做基类类型对象. 注: 任何可以在基类对象上执行的操作也可以通过派生类对象使用
静态类型和动态类型
静态类型 | 编译时可知的引用类型或指针类型 |
动态类型 | 指针或引用所绑定的对象的类型,这是仅在运行时可知的。 |
概念:
引用和指针的静态类型与动态类型可以不同!
对象是非多态的,对象类型已知且不变。对象的动态类型总是与静态类型相同。
只有通过引用或指针调用,虚函数才在运行时确定。
编译时确定非 virtual 调用
: 非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
EX : class A_base ; class A;
A_base 中定义了非虚函数 void fun() ,
A 中也定义了 void fun();
A_base & ab = *(new A());
即定义A_base 的引用,引用的对象是A ,但是如果执行 ab.fun() ,也是调用基类的func ,指针也相同。
覆盖虚函数机制:
情况: 希望覆盖虚函数机制 并 强制函数调用使用虚函数 的特定版本,可以使用作用域操作符:
EX: 假设 A_base 定义了虚函数 :virtual int getValue(){};
A 重定义了 getvalue() : int getValue(){};
A aClass;
A_base *baseP = &aClass;
int ans=baseP->A_base::getValue();
注意: 只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制
注意: 派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略作用域操作符,函数调用会在运行时确定并且将是一个自身调用,导致无穷循环
虚函数与默认实参: 虚函数中的
默认实参值 是在编译时确定的
。所以在同一虚函数的基类版本和派生类版本中使用不同的默认实参会引起麻烦。(就会用基类的默认参数值~~~ 与期望的值不同)
访问控制, 公用,私有 和 受保护
每个类控制他所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。
无论 派生列表中是什么 访问标号, 所有继承基类的 类 对基类的成员具有相同的访问。 派生访问标号将控制派生类的用户对从 基类继承而来的成员的访问。
派生访问标号 还控制 来自 非直接派生类 的访问。
其实就是派生访问标号 在什么时候 有作用:
1) class A : public A_base {}
class AA : private A_base{};
则 在类 A 和 类AA 对 A_base 的成员具有相同的访问; 而 A 和 AA 的具体对象对A_base 的访问 权限则不同。
2) class AAA : public AA; AAA 则不能 对A_base 的成员进行访问, 因为AA 从A_base 继承 的成员 为private,跟派生访问标号相同。
接口继承与实现继承:
public 派生类 继承基类的接口,具有与基类相同的接口。
使用private 或 protected 派生的类不继承基类的接口, 称为实现继承。
恢复继承成员的访问级别:
派生类可以恢复继承成员的访问级别, 但不能使访问级别比基类中原来指定的更严格或跟宽松。
using 基类类名::基类成员
默认继承保护级别:使用class 保留字 定义的派生类 默认具有 private 继承
struct ........................................... public
友元关系与继承:
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
继承与静态成员
如果基类定义了static 成员, 则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。
转换和继承
1 引用转换 != 转换对象: 将对象传给希望接受引用的函数时,引用直接绑定到该对象, 实际上实参是该对象的引用,对象本身未被复制, 并且,转换不会在任何方面改变派生类型对象,如果传递的实参是存储派生类的基类引用! 该对象仍是派生类型对象。
将派生类对象传给希望接受基类类型对象(不是引用)的函数是, 情况完全不同。这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。 如果实参是形参的派生类,则该派生类对象的基类部分被复制到形参。
(注意以上两种转换的差别!!!!)
2 用派生类对象对基类对象进行初始化 或 赋值
初始化: 调用函数 —— 复制构造函数
赋值: 调用函数 —— 赋值操作符
1、可以自己定义基类的复制构造函数(以派生类引用作为形参) 和 赋值操作符(右操作数为派生类const引用)
2、基类定义自己的复制构造函数 和 赋值操作符。 这些成员接受一个形参,该形参是一个基类的const 引用。(因为存在派生类引用到基类引用的转换)一般是定义这个的复制构造函数
再次谨记: 派生类的引用是可以由编译器自动转换成基类引用的。
3 派生类到基类转换的可访问性
判定规则:如果派生类对基类的public 成员可以访问,则转换可访问的;否则,转换不可访问。
public 继承, 则用户代码和后代类都可以使用派生类到基类的转换。
private 或 protected :用户代码不能将派生类型对象转换成基类对象。
private继承: 从private继承类派生的类 不能转换成基类。 而protected 则可以。
基类到派生类的转换:
从基类到派生类的自动转换是不存在的。
如果知道从基类到派生类的转换是安全的。可以使用 static_cast 强制转换 或 dynamic_cast 申请运行时检查。
构造函数 和 复制控制:
构造函数 和 复制控制成员不能继承
派生类构造函数:
每个派生类的构造函数除了初始化自己的数据成员,在这之前,还应调用基类的构造函数初始化基类成员
向基类构造函数传递实参:派生类构造函数的 初始化列表只能初始化派生类的成员,不能直接初始化继承成员。但是,派生类构造函数通过将基类包含在构造函数初始化列表 中来间接初始化继承成员。
只能初始化直接基类
复制控制和继承
定义派生类复制构造函数: 如果派生类显示定义自己的复制构造函数或赋值操作符,则该定义完全覆盖默认定义,被继承类的复制构造函数和赋值操作符 负责对基类成分 以及 类自己的成员进行复制或赋值。
注意: 如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类成分。
形式 Derived(const Derived& d) : base(d) {}; 里面自动把派生类的引用转换成基类引用
原因: 如果没有,则对象的基类成员 调用基类的默认构造函数初始化 : 基类成分 保存默认值, 而 派生类部分是另一对象的副本。
定义派生类赋值操作符: 如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显示复制:
形式: Derived & Derived::operator=(const Derived &rhs){
if(this != &rhs) {
if(this != &rhs) {
Base::operator=(rhs); .....}
return *this;}
派生类析构函数:派生类析构函数 不负责撤消基类对象的成员
对象的撤消顺序与构造函数相反: 首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数。
虚析构函数
:动态释放对象有关。。。个人认为只要跟动态扯上关系,就离不开virtual 了~~~
原因:指针的动态类型与静态类型不同。 比如实际指向派生类对象的基类类型指针。
如果析构函数为虚函数,通过指针调用时,运行哪个析构函数将 因指针所指向对象类型的不同而不同。
注意: 即使析构函数没有工作,继承层次的根类也应该定义一个虚析构函数
注意: 构造函数 和 赋值操作符不是虚函数
构造函数和析构函数中的虚函数
两种情况下:运行构造函数 或 析构函数时候, 对象都是不完整的。
在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
规则: 如果在构造函数 或 析构函数中调用虚函数, 则运行的是为构造函数 或 析构函数 自身类型定义的版本。
对象、引用或指针 的静态类型决定了对象能够完成的行为。
作用域与成员函数:
同名 不同类型 的函数 屏蔽!!!!
在基类和派生类中使用同一名字的成员函数, 其行为与数据成员一样: 派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同, 基类成员也会被屏蔽。
注意: 局部作用域中声明的函数不会重载全局作用域中定义的函数, 同样,派生类中定义的函数也不重载基类中定义的成员。 通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类中根本没有定义该函数时,才考虑基类函数。
重载函数:成员函数也可以重载。派生类可以重定义所继承的0个或多个版本。
如果派生类重定义了重载成员, 则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。
通过using 声明可以简化 重载 所有版本的函数!!! 只是简化而已,原理还是一样的。
using 声明: 派生类不用重定义所继承的每一个基类版本,用using 声明将该函数的所有重载实例加到派生类的作用域。 之后,派生类只需要重定义 本类型确实必须 定义的那些函数。对其他可以使用继承的定义。
注意: 一个 using 声明 只能指定一个名字, 不能指定形参表。
虚函数与作用域
获得动态绑定:必须通过基类的引用或指针调用虚函数。
通过基类调用被屏蔽的虚函数
class Base
{public:virtual int fcn();};class D1:public Base{public:int fcn(int);// 隐藏了base 的虚函数 fcn 名字相同,原型不同,屏蔽了~~~};
class D2:public D1{public:int fcn(int); //重定义了 D1 中的 fcn(int); 原型相同,重定义了。int fcn(); // 重定义了 base 的 fcn();};
名字查找与继承:
确定函数调用的四个步骤:
1)首先确定进行函数调用的对象、引用或指针的静态类型。
2)在该类中查找函数,如果找不到,就在直接基类上查找,如此循着继承链往上找。如果都找不到,则调用错误。
3)一旦找到名字, 进行常规类型检查,查看找到的定义,该函数调用是否合法。
4)假定函数调用合法, 编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数
如上 3)步骤可以解释为什么 名字相同,原型不同的函数 在派生类里面为什么屏蔽了基类的函数, 因为找到名字之后就不眼继承链再找了,直接进行匹配了,所以不存在重载的可能性了。
4) 步骤可以解释为什么虚函数 规定在 基类和派生类里面要完全匹配了, 一般是通过 基类的引用或指针 去调用虚函数的,如果某个派生类的虚函数 与 基类不完全匹配(返回类型可以不一样) ,则在调用虚函数的时候,基类找到这个虚函数的名字,但是函数的匹配和调用不同,已经报错了, 还谈什么动态绑定。
运行实例:
class Base
{public:virtual void fun(int i=8){cout<<"Base:"<<i<<endl;//return this;}};
class Derived: public Base{public :void fun(int i){cout<<"Derived:"<<i<<endl;//return this;}};
int main(){
Base * bp1, *bp2;bp1 = new Base();bp2 = new Derived();
bp1->fun(1);bp2->fun();}
结果:Base:1Derived:8 // 这里的默认值确定 ,应为虚函数的默认实参是在编译时确定的,因为是通过基类指针调用的,所以默认参数值是基类里面的默认值请按任意键继续. . .
纯虚函数 在函数形参表后面写上=0
含有(或继承) 一个或多个纯虚函数的类是抽象类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
容器与继承
因为派生类对象在赋值给基类对象时会被“切掉”, 所以容器与通过继承相关的类型不能很好地融合。
容器补充:带比较器的容器
首先容器应该知道比较器的类型,所以首先定义比较器的类型
EX: typedef bool (*Comp) (const Sales_item&, const Sales_item&) ; Comp定义为 函数类型指针类型的同义词。
std::multiset< Sales_item, Comp> items (compare) ; compare是一个函数名 (注意: 函数名其实就是函数的地址,可以赋给函数指针)
以上的定义: items 是一个 multiset ,保存 Sales_item 对象并使用Comp类型的对象比较它们。
文本查询实验总结:
1、 在class A 中把 class B 声明为友员,只需在 class A 中的头文件声明 class B 中即可,不用包含 class B 的定义。
2、 在class A 中如果有定义到class B ,或对class B 的指针或引用进行操作,则不能仅仅声明class B ,还必须把 class B 的定义包括进来,具体就是include class B 的定义文件(我的操作)
3、 在 class A 中把某个函数func() 声明为友员时,只需声明函数func()即可。
4、 如果没把inline 函数定义在头文件,不小心如果写到实现文件(我不小心写到cpp) 文件里,编译的时候出现了" 无法解析外部符号"的错误。
5、类之间的继承层次要清楚, 因为每个类一般都有自己的定义和实现文件(个人做法) ; 所以继承之间、友员、函数参数 这些东西发生关系,头文件之间的包含关系就来了。。。注意注意: 绝对不能出现两个头文件互相include ,不然会出现很多错误,而且都是莫名其妙的错误,所以当出现很多错误而且觉得莫名其妙是,就检查一下头文件之间的include 是否出现 连环(不仅仅是两个头文件之间) 。 个人觉得一般从基类到派生类的继承层次 了解清楚,所以code 之前还是想清楚。然后再决定一些类似 非成员inline函数 应该放在哪个哪个文件里面。