基础
通俗来说,多态就是不同的对象去完成某个行为时会产生不同的状态
虚函数的重写
被virtual修饰的类成员函数被称为虚函数,如:
class Person
{
virtual void BuyTicket()//虚函数
{
//...
}
};
虚函数的重写(覆盖)的定义为:派生类中有一个与基类完全相同的函数,即派生类的虚函数与基类的虚函数的返回值类型、函数名、参数列表(参数类型和位置一样,参数名可以不同)完全相同,则称派生类的虚函数重写了基类的虚函数。
class Person
{
virtual void BuyTicket()//虚函数
{
//...
}
};
class Student:public Person
{
virtual void BuyTicket()//虚函数重写
{
//...
}
};
这里有一点需要注意,如果派生类的虚函数不加关键字virtual,也算是完成了虚函数重写(父类的虚函数不加关键字virtual不行),但这种写法不规范,不建议使用。
虚函数的重写有两个例外:
1.协变
当基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类的指针或者引用时,我们称之为协变,也算是虚函数的的重写。
2.析构函数的重写
如果基类的析构函数为虚函数,只要派生类的虚函数定义了,无论派生类的析构函数是否加virtual,函数名是否与基类的函数名相同,都算是与基类的虚构函数构成重写。
之所以基类的析构函数名与派生类的函数名不同也算构成重写,是因为编译器对析构函数的函数名进行了特殊处理,编译后析构函数的函数名统一处理成destructor。之所以这样处理,是因为当我们用一个基类的指针指向一个子类然后对这个指针进行delete时会变成多态调用,从而使资源释放更加彻底,避免内存泄漏,总之,如果派生类开辟了资源,就应该将析构函数写成虚函数。
我们再来看看重载、覆盖(重写)、隐藏(重定义)的区别:
因此基类和派生类的同名函数不是重写就是隐藏。
需要注意的是,如果由于某些错误没有满足虚函数重写条件而造成虚函数重写失败,只要运行时没有错误,编译器是不会报出来的,因此c++提供了2个关键字override和final来检查是否重写虚函数。
1.override:检查派生类是否重写了某个虚函数,如果没有重写,则编译报错。
class Person
{
virtual void BuyTicket()//虚函数
{
//...
}
};
class Student:public:Person
{
virtual void BuyTicket() override//检查虚函数是否重写
{
//...
}
};
2.final:修饰虚函数,表示该虚函数不能被重写,修饰类时表示该类不能被继承
class Person
{
virtual void BuyTicket() final//不能重写该虚函数
{
//...
}
};
class Student:public Person
{
virtual void BuyTicket()//虚函数重写会报错
{
//...
}
};
多态定义
在不同继承关系的类对象去调用同一函数,产生了不同的行为,这就是多态。在继承中构成多态需要满足两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的虚函数必须是虚函数,且派生类必须对基类的虚函数进行重写
满足条件以上2个条件的函数调用是多态调用,多态调用的函数只与对象相关,否则就是普通调用,普通调用的函数与类型相关。
如果是普通调用,则调用哪个函数看的是指针或者引用或者对象的类型,如果是多态调用,则看的是指针或者引用指向的对象。
class Person
{
public:
virtual void fun()
{
cout<<'1'<<endl;
}
};
class Student:public Person
{
public:
virtual void fun()
{
cout<<'2'<<endl;
}
};
void test1(Person& p)
{
p.fun();
}
void test2(Person p)
{
p.fun();
}
int main()
{
Person p;
Person* pp=nullptr;
Student s;
Student* ps=nullptr;
p.fun();//1
pp=&p;
pp->fun();//1
s.fun();//2
ps=&s;
ps->fun();//2
pp=&s;
pp->fun();//2
ps=(Student*)&p;
ps->fun();//1
test1(p);//1
test1(s);//2
test2(p);//1
test2(s);//1
}
普通函数的继承是一种实现继承,派生类继承了基类函数,可以在派生类中使用基类非私有函数,继承的是函数的实现(包括接口)。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,不继承实现,目的就是为了重写,从而达成多态,因此如果我们不实现多态就不要把函数定义成虚函数。
抽象类
在虚函数后面加上 =0 则表示这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,被派生类继承后也不能实例化出对象,只有当派生类重写了纯虚函数时,派生类才可以实例化出对象,因此抽象类强制派生类必须进行虚函数重写。
class Person
{
virtual void BuyTicket() =0 //纯虚函数
{
//...
}
};
class Student:public Person
{
virtual void BuyTicket()//必须重写纯虚函数才能实例化出对象
{
//...
}
};
需要注意抽象类可以定义指针以进行多态调用。
多态的原理
多态原理
每一个含有虚函数的类都至少有一个名为_vfptr的虚函数表指针,这个指针存放在对象的前4个字节中(x86程序中,如果是x64程序,为8个字节),指向一张虚函数表,简称虚表(本质是函数指针数组),虚表存放的是虚函数的地址,虚表最后一般回放个nullptr。当派生类继承基类时,编译器会将基类的虚表内容复制一份到派生类的虚表中,如果派生类重写了基类的某个虚函数,编译器就会在派生类的虚表中用派生类重写的虚函数去覆盖虚表中对应的基类的虚函数(由此我们也可得知重写是语法上的叫法,覆盖是原理层的叫法),如果派生类自己新增了虚函数,则按在派生类中声明的次序依次追加到虚表中。
需要注意的是虚函数和普通成员函数一样,都是存放在代码段中,只不过又将虚函数的指针拿出来放在一张虚表中,而对象只需要存放一个指向虚表的指针,就可以找到对应的虚函数,在vs下,虚表存放在代码段中。同时虚表是在编译时生成的,而虚表指针是在构造时生成的,对于同类型的对象,他们的虚表指针都指向同一张虚表。
class Person
{
public:
virtual void fun()
{
cout<<'1'<<endl;
}
};
class Student:public Person
{
public:
virtual void fun()
{
cout<<'2'<<endl;
}
};
void test1(Person& p)
{
p.fun();
}
void test2(Person* p)
{
p->fun();
}
当我们传引用或指针给test1或test2 时,p指向的是原对象,只不过如果传的是派生类的指针或引用会进行切片而已(其实数据还是在那里,只不过由于类型发生变化导致读取的内存大小不同),这样我们就可以通过原对象找到其虚函数指针指向的虚函数表,然后再调用相应的虚函数。由于编译器只知道p是一个指针或引用,但他不知道这个指针或引用是指向一个派生类还是基类,因此他就不知道调用基类还是派生类的虚函数,只有通过虚表能找到要调用的虚函数,这样调用虚函数是在运行时才能找到被调用的虚函数。
如果通过对象调用虚函数,编译器会在编译期间就认为该对象的虚函数就是我们想要调用的虚函数,直接就从符号表就确认了要函数的地址,直接call就行了。
动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,也称为静态多态,如函数重载等,动态绑定又称为后期绑定(晚绑定),在程序运行期间根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
虚函数进行继承时,虽然进行的是接口继承,但是并不继承基类的函数的缺省值我叫RT的博客。进行虚函数调用时,具体是用子类还是派生类的函数参数的缺省值,由调用这个函数的对象的类型在编译期间确定,所以在编译后就确定了用的是派生类还是子类的函数的参数缺省值。如果是直接通过对象调用虚函数,在编译期间也可以直接确定就是调用这个对象的类型的虚函数,但如果是通过指针或者引用调用虚函数,函数的实现用的是派生类还是子类的还需要等到运行期间才能确定,在编译期间会通过虚表指针找到需要调用的函数的实现部分。
class Person
{
public:
virtual void fun(int value=1)
{
cout << "A->"<<value << endl;
}
};
class Student :public Person
{
public:
virtual void fun(int value=2)
{
cout <<"B->"<< value << endl;
}
};
void fun1(Person& p)
{
p.fun();
}
void fun2(Person p)
{
p.fun();
}
void test1()
{
Person p;
Person* pp = nullptr;
Student s;
Student* ps = nullptr;
p.fun();//A->1
pp = &p;
pp->fun();//A->1
s.fun();//B->2
ps = &s;
ps->fun();//B->2
pp = &s;
pp->fun();//B->1
ps = (Student*)&p;
ps->fun();//A->2
fun1(p);//A->1
fun1(s);//B->1
fun2(p);//A->1
fun2(s);//B->1
}
int main()
{
test1();
return 0;
}
//面试题
class A
{
public:
virtual void fun(int value = 0)
{
cout<<"A->"<<value << endl;
}
virtual void test()
{
fun();
}
};
class B : public A
{
public:
void fun(int value = 1)
{
cout<<"B->"<< value <<std::endl;
}
};
int main()
{
B*p = new B;
p->test();
return 0;
}
面试题解析:main函数中的p调用的是基类继承来的test函数,test有一个隐藏的this指针,这个指针的类型是A*类型的,在通过这个基类的this指针去调用fun函数,即this->fun(),用的是哪个函数的缺省值由指针的类型在编译期间决定,因此可以确定用的是基类的虚函数的参数缺省值,即value=0。又由于是通过指针调用虚函数,而main函数是通过派生类指针调用test函数,这个派生类指针又赋给基类的this指针去调用fun函数,且派生类的虚函数fun进行了重写,在运行期间通过虚函数表找到的是派生类的函数的实现,因此输出:B->0
单继承和多继承的虚函数表
1.单继承的虚函数表
class Base
{
public:
virtual void fun1()
{
cout << "Base:fun1:" << endl;
}
virtual void fun2()
{
cout << "Base:fun2" << endl;
}
void fun3()
{
cout << "Base:fun2" << endl;
}
};
class Derive:public Base
{
public:
virtual void fun1()
{
cout << "Derive:fun1" << endl;
}
virtual void fun4()
{
cout << "Derive:fun4" << endl;
}
};
单继承的虚表大致如下所示:
2.多继承的虚函数表
对于多继承,派生类有多少个直接父类,就有多少个虚函数表,如果派生类自己又添加了一个虚函数,那这个虚函数放在第一继承基类的虚表末尾。
class Base1
{
public:
virtual void fun1()
{
cout << "Base1:fun1:" << endl;
}
virtual void fun2()
{
cout << "Base1:fun2" << endl;
}
};
class Base2
{
public:
virtual void fun1()
{
cout << "Base2:fun1:" << endl;
}
virtual void fun2()
{
cout << "Base2:fun2" << endl;
}
};
class Derive : public Base1,public Base2
{
public:
virtual void fun1()
{
cout << "Derive:fun1" << endl;
}
virtual void fun3()
{
cout << "Derive:fun3" << endl;
}
};
由于菱形继承和菱形虚拟继承在实际中不常见,我们这里就不做研究。