多态指的是完成某一个具体的行为
,使用不同的对象
完成时会出现不同的状态
。例如:买火车票,学生买票是半价,成人买票是全价,军人买票优先。
多态
是在不同继承关系的类对象
,去调用同一函数
,产生了不同的行为。比如Student
继承了Person
。 Person
对象买票全价,Student
对象买票半价。
多态的分类:
声明
时的类型,在编译
时确定(函数重载、泛型编程
)动态地
确定操作所针对的对象(虚函数
)指针或者引用
虚函数
,并且完成了虚函数的重写
虚函数:在类的普通成员函数前加上virtual
关键字。
class Date
{
public:
virtual void Print()
{
cout << _year << "-" << _month << "-" << _date <<endl;
}
};
虚函数的重写:派生类中有一个跟基类的完全相同
虚函数,我们就称子类的虚函数重写了基类的虚函数。完全相同是指:函数名、参数、返回值都相同。另外虚函数的重写
也叫作虚函数的覆盖
。但是虚函数重写存在一个例外:协变
。
协变:重写的虚函数的返回值
可以不同,但是必须分别是基类指针
和派生类指针
或者基类引用
和派生类引用
。
class A{};
class B :public A{};
class Person
{
public:
virtual A* Func()
{
return new A;
}
};
class Stu :public Person
{
virtual B* Func()
{
return new B;
}
};
class Person
{
public:
virtual void Func()
{
cout << "全票" << endl;
}
};
class Stu : public Person
{
public:
void Func()
{
cout << "半票" << endl;
}
};
在派生类中重写的成员函数可以不加virtual
关键字,也是构成重写的,因为继承后基类的虚函数被继承
下来 了在派生类依旧保持虚函数属性,我们只是重写
了它。但是这样做是不规范的写法。
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数
。虽然他们的函数名不相同,看起来违背了重写
的规则,其实并没有,因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
基类指针可以指向派生类的对象(多态性)
,如果删除该指针delete []p
;就会调用该指针指向的派生类析构函数
,而派生类的析构函数又自动调用基类的析构函数
,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定(也就是说和类型有关)
,在删除基类指针时,只
会调用基类的析构函数
而不调用派生类析构函数,这样就会造成派生类对象析构不完全
。所以,将析构函数声明为虚函数
是十分必要的。
普通函数的继承是一种实现继承
,派生类继承了基类函数,可以直接使用该函数
,继承的是函数的实现
。虚函数的继承是一种接口继承
,派生类继承的是基类虚函数的接口
,目的是为了重写
,从而达成多态。总结:C++中实现继承时为了实现继承,而接口继承时为了实现多态。
同一作用域
、函数名相同、参数列表不同(覆盖)
:在基类和派生类两个作用域
、两个函数必须是虚函数
、函数名/参数/返回值必须相同(协变除外)
(隐藏)
:在基类和派生类两个不同
的作用域、函数名相同在虚函数的后面写上=0
,则这个函数为纯虚函数
。包含纯虚函数的类叫做抽象类(也叫接口类
),抽象类不能实例化出对象。 派生类继承后也不能实例化出对象,只有重写纯虚函数
,派生类才能实例化出对象
。纯虚函数规范了派生类必须重写
,另外纯虚函数更体现出了接口继承
。
使用抽象类实现一个简单的多态:
class Car
{
public:
virtual void Func() = 0;//纯虚函数
};
class BMW:public Car
{
public:
virtual void Func()//重写纯虚函数
{
cout << "舒适" << endl;
}
};
class DaBen:public Car
{
public:
virtual void Func()//重写纯虚函数
{
cout << "好看" << endl;
}
};
void Test()
{
Car* pb = new BMW;
pb->Func();
Car* pd = new DaBen();
pd->Func();
}
override:override 修饰派生类虚函数强制完成重写。我们实际使用中可以使用纯虚函数+ override
的方式来强制重写虚函数
,因为虚函数的意义就是实现多态
,如果 没有重写,虚函数就没有意义。
final:final
修饰的类不能被继承
,final
修饰的虚函数不能被重写。和Java
中final功能类似。
class A
{
public:
virtual void Func(){}
private:
int a;
};
int main()
{
cout << sizeof(A) << endl;//8,在32位平台下指针4个字节
return 0;
}
上边的类中除了a成员
,还多一个_vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)
,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表 function)
。一个含有虚函数的类中都至少都有一个
虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表
。
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
virtual void Func2()
{
cout << "A::Func2()" << endl;
}
void Func3()
{
cout << "A::Func3" << endl;
}
private:
int a;
};
class B :public A
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
private:
int b;
};
int main()
{
A a;
B b;
return 0;
}
在下边的监视窗口
中可以看到:
A对象a
中有一个虚函数表指针,指向的这个虚函数表类似于一个函数指针数组
,里边存放的是类A中的两个虚函数地址
。B对象b
中也有一个虚函数表指针,该对象由两部分构成,一部分是父类继承下来的成员,虚表指针存在从父类继承
下来那部分。该虚函数表指针指向的两个虚函数,一个已经变成了派生类B重写的Func1函数
。Func1
完成了重写,所以b的虚表中存的是重写的B::Func1
,所以虚函数的重写也叫作覆盖
,覆盖就是指虚表中虚函数的覆盖
。重写是语法的叫法,覆盖是原理层的叫法。虚函数指针的指针数组
,这个数组最后面放了一个nullptr
。Func2
是虚函数,继承在派生类B中,也在虚函数表中,而A::Func3
不是虚函数,继承下来不在虚函数表中。派生类虚函数表的生成过程:
拷贝
到派生类虚表中重写
了基类中某个虚函数,用派生类自己的虚函数覆盖虚表
中基类的虚函数新增加
的虚函数按其在派生类中的声明次序
增加到派生类虚表的最后虚函数存在哪里?虚函数表存在哪里?
虚函数和普通函数
一样存放在代码段
中,指向虚函数的指针
存在虚函数表
中,在每一个对象中,都存这一个虚表指针
,而不是虚表
。虚表存在哪里我们可以自行写代码验证,我在vs
下验证发现虚表应该是存放在全局域
。(验证方法:可以将虚表的地址打印出来,和各个区域的指针比较可以发现大致在哪个区域)。
class Person
{
public:
virtual void Ticket() = 0;//纯虚函数
};
class Parent :public Person
{
public:
virtual void Ticket()
{
cout << "全票" << endl;
}
};
class Stu :public Person
{
public:
virtual void Ticket()
{
cout << "半票" << endl;
}
};
void Func(Person& p)
{
p.Ticket();
}
int main()
{
Parent p;
Stu s;
Func(p);
Func(s);
return 0;
}
p(parent)
对象时,p.Ticket
在p(parent)
的虚表中找到虚函数是 Parent::Ticket
s
对象时,p.Ticket
在s的虚表中找到虚函数是Stu::Ticket
不同对象
去完成同一行为
时,展现出不同的形态
编译
时确定的,是运行起来
以后到对象的中取找的。不满足多态的函数调用时编译
时确认好的。虚函数覆盖
,一个是对象的指针或引用
调用虚函数前期绑定(早绑定)
,在程序编译
期间确定了程序的行为,也称为静态多态
,比如:函数重载和泛型编程后期绑定(晚绑定)
,是在程序运行
期间,根据具体拿到的类型
确定程序的具体行为,调用具体的函数,也称为动态多态
class A
{
public:
virtual void Func1()
{
cout << "A::Func1" << endl;
}
virtual void Func2()
{
cout << "A::Func2" << endl;
}
private:
int a;
};
class B :public A
{
public:
virtual void Func1()
{
cout << "B::Func1" << endl;
}
private:
int b;
};
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1" << endl;
}
virtual void Func2()
{
cout << "Base::Func2" << endl;
}
private:
int b;
};
class Child :public Base
{
public:
virtual void Func1()
{
cout << "Child::Func1" << endl;
}
virtual void Func3()
{
cout << "Child::Func3" << endl;
}
virtual void Func4()
{
cout << "Child::Func4" << endl;
}
private:
int c;
};
但是vs的监视窗口
只可以看到重写父类的的虚函数和继承于子类的虚函数
,不能看到自己的虚函数。我们可以将各个虚函数的地址打印出来观察:
typedef void(*VFPTR)();
void PrintTable(VFPTR table[])
{
cout << "虚表指针:" << table << endl;
for (int i = 0; table[i] != nullptr; i++)
{
cout << "第" << i << "个虚函数地址:" << table[i] << "->";
VFPTR f = table[i];
f();
cout << endl;
}
}
int main()
{
Base b;
Child c;
//拿出虚表指针赋值给pb
VFPTR* pb = (VFPTR*)(*(int*)&b);
PrintTable(pb);
VFPTR* pc = (VFPTR*)(*(int*)&c);
PrintTable(pc);
return 0;
}
在上图中可以很好的看出单继承中基类和派生类
的虚函数表。
class B1
{
public:
virtual void func1() { cout << "B1::func1" << endl; }
virtual void func2() { cout << "B1::func2" << endl; }
private:
int b1;
};
class B2
{
public:
virtual void func1() { cout << "B2::func1" << endl; }
virtual void func2() { cout << "B2::func2" << endl; }
private:
int b2;
};
class C : public B1, public B2 {
public:
virtual void func1() { cout << "C::func1" << endl; }
virtual void func3() { cout << "C::func3" << endl; }
private:
int c;
};
typedef void(*VFPTR)();
void PrintTable(VFPTR table[])
{
cout << "虚表指针:" << table << endl;
for (int i = 0; table[i] != nullptr; i++)
{
cout << "第" << i << "个虚函数地址:" << table[i] << "->";
VFPTR f = table[i];
f();
cout << endl;
}
}
int main()
{
B1 b1;
B2 b2;
C c;
//派生类存在两个虚表(继承于两个基类),所以存在两个虚表指针8+8+4=20
cout << sizeof(C) << endl;
VFPTR* v1 = (VFPTR*)(*(int*)&c);
PrintTable(v1);
VFPTR* v2 = (VFPTR*)(*(int*)((char*)&c + sizeof(B1)));
PrintTable(v2);
return 0;
}
func1
重写两个基类B1和B2中的func1
。func3
放在第一个虚函数表中,并且是放在最后的。上述代码的对象模型:
注:多继承派生类的未重写的虚函数
放在第一个继承基类部分
的虚函数表中。
答:多态就是使用不同的对象来完成同一种
行为而体现出来的不同的形态
重载:在同一作用域
、函数名相同、参数列表不同
重写(覆盖)
:在基类和派生类两个作用域
、两个函数必须是虚函数
、函数名/参数/返回值必须相同(协变除外)
重定义(隐藏)
:在基类和派生类两个不同
的作用域、函数名相同即可构成隐藏
答:内联函数在调用的时候直接展开,它是不存在地址
的,所以不能放到虚函数表中,不能是虚函数。
答:为了完成接口继承
,强制要求子类对纯虚函数进行重写,为了复用是父类的声明而不是实现
答:静态成员如果是虚函数,要调用它就必须在虚函数表中查找,而虚函数表指针示在对象中的,而静态成员是没有this
指针的,也就是说它找不到虚表指针,所以静态函数不能是虚函数
如果构造函数是虚函数,需要查找虚函数表调用,但是虚函数表示在构造时才创建的
,所以它不能是虚函数。
析构函数建议是虚函数,这样子类重写父类的析构函数,构成多态。如果不构成多态,使用父类的对象接收子类的对象时,在析构时只会析构父类的对象,而不去调用子类的析构,如果子类的对象存在资源管理,就会导致资源泄露的问题。
答:首先如果是普通对象,是一样快
的。如果是指针对象或者是引用对象
,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表
中去查找存在调用开销,所以访问普通函数较快。
答:虚函数表是在编译阶段
就生成的,一般情况下存在常量区的。