多态即多种形态。多态分两种
-
编译时多态:在编译期间编译器根据函数实参的类型确定要调哪个函数(这个我们之前已经接触过了,和函数重载差不多,是同名函数,但是参数不同)
-
运行时多态:在程序运行期间才会去判断到底会调用哪个函数。这里我们主要讲的就是动态多态。
C++怎么实现多态?(动态多态,静态多态,函数多态,宏多态)
- 动态多态通过虚函数和继承实现
- 静态多态通过泛型来实现
- 函数多态通过函数重载来实现
- 宏多态通过宏替换来实现
函数的重写
-
被重写的那个函数一定要是虚函数(即父类中的那个函数必须是虚函数,在父类中是虚函数的情况下,子类中重写的那个函数可以不加上virtual关键字)
-
重写的两个函数一定是一个在父类,一个在子类,子类的虚函数对父类的虚函数进行重写
-
两个函数的函数名和参数列表必须相同,一模一样!
多态的形成条件
-
子类有对父类中的虚函数重写
-
有一个父类的指针/引用来调用重写的虚函数(指向父类调父类的,指向子类调子类的)
注意:如果不构成多态,即不满足上面的两个条件,具体调用哪个函数是根据调用者的类型来确定的。一旦构成多态,具体调用哪个函数则是根据调用者的对象来确定的(因为父类的指针/引用可以指向子类的对象)
通过下面的代码来感受一下多态
class Person
{
public:
virtual void BuyTickets()
{
cout << "普通人全票" << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
virtual void BuyTickets()
{
cout << "学生半票" << endl;
}
};
void Test(Person& p)
{
p.BuyTickets();
}
int main()
{
Person p;
Student s;
Test(p);
Test(s);
return 0;
}
上述的结果则是Test(p)输出“普通人全票”,Test(s)输出“学生半票”,这两个函数构成了多态。
那么,注意,假设这两个函数没有构成重写,即假设,把两个函数virtual关键字去掉,这两个函数其实是构成了重定义,即子类中的函数重定义了父类中的函数。那么这个时候会输出什么呢?
注意这个时候没有构成多态,那么就不看调用者到底是什么对象了,而是看调用者是什么类型。这里Test函数中的参数p是一个Person类型,那么调的就是父类的函数,即输出两个“普通人全票”。
这个时候,我们假设子类中的函数是虚函数,父类中的函数不是虚函数,即子类函数有关键字virtual,父类没有,构不构成多态呢?
注意到这个时候并不构成多态。
那么反过来呢?即父类中函数有virtual关键字,子类函数中没有,会怎样?
这种情况下其实是构成多态的,函数重写是默认支持这样的方式的,即子类中的函数默认支持父类的虚函数属性。但是这样的写法是不被提倡的,最好还是在两个函数中都加上virtual关键字。
关于多态的一些总结
-
派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同(协变除外),注意这里返回值也必须要相同,协变的话返回值可以不同,下面是一个协变的例子
class A
{
public:
virtual A* f1()
{
cout << "A::f1()" << endl;
return this;
}
};
class B :public A
{
public:
virtual B* f1()
{
cout << "B::f1()" << endl;
return this;
}
};
上面的这种情况就是协变,能够构成多态,即一个虚函数返回值可以是父类的指针或引用,一个虚函数可以是子类的指针或引用
-
基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性(即可以不加virtual关键字,但最好加)
-
只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数(静态成员不能被继承)
-
如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
-
构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆
-
不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
-
最好将基类的析构函数声明为虚函数。(这是为了正确地释放子类对象,考虑到A* p = new B();delete p;
如果父类的析构函数不是虚函数,那么delete的时候会去调用父类的析构函数,这样就子类对象中属于子类的那部分就没有被释放(父类那部分调用父类析构函数释放了)。如果父类析构函数是虚函数,那么子类的析构函数会默认继承父类析构函数的虚函数属性,虽然两者的名字不一样,但在这里是一个特殊,在汇编里面的名字其实已经变了的,仍然构成重写。这样的话,如果父类的指针指向的是父类的对象就会调用父类的析构函数,父类的指针指向子类的对象就会调用子类的析构函数,从而构成多态),下面就是析构函数在汇编中的名字
-
虚表是所有类对象实例共用的
重载、重写、重定义的区别
| 返回值 | 函数名 | 作用域 | 参数列表 | 其他 |
重载 | 可以相同,可以不同 | 相同 | 同一作用域 | 必须不同 | 访问修饰符可以不同,可以一个是公有的,一个是私有的 |
重写 | 相同(协变除外) | 相同 | 父子作用域 | 必须相同 | 父类函数必须有virtual关键字
访问修饰符可以不同 |
重定义 | 可以相同,可以不同 | 相同 | 父子作用域 | 可以相同,可以不同 | 在父子作用域中相同名字的,不是重写就是重定义 |
纯虚函数
在成员函数形参后面写上=0,则该成员函数就是纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。这个纯虚函数声明出来就是让子类来重写的,在子类中重写了这个纯虚函数之后,才能够实例化出对象。纯虚函数不用在父类中初始化
class A
{
public:
virtual void f1()=0;
};
class B :public A
{
public:
virtual void f1()
{
cout << "B::f1()" << endl;
}
};
虚表(虚函数表)
对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就是指向虚表的指针(注意和菱形继承虚继承中的虚基表的区别)
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
private:
int _a;
};
我们先看看这个Base类型的对象的大小,我们本身期望的是4个字节(注:这是在32位平台下)
void Test1()
{
Base b;
cout << sizeof(Base) << endl;
}
结果是8个字节,这多出来的4个字节就是指向存放虚表位置的指针。只要该对象模型有了虚函数,那么编译器就会为这个类维护一张存放虚函数的表。注意虚函数并不是放在虚表中的,虚函数还是放在代码段,指向虚函数的指针放在虚表中。
接下来看下重写了虚函数和没有重写虚函数的差别
首先来看下如果没有虚函数的重写
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
};
class Derive :public Base
{
public:
virtual void func3()
{
cout << "Derived::func3()" << endl;
}
virtual void func4()
{
cout << "Derived::func4()" << endl;
}
};
typedef void(*ptr)();//设一个函数指针
void PrintVTable()//通过这个函数来输出函数在内存中分布的地址
{
Base b;
for (int i = 0; i < 2; ++i)
{
ptr p = (ptr)(*((int*)*(int*)&b + i));
p();
cout << ":" << (int*)p << endl;
}
cout << endl;
Derive d;
for (int i = 0; i < 4; ++i)
{
ptr p = (ptr)(*((int*)*(int*)&d + i));
p();
cout << ":" << (int*)p << endl;
}
}
再来看看有虚函数重写的虚表
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
};
class Derive :public Base
{
public:
virtual void func1()//重写父类的虚函数func1
{
cout << "Derived::func1()" << endl;
}
virtual void func4()
{
cout << "Derived::func4()" << endl;
}
};
typedef void(*ptr)();//设一个函数指针
void PrintVTable()//通过这个函数来输出函数在内存中分布的地址
{
Base b;
for (int i = 0; i < 2; ++i)
{
ptr p = (ptr)(*((int*)*(int*)&b + i));
p();
cout << ":" << (int*)p << endl;
}
cout << endl;
Derive d;
for (int i = 0; i < 3; ++i)
{
ptr p = (ptr)(*((int*)*(int*)&d + i));
p();
cout << ":" << (int*)p << endl;
}
}
看完了上面的单继承,再来看看多继承的(给一串代码,给一个模型)
class Base1
{
public:
virtual void fun1()
{
cout << "Base::fun1" << endl;
}
virtual void fun2()
{
cout << "Base::fun2" << endl;
}
private:
int b1;
};
class Base2
{
public:
virtual void fun1()
{
cout << "Base::fun1" << endl;
}
virtual void fun2()
{
cout << "Base::fun2" << endl;
}
private:
int b2;
};
class Derive :public Base1, public Base2
{
public:
virtual void fun1()
{
cout << "Derive::fun1" << endl;
}
virtual void fun3()
{
cout << "Derive::fun3" << endl;
}
private:
int d;
};