virtual关键字
1.可以修饰原函数,为了完成虚函数的重写,满足多态的条件之一。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2.可以在菱形继承中,去完成虚继承,解决菱形继承数据冗余和二义性。
多态的两个条件
1.虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类 型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
2.父类对象的指针或引用(父类可以接受子类->切割/切片,子类不一定能接受父类)去调用虚函数
void Func(Person& p)
{ p.BuyTicket(); }
将&去掉后就无法满足多态。
满足多态:跟指向的对象有关,指向那个对象就调用其虚函数。
不满足多态:跟调用对象的类型有关,类型决定其调用的虚函数。
虚函数重写的两个例外:
协变(基类与派生类虚函数返回值类型不同)
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
}
父类写了virtual子类可以不写->但是这样的话不太标准。~~~~~~~~~~
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的 析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规 则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处 理成destructor。
class Person{
public:
virtual ~Person() {cout<<"~Person()"<<endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成
多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
virtual ~Student() { cout << "~Student()" << endl; }
};
子类和父类的析构函数的函数名均会被处理为destructor。
这样使用没有问题。
delete p2;不构成多态时,调用的指针(Person)类型决定了要调用(Person)的析构函数。
构成多态时,调用的指针指向谁,就调用谁的析构函数。
C++11 override 和 final
final
final:修饰虚函数,表示该虚函数不能再被重写
final:修饰类,表示该类不能被继承<----->构造函数私有化
override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重载、覆盖(重写)、隐藏(重定义)的对比
重写时为了重写函数实现。
重写比重定义要求更严格。
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
纯虚函数
virtual void Drive() = 0;
1.强制子类去完成重写。
2.表示抽象(在现实中没有对应的实体)的类型。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所 以如果不实现多态,不要把函数定义成虚函数。
vfptr
虚函数表指针(简称虚表指针)。
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
虚函数表其实就是一个函数指针数组。通常以0x00000000 结束。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
int _p = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _s = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
满足多态时:
运行时 到指向对象的虚函数表中查找对应的虚函数的地址
不满足多态时:
编译时 直接通过p的类型决定要调用函数的地址。
重写之后子类的虚函数会将父类的虚函数(virtual void BuyTicket())覆盖掉。
虚函数存在那?
普通函数和虚函数最终都会编译为指针,存在代码段。
注:虚函数不是存在虚表,虚表中存的是虚函数的指针。
虚函数表存在那?
代码段(常量区)同类型的对象共用一张虚表。
代码证明
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
void func()
{
//取出对象的前4个字节
Base b1;
printf("vfptr虚表地址: %p\n", *(int*)&b1);
int i = 0;
int* p1 = &i;
int* p2 = new int;
const char* p3 = "hello";
printf("栈变量: %p\n", p1);
printf("堆变量: %p\n", p2);
printf("代码段常量: %p\n", p3);
printf("虚函数地址: %p\n", &Base::Func1);
printf("普通函数地址: %p\n", func);
}
动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数 重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用 具体的函数,也称为动态多态。 (运行时到虚表中找虚函数地址)
单继承和多继承关系的虚函数表
单继承中的虚函数表
函数指针的定义
void(*p)();
class Base{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
打印虚函数表
typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR* pTable)
{
for (size_t i = 0; pTable[i] != 0; ++i)
{
printf("vfTable[%d] : %p-> ", i, pTable[i]);
VF_PTR f = pTable[i];
f();
}
cout << endl;
}
多继承中的虚函数表
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
}
sizeof(Derive)=20(8+8+4)
子类继承父类,会继承父类的虚表,子类直接覆盖父类的虚表即可。
char*+1---->+1
int*+1----->+4
Derive d;
PrintVFTable((VF_PTR*)(*(int*)&d));
PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
多继承时func3()往Base1的虚表里放。
多态练习题
1.子类缺省参数不起作用/虚继承
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: 以上都不正确
virtual void test(){//A* this
func();
}
p->test();//p->test(p);
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
子类缺省参数不起作用。
虚函数继承的是接口(函数名,参数,返回值),唯独不继承函数体。
重写指的是对其实现进行重写。
{ std::cout<<"B->"<< val <<std::endl; }
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型的函数
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类使用的不是同一张虚表
多继承中指针偏移问题
下面说法正确的是( )
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;
}