C++ OOP基础--动态绑定
May 29, 2017
面向对象其实就是那样,都差不多,不一样就是关键字还有一些奇怪的机制问题,总的来说实现的东西都是一样的。
虚函数 #
对应Java的抽象函数。但是略有区别。
虚函数在定义的时候使用关键字virtual
来修饰声明,但是不一定是没有实现的,可以指定一个已经是实现的函数为虚函数。虚函数最主要的定义是使得每个继承他的子类都实现自己的版本。并且虚函数是实现C++动态绑定的关键。
虚函数只能声明在类的内部。
动态绑定 #
所谓动态绑定就是,有些函数并不是在编译的时候就知道要去调用那个函数的,而是在真正运行的时候才能根据变量的类型去调用对应的函数。这个实现就是对于基类的虚函数,子类也实现一个版本(子类在实现的时候不需要指定函数为虚函数,因为继承过来的函数就是虚函数,是可以传递的),这样对于同一个函数就有两个版本,当我在调用的时候使用指针调用或引用调用(注意必须是这两种调用之一的方式才会有动态绑定的效果,其他都是在编译阶段就能够确定下来。)就可以实现动态绑定。
由于虚函数是在执行的时候才调用,所以在编译阶段就有些错误报不出来,比如如果我们没有实现某个子类的虚函数,在调用的时候就没有函数去执行,所以所有的虚函数都必须被定义,而不仅仅是声明,普通的函数只要我们永不到就可以不进行定义。
引用和指针调用 #
所谓引用和指针调用,是指函数中关于虚函数的调用对象是作为引用参数或者是指针参数传进去的,这样就需要根据参数的类型来调用不同版本的虚函数。这里就要说一下基类与派生类之间的类型转化问题,正是因为其二者可以进行类型转化,所以在参数列表中的类型并不能唯一限定参数的类型,才有了虚函数调用时的动态绑定。
类间类型转换 #
子类中有一部分是来自与基类的,这一部分可以被单独利用。也就是说,一个子类,我们也可以把他当作他的某个基类来使用,使用的时候只用到基类的部分。也就是说,我有一个基类的指针,我就可以指向这个子类,此时,指针就操作子类的基类部分。这就是从派生类到基类的类型转换。
反过来就是不行的,可想而知,多的东西可以不要,少了东西就会有问题,所以不存在从基类到派生类的转化。同时我们也就明白了,如果你要对于一个虚函数动态绑定,在调用函数的对象的类型声明的时候就要声明为基类的类型,这样当你传入一个子类的引用的时候,该引用就会转换为子类,进而去调用子类的虚函数实现;如果你写声明的时候声明为了子类的类型,就没有办法动态绑定了,因为反向没有办法转换。
虚函数覆盖 #
子类要实现自己的虚函数版本就要对基类的虚函数进行覆盖,override,又是这个熟悉的东西。覆盖当然就要参数类型完全一样,参数不一样的那是重载,是不同的函数。书上说,一般我们都是要进行覆盖而不是重载,但是有的时候我们会搞错参数列表的顺序等细节,所以,你可以在函数声明的参数列表后加override
说明符来告诉编译器,你是要覆盖虚函数的,如果有细节错误就告诉我!
如果你在后面加上了final
修饰符,就意味着,这个虚函数不希望再被覆盖了。
关于虚函数的默认参数,不同版本可能会有不同的默认参数值,但是在默认参数的选取上,是依据静态类型决定的,也就是说不能做到动态绑定,调用函数的对象定义成什么类型就用什么默认值。如你调用函数的对象是基类的引用,那么即使你最后执行的是子类的函数,默认值也是基类的默认值,所以这个就会出现隐蔽的问题。所以安全起见,默认参数一致最好。
猜想 #
同时从这里我们也可以窥探出编译器的原理。对于一个函数覆盖来说,函数的默认值必须是在编译阶段就写进机器码中的,尽管你有两个版本的函数,编译器会对两个函数分别编译,但是默认值存储的实现是与函数编译分开的。这里只是猜测,对于函数的默认值,其实编译器可以将其存储在对应的栈位置上,也可以将其转成一个赋值命令存储在函数代码段中,这两种理论上都可以实现默认参数的功能,但是从编译器的定义角度,参数是单独存放在数据段的,而不在代码段,那么在编译的时候就会根据你参数的字面类型进行设定,而无法进行动态绑定。这个应该是C++的一个设计缺陷,最初设计函数编译的时候没有动态一说,就直接写进数据段了,等到出现动态绑定的时候沿用之前的函数处理的方式,就无法实现绑定。(不知道这个思路对不对,以后有时间考证下)。
回避动态绑定 #
除了动态绑定,也可以用作用域操作符
来强制执行某一类的虚函数版本。
double ans = C->Base::Update();
为什么要回避动态绑定,这与类间设计有关。如果基类与派生类关于虚函数功能设定是:基类虚函数处理通用处理,派生类处理剩余子类相关内容,那么对于真的要完整实现功能的时候就要在派生类的虚函数中先调用一下基类的虚函数,再实现派生类的函数剩余部分,所以这时候就要回避动态绑定。
纯虚函数 #
你的直觉会告诉你这个纯虚函数应该是个只有声明没有定义的函数。没错,这就是纯虚函数,在格式上有一个特点,就是函数声明最后有一个=0
标志。这个声明必须在类的内部,并且类内不能对这个函数进行定义,在类外部定义是可以的。
纯虚函数是在一些设计上必要的。比如我们有一个操作,有4个版本,每个版本根据不同的参数值有不同的动作,那么我们会设计一个基类,并在基类中声明这个函数,但是这个函数明显什么都不能做,而是要等到不同的子类根据自己的方案实现这个函数。所以这时候就声明一个纯虚函数就好了。
子类只要正常的覆盖这个函数就可以,如果不覆盖就继续往下传递。
抽象类 #
很熟悉,Java中含有抽象函数的类是抽象类,在这里有纯虚函数的类就是抽象类。并没有什么修饰符,就是个类。
继承中的友元 #
- 友元不可继承、不可传递。
- 派生类的友元不可访问基类中的非公有成员。