多态的概念:
多态即多种形态,在生活中不同的人去做一件事可能是不同的结果。比如学生买票和成人买票,学生买票为半价,而成人则是全价票。
多态的定义
多态的构成条件
多态为在继承关系中不同的类对象去调用同一函数,产生了不同的结果。比如现在有两个类为Person和Student,Student继承了Person。Person买票为全价,Student买票为半价。
在继承中构成多态需要两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类要对基类的虚函数进行重写(注意区分重写与重定义,重定义为隐藏)
class Person
{
public:
virtual void buy_ticket()
{
cout << "成人买票全价" << endl;
}
};
class Student:public Person
{
public:
virtual void buy_ticket()
{
cout << "学生买票半价" << endl;
}
};
void func(Person& p)
{
p.buy_ticket();
}
void test1()
{
Person p1;
Student s1;
func(p1);
func(s1);
}
其运行结果为:
可以看到子类与父类对象调用func函数时,其产生了不同的结果
虚函数
虚函数即为被virtual修饰的类成员函数
虚函数的重写
虚函数的重写(覆盖):子类中有一个与父类完全相同的虚函数(返回值类型,函数名,参数列表相同),称子类的虚函数重写了父类的虚函数。
上面的代码例子中buy_ticket即为虚函数重写
虚函数重写的例外
1.协变(父类与子类虚函数的返回值类型不同)
当父类虚函数的返回值为父类对象的引用或者指针,子类虚函数的返回值为子类对象的引用与指针时,将此类特殊情况成为协变。
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;
}
}
2.析构函数的重写
如果父类的析构函数为虚函数,此时子类的虚函数只需要定义,无论有没有关键字virtual都与父类的析构函数构成重写(编译器将析构函数统一叫做destructor)下
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student:public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
void test2()
{
Person* p = new Person;
Student* s = new Student;
delete p;
cout << "---------------------------------" << endl;
delete s;
}
执行结果为:
可以看到父类的对象析构调用的是父类的析构函数,而子类的析构也调用了父类的析构,如果子类没有需要额外自行清理的资源(如开辟了空间需要释放等,则其可以只定义析构函数,然后直接使用父类的析构函数)
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student:public Person
{
public:
~Student()
{
}
};
void test2()
{
Person* p = new Person;
Student* s = new Student;
delete p;
cout << "---------------------------------" << endl;
delete s;
}
如上,没有需要额外清理的空间,直接使用父类的析构函数
补充final与override
1.final:修饰虚函数,表示该虚函数不能被重写,如果重写会报错
2.override:检查子类虚函数是否重写了某个父类的某个虚函数,如果没有重写则编译报错
class A
{
public:
virtual void fuc1()
{}
};
class B:public A
{
public:
virtual void fuc1() override
{
cout << "b" << 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;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
多态的原理
虚函数表
这里的sizeof(Base)为多少?
class Base
{
public:
virtual void Func1()
{
cout<<"Func1()"<<endl;
}
private:
int _b = 1;
};
通过窗口可以看到sizeof(Base)为8bytes(x86)环境下。
除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
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:
//重写了Func1
virtual void Func1()
{
cout<<"Derive::Func1()"<<endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
表指针也就是存在这部分的,另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表在vs下是存在代码段的