什么是多态
在继承关系中,不同类型的对象去调用同一函数,产生不同的行为。
例如:卖火车票的时候,学生是半价,成人是全价。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
多态又分为静态多态(编译时)和动态多态(运行时)
静态多态:例如重载
动态多态:根据具体拿到的类型确定程序的具体行为,调用具体的函数。
多态的两个条件:
1,必须是虚函数,派生类必须对基类进行重写
2,必须通过基类的指针和引用去调用虚函数。 (如果是父类对象去调用方法,一定是父类的方法)
重写的条件: 必须是虚函数,然后子类,父类的函数名,返回值,参数必须相同
例外:1,协变 父类和子类的虚函数返回值可以是其他继承关系的父类和子类的指针或者引用
2,析构函数,只要基类的析构函数是虚函数,派生类只要定义了析构函数,
编译器将父类和子类的析构函数统一处理为destructor()。
我们自己实现的时候最好将基类的析构函数设置为虚函数,这样可以保证在动态(new)生成父类子类对象后,
能正确调用各自的析构函数。
重载:在同一作用域,函数名相同,参数不同
重写(覆盖):在继承关系中,两个虚函数分别在父类和子类,函数名,参数,返回值必须相同。
重定义(隐藏):两个函数分别在父类和子类,同名函数。( 在继承关系中,两个同名的函数不构成重写,那一定就是重写。)
关于关键字final (禁止该函数被重写) 和override(检查是否重写,不重写就报错)
抽象类
在虚函数后面加上 = 0 ,该函数就是纯虚函数,有纯虚函数的类就是抽象类,抽象类不能去实例化对象。
子类继承抽象类后,子类如果不重写该虚函数,则该子类也是抽象类。例外父类的纯虚函数可以有实体,但是没什么意义。
class A
{
public:
virtual void Print() = 0 //这个就是纯虚函数
{
cout << "class A " << endl;
}
};
class B
{
public:
virtual void Print()
{
cout << "class B " << endl;
}
};
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数
多态的原理
这段代码运行起来是 8个字节,为什么呢?
class A
{
public:
virtual void Print()
{
cout << " haha" << endl;
}
int _a;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
回到原点:多态的实现原理。当用父类的指针和引用去指向和引用子类的时候,在用父类指针去和引用去调用方法,
调用的是子类方法。 因为子类和父类各自生成了虚表,通过虚表各自找到所需要的虚函数,从而实现了多态。
为什么不能用父类对象呢? 因为赋值的时候,vfptr不会拷过去。
一些概念的理解问题:
一个类对象可以有多个虚表,因为有多继承。
所有同类的对象公用同一张虚表,因为虚表和虚函数都是存放在代码段的(常量区)。
虚表指针是在构造函数初始化列表阶段初始化的,而虚函数表是在编译时生成的。
当出现多继承,也就是说子类有多个虚表,但是子类中有一个虚函数(父类中没有的),这个虚函数就
放在第一张虚表的后面。
构造函数初始化列表顺序只跟继承顺序有关 , 析构顺序与构造顺序完全相反。
关于多态的选择题, 非常重要!!!!
1,下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
2, ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,
而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
扩展: 动态绑定有多态和模板 静态绑定 : 重载
3, 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
装性的表现
4, 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
解析: 实际上:纯虚函数可以有函数体, 但是没什么意义。
5, 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
解析:对于inline来说, 你可以在虚函数前面加上inline关键字, 但是编译器会忽略inlin属性, 因为虚函数的地址
要存放在虚表中,虚函数要存在代码段。 而inlin是直接展开(避免建立函数栈帧)。
对于static来说,没有this指针, 通过类型::函数名无法访问到虚函数的。
6, 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
解析: 虚表指针是在构造函数初始化列表阶段初始化的,而虚函数表是在编译时生成的
7, 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
8,
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):B(s1,s2),C(s1,s3),A(s1)
{ cout<<s4<<endl;}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
解析: 构造顺序取决于继承的顺序
9,
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
10,
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test() //这里传过来调用是子类指针this(子类可以赋值给父类 切割 / 切片)
{
func(); //this是子类, 满足多态的条件,所以要去调用子类的
}
};
class B : public A
{
public:
//因为B继承自A, 所以virtual会自动加上,然后形参相同, 参数也会用父类的替换
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: 以上都不正确
11,
对象访问虚函数快,还是访问普通函数快?
解析 : 如果不构成多态, 是普通调用, 其实两者一样快。
如果构成多态, 肯定是普通函数快, 因为访问虚函数需要通过虚表指针去虚表中查找。
核心知识点 : 虚表是在编译阶段生成, 而虚表指针是在构造函数初始化列表初始化的。