一、总结
静态多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为
动态多态:调用虚函数,不同的对象去调用,就有不同的行为/形态
多态的条件:
a.子类中重写父类的虚函数。
b.必须是由父类的指针或者引用去调用重写的虚函数。
虚函数的重写:
a、父类和子类都必须是虚函数
b、函数名、参数、返回值都必须相同
例外:
a、协变(父子类的指针或引用)
b、析构函数要特殊处理,因为父类指针可能指向子类的对象,就要调用子类对象的析构函数
c、子类中的重写虚函数可以不加virtual
对象的虚表指针是在构造函数的初始化列表才生成,但是虚函数表是在编译期间就生成的。原因:运行期间要动态生成通常需要空间,需要在堆上申请。
二、习题
下面的代码为什么跑不过呢?
class A
{
public:
virtual void func1()
{}
int a = 0x10;
};
class B:virtual public A
{
public:
virtual void func1()
{}
int b = 0x20;
};
class C :virtual public A
{
public:
virtual void func1()
{}
int c = 0x30;
};
class D:public B,public C
{
public:
/*virtual void func1()
{}*/
int d = 0x40;
};
int main()
{
D d;
return 0;
}
分析它的对象模型,由于B,C虚继承了A,则在d的对象模型当中就会只有一份A,但是B和C都对A进行了重写,导致在d看来func1有两个不同的实现版本,所以在D中不重写func1就会导致编译错误。但是如果B,C中没有对func1进行重写,在D处虽然会有两份func1,但是他们都是一样的,这个时候就不会报编译错误了。
虚基表指针内容的前四个字节的作用:
是用来算虚表指针到虚基表指针的一个距离的。
下面代码我们来分析d的对象模型。
class A
{
public:
virtual void func1()
{}
int a = 0x10;
};
class B:virtual public A
{
public:
virtual void func1()
{}
virtual void func2()
{}
int b = 0x20;
};
class C :virtual public A
{
public:
virtual void func1()
{}
virtual void func2()
{}
int c = 0x30;
};
class D:public B,public C
{
public:
virtual void func1()
{}
virtual void func2()
{}
int d = 0x40;
};
int main()
{
D d;
return 0;
}
通过内存窗口和监视窗口,我们可以得知,d对象当中的B部分的第一个地址是指向虚函数表的指针,而第二个字段则指向的虚基表,在先前的博客中虚基表的第一个字段一直都是0x00 00 00 00,而现在是0xff ff ff fc,也就是-4,从虚基表指针的地址往上走4字节正好是虚函数表指针。所以,虚基表中第一个字段也就是用来得到虚函数表的位置。
选择题
- 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
A.继承,是一种复用
- ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
D.父类的指针或者引用可以指向子类的虚函数表
- 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为
白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也
称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
C.优先使用组合,继承是父类的静态复用,组合是动态复用。
- 以下关于纯虚函数的说法,正确的是( ) A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类 C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
A,C中子类可以不实现纯虚函数,不过他就无法实例化出对象。
- 关于虚函数的描述正确的是( ) A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数 C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
B.内联对编译器是一种建议,声明了virtual后就会忽略内联的属性,因为内联函数是没有地址的,他都在调用的地方展开了。而虚函数都是要放到虚函数表当中的。注:vs2013和liunx下都可以跑得过。
- 关于虚表说法正确的是( ) A:一个类只能有一张虚表 B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表 C:虚表是在运行期间动态生成的 D:一个类的不同对象共享该类的虚表
D,C中运行期间要动态生成通常需要空间,需要在堆上申请。所以我们可以猜测他是在编译的时候生成的。
- 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址 B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同 D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
D,虚函数表是多态的,虚基表是虚继承的找超类当中的成员变量
- 下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
class A{
public:
A(char *s) { cout << s << endl; }
~A(){}
};
class B :virtual public A
{
public:
B(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(char *s1, char *s2, char *s3, char *s4) :C(s1, s3), B(s1, s2), A(s1)
{
cout << s4 << endl;
}
};
int main() {
D *p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
答案:A
首先,排除法,我们声明继承的顺序就是初始化的顺序,注意这个不是初始化列表的顺序,那么我们可以排除CD,因为B要在C的前面。然后我们要先初始化父类,则A就在就前面了。并且编译器会进行优化,每个对象都只会初始化一次。
9.以下的结果
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
return 0;
}
答案:输出1 随机值
因为成员变量声明的顺序就是初始化列表的顺序,所以他会先走_a2(_a1)然后再走_a1(a)。
- 多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
选C
- 以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
选B
因为B中没有对test进行重写,那么调用父类的test(),test中调用了func,这里的this指针是B类型的,B并且对func进行了重写,重写是一种接口继承,重写只要满足参数,函数名,返回值相同就可以,而缺省参数会用父类的,所以最终打印出来就是B->1。
普通函数是实现继承。
虚函数的继承,是一种接口继承。
class A
{
public:
virtual void func(int val = 1)
{
}
void test()
{
}
};
// A 编译报错 B运行崩溃 C 正常运行
int main()
{
// 1、
A* p1 = nullptr;
p1->func();
// 2、
A* p2 = nullptr;
p2->test();
return 0;
}
1B 2C,原因,普通的成员函数是放在代码段当中的,我们不需要解引用行为去调用,所以第二个不会出错,而第一个由于多态的两个条件都满足,所以他就会访问p1指向的对象头上四字节,因此会报错。
问答题:
静态成员可以是虚函数吗?
不能,静态成员函数没有this指针,适用类型::成员函数的调用方式无法访问虚函数表,
构造函数可以是虚函数吗?
构造函数需要实现多态吗?没有意义,因为我们实例化子类对象的时候也要调用父类的构造函数。对象的虚函数表是在编译时初始化,虚表指针是在运行时经过构造函数的初始化列表之后才生成的。
对象的虚表指针在构造函数初始化列表才初始化,将构造函数弄成虚函数,那么对象要有虚表指针才能调用构造函数,就自相矛盾了,所以当把父类的构造函数设置为虚函数会直接报编译错误。
析构函数需要虚函数吗?
需要,因为 当我们delete(指针),我们并不知道这个指针指向的对象是父类的对象还是子类的对象,通过多态我们就可以进行动态的一个检测再释放。
对象访问普通函数快还是虚函数更快??
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
对象调用是不可能实现多态的
小知识点
1.模板属于编译时多态,我们通过反汇编可以看到它运行时call一下就跳转到对应的函数了。
2.使用父类对象调用的永远都是父类的方法!
3.接口继承的体现
4.静态成员函数不能设置成虚函数的原因,是因为静态成员函数没有this指针。没有this指针无法拿到虚表,没法实现多态。
5.假设重写成功,通过指针或引用就一定能实现多态吗?
错!一定要是父类的指针或引用。
6.在多态和组合都能用的时候,推荐用组合,因为多态的调用是有额外的开销,有虚函数表指针,虚函数表,运行时决议等等。
7.友元函数不能设置成虚函数,因为友元函数不属于成员函数。
总结
多态一节到此为止。