目录
多态的实现
例题
重载 重写 重定义的区别
抽象类
多态实现原理
C++中的多态是指,当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。在C++中,通过将基类中的成员函数声明为虚函数,即可实现多态。
多态的发生是在继承的前提条件上,且要满足两个重要条件,否则都不能是多态:
1.虚函数的重写(要求三同,同函数名,同返回类型,同参数)-协变除外
2.父类的指针或者引用去调用函数
这与我们普通调用函数时所观察的函数类型不一样,多态调用看的是调用指针或者引用指向的对象,指向父类调用父类函数,指向子类调用子类的函数,这里他看的是指针或者引用指向的对象。
其次虚函数的重写存在两个例外:
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;}
};
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;//调父类析构
//此时子类析构调用虚析构且这里的析构函数是一种重写
delete p2;//先调子类析构释放子类自己的那一部分空间,再调父类析构,释放剩下的父类的空间
return 0;
}
其次我们要知道,只有对于 new子类对象给父类指针时,才会需要调用虚析构,去释放除了子类本身的那一部分空间,还要释放继承父类的那一部分空间,否则会造成内存泄漏。
下面用几道题检验我们的水平:
这里正确答案选择c项,对于p1它是父类指针B1指向子类对象,由于切片的原因,所以p1就是表示Base1的空间同理p2指向Base2的空间。但是p3只想自己,也就是它包含了继承的父类的空间,按照声明的顺序,p3指向Base1+Base2+Derive,首地址的话就是Base1,故选择c。
该题正确答案是B,相信大家可能都会选择D项,首先我们知道p指针是一个子类指针,但他继承了父类的成员函数,所以调用test是父类的函数,test再次调用func函数(这里的调用还是父类this调用func函数,继承父类的),由于指向的对象是子类对象,且满足函数重写,故这里会去调用子类的func,但是记住一点,子类的函数只会重写函数体,对于参数和函数名函数类型都是继承父类的,所以这里的缺省参数应该还是父类里的。
总的来说对于继承不是重写就是重定义,函数重载参数不同(参数类型,参数个数,参数类型顺序)。
当一个基类的成员函数不仅仅添加了virtual,并且函数体为空,如:Drive()这个函数
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
}
这样的函数我们将它称为纯虚函数,这样的类我们叫做抽象类。抽象类不能直接实例化对象!
其次关键字overried可以检查虚函数是否完成重写。
那么虚函数这种是怎么实现的呢?
在此之前我们先了解一下虚函数表:
首先对于虚函数的实现,在类中会有一个虚函数表指针,我们也可以根据类的大小看到有一个指针。
那么这个虚函数表指针是干嘛用的呢?
class Person {
public:
virtual void Bytiket()
{
cout << "成人买票全价" << endl;
}
virtual void Job()
{
cout << "我是医生" << endl;
}
void habit()
{
cout << "打球" << endl;
}
private:
int _b;
};
class Student : public Person {
public:
virtual void Bytiket() { cout << "学生买票半价" << endl; }
private:
int _d;
};
void Func(Person &p)
{
p.Bytiket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
首先对于父类,监视窗口并不能看到真正的情况,我们利用内存窗口再进行观察:
&p
此时我们再观察派生类里面的内容:
&s
vfptr
可以看到子类中也有一个虚表指针,而且这与父类的虚函数表指针不一样,可以看到两者的虚函数指针地址都不一样,但是仔细观察里面存放的各个虚函数,可以看到第一个虚函数指针与父类的不一样,而第二个虚函数指针与父类的一样。
仔细一想我们大概就知道原因了,我们知道虚函数的重写其实是虚函数的覆盖,子类将虚函数表拷贝过来,在我们重写了某一个虚函数时,对应的虚函数指针就会被覆盖成新的,当我们不重写时,对应的虚函数地址没有发生改变,因此虚函数的重写本质上就是虚函数指针的覆盖。
完成覆盖后,当我们利用父类的指针或者引用指向子类对象,在调用时,就会调用完成覆盖后的虚函数的地址(新的虚表),此时调用的就是子类中重写的方法,这也就是我们会说调用的函数和指向的对象有关,指向子类调用子类的,指向父类调用父类的。本质就是指向某个对象的虚函数表。
那么又有一个问题我们也可以仔细想象了:为什么必须是父类对象的指针或者引用,对象就不行呢?
了解到虚函数表的存在,我们再次思考,对象的指针或者引用那就是代表父类的这一部分的空间,指向子类对象时,中间不产生临时对象,可以当作切片剩下父类的那一部分,引用也就是直接引用那一部分。所以父类的引用与指针相当于就是子类中父类的那一部分空间,而我们用的是父类对象的话,父类对象指向子类对象,单单就是把子类中那父类的一部分给给父类但是不包括虚函数表,没有虚函数表多态就无法实现,故此必须是父类的指针或引用指向子类对象。
至于这里虚函数表不能拷贝,在设计之时就已经必须这样规定,如果虚函数指针也能被拷贝,那就全乱了,在调用时,该访问哪一个虚表?