目录
1多态概念:
通俗来讲,就是多种形态,同一事物在不同场景下表现出的不同状态。(买票例子)
多态性可以简单地概括为“一个接口,多种方法”。
2多态实现:
c++的多态是在继承的基础上,增加了虚函数,并且让派生类对基类的虚函数进行重写
2.1虚函数
在类的成员函数前添加virtual关键字即可,称该函数为虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
总结:
- 派生类重写基类的虚函数实现多态,要求函数名,参数列表,返回值完全相同(协变除外)
- 基类中定义虚函数,派生类中该函数始终保持虚函数特性。
- 只有类的成员函数才能被定义成虚函数。
- 静态成员函数不能定义成虚函数。
- 如果在类外定义虚函数,只能在声明是加上virtual,类外定义时不能加virtual。
- 构造函数不能定义为虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化
- 不要在构造或者析构函数中调用虚函数,在构造函数或析构函数中对象是不完整的,可能会发生未定义的行为
- 最好把基类的析构函数声明为虚函数(这样我们可以通过基类的指针或引用去释放子类的资源,防止内存泄漏)
2.2重写(覆盖)
重写是针对基类中虚函数的,在派生类中实现一个与基类虚函数原型(返回值类型、函数名字、参数列表)相同的虚
函数,即派生类与基类中虚函数的原型完全相同,才称之为对基类虚函数的重写。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
//基类声明后所有派生类同名函数都继承虚函数的特性
//但是最好在函数前加上vtrtual,增加可读性
但是有两个类外:
1 协变:基类中虚函数返回基类对象的指针或引用,派生类域基类同名虚函数返回派生类的指针或引用,这种情况也构成重写, 但是此时派生类与基类返回值类型不同
2 析构函数:基类中的析构函数如果是虚函数,只要派生类的析构函数显示提供,就构成重写(此种情况派生类与基类虚函数名不同)
2.3重载,覆盖(重写),隐藏(重定义)的区别
隐藏(重定义):隐藏是指派生类的成员函数遮蔽了与其同名的基类成员函数
1:派生类的函数与基类函数重名时,但是参数列表不同,无论是否有virtual,基类的函数在派生列类中被隐藏
2:派生类的函数与基类函数重名时,参数列表相同,基类没有virtual,基类的函数在派生列类中被隐藏
2.4多态的构成条件
- 继承:实现派生类对基类的继承
- 重写:基类中一定要构成虚函数,并且派生类中一定要对基类的虚函数进行重写
- 基类的指针或引用:通过基类对象的指针或引用调用虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void Func(Person &p)
{
p.BuyTicket();
}
void test()
{
Person a;
Func(a);
Student b;
Func(b);
}
(普通调用跟对象类型有关,多态调用跟具体对象有关)
2.5动态绑定与静态绑定
也叫做静态联编和动态联编
静态绑定:又称为前期绑定,在程序编译期间确定了程序的行为(编译阶段就将函数实现和函数调用关联起来),也称为静态多态
静态绑定一定发生在编译期间,编译器根据所传递参数的类型确定具体应该调用那个函数,如果有对应函数就调用,否则可能会发生隐式类型转换或者报错。
动态绑定:又称后期绑定,在程序运行期间(运行时到虚表中寻找调用函数的地址),根据具体拿到的类型确定程序的具体行为(与对象有关),调用具体函数,也称为动态多态
一般情况下都是静态绑定,一旦涉及到多态或是虚函数就必须使用动态联编
(通常:虚函数是动态绑定,非虚函数是静态绑定,缺省参数值也是静态绑定)
C++多态有两种形式,动态多态和静态多态;
动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;
静态多态是通过模板或是重载来实现,因为这种多态实在编译时而非运行时,所以称为静态多态
3抽象类
在成员函数(必须是虚函数)的形参列表后面写上=0,则成员函数为纯虚函数,
包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象,纯虚函数在派生类中重新定义以后,
派生类才能实例化出对象。(一种强制子类重写虚函数的机制)
- c++中包含纯虚函数的类就是抽象类,抽象类不能使用new出对象,只有实现了纯虚函数的子类才能new出对象
- c++纯虚函数更像是“只提供声明,没有实现”是对子类的约数,是接口继承
- c++纯虚函数也是一种“运行时多态”,动态联编
【面试题】
哪些函数不能被定义为虚函数?
1:不能被继承的函数2:不能被重写的函数3:普通函数,友元函数,构造函数,内联成员函数,静态成员函数。
【注意点】
- 虚函数重写:函数必须完全相同,返回值可以不同,但必须是父子类的指针或引用
- 子类重写虚函数可以不加virtual(c++语法坑)
- 普通函数不能定义为虚函数(只有成员函数可以)
- 静态成员不能定义为虚函数(调用不需要对象,没有this指针)
- inline内联函数不能定义虚函数,(调用直接展开,可以理解为没有地址,没有指针)
- 构造函数,拷贝构造函数不能定义为虚函数,operator=(可以,但无意义,也不构成重写)
- 基类定义了static成员,无论派生出多少子类,都只有一个static实例
4带有虚函数对象模型剖析
1: 包含有虚函数的类对象与普通类对象的区别
class B1
{
public:
void TestFunc()
{}
int _b;
};
class B2
{
public:
virtual void TestFunc()
{}
int _b;
};
void TestVirtualFunc()
{
cout<<"sizeof(B1) = "<<sizeof(B1)<<endl;
cout<<"sizeof(B2) = "<<sizeof(B2)<<endl;
}
一个类对象中实际只包括了该类中非静态的成员变量,所以B1和B2大小应该都是4字节,
实际B2增加了虚函数,导致B2多了4个字节
2:虚函数表
带有虚函数的类对象模型中多了4个字节,前4个字节中放置了一个特殊数据。
B2类对象中前4字节所放地址指向的位置中存放的是虚函数的地址。一般将存放
虚函数的位置称作为虚函数表,简称虚表,将指对象前4个字节指向虚表的指针称为虚表指针。
3:打印虚表
typedef void (*PVFT)();
//PVFT类型的指针,指向了返回值和参数的为空的函数指针
void PrintVFT(B2& b, const string& str)
{
cout<<str<<endl;
//&b:从对象前4字节取出虚表地址
//(int*)&b:强制转换为int类型的指针,利用int类型和指针都是4字节长度,获取虚函数指针
//(*(int*)&b):解引用取出虚函数
//(PVFT*)(*(int*)&b):强制类型转换为PVFT类型
PVFT* pVFT = (PVFT*)(*(int*)&b);
while(*pVFT)
{
// 调用该虚函数
(*pVFT)();
++pVFT;
}
cout<<endl;
}
结论:基类虚函数在虚表中的次序与其在类中生命次序一致。(按顺序连续存放最后空地址)
5基类与派生类虚表中内容
先将基类中的虚表内容拷贝一份到派生类虚表中。
如果派生类重写了基类中某个虚函数,用派生类自己的虚函数替换虚表中基类的虚函数。
派生类自己新增加的虚函数按其在派生类中的生命次序增加到派生类虚表的最后。
6对象模型
1:虚函数调用过程
2:单继承
3:多继承
多继承:子类的虚函数放在第一个继承类的虚函数表中