文章目录
多态
静态的多态:函数重载
比如,cout函数流插入流提取时重载,调用的是不同的重载函数.调用看起来相同的函数有不同的行为.编译时实现.
动态的多态
一个父类对象的引用或者指针去调用同一个函数,传递不同的对象,会调用不同的函数。
本质就是不同类型的对象去做同一件事请,结果不同。
子类中函数名相同,参数相同,返回值相同,会构成隐藏.并且如果是虚函数,virtual在函数之前,叫做重写(覆盖)
模板实例化出不同的函数重载是静态多态
构成多态,传的是哪个类型的对象,调用的就是这个类型的虚函数,跟对象有关.
不构成多态,调用的就是形参p类型的函数,跟类型有关。
不存在类型转换,只是切片,成为父类继承部分的引用。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
动态多态的条件
必须是基类的指针或者引用调用虚函数.虚函数必须是派生类对基类进行了重写。
重写的条件:
虚函数+满足“三同”
虚函数:被virtual修饰的成员函数.只能是类的非静态成员函数才是虚函数.
virtual 关键字只在声明时加上,在类外面实现时不能加.
static和virtual是不能同时使用的。
重写要求反回值相同,但是有一个例外:协变(要求返回值是任意两个父子关系的引用或者指针).
//也构成多态
class A {};
class B :public A {};
class Person
{
public:
virtual A& BuyTicket()
{
cout << "买票-全价" << endl;
A a;
return a;
}
};
class Student : public Person
{
public:
virtual B& BuyTicket()
{
cout << "买票-半价" << endl;
B b;
return b;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
析构函数
析构函数是不是虚函数都能正确调用析构函数,自己调用自己的析构函数不涉及基类指针调用子类对象等多态的条件,不构成多态.
如果析构函数被设置为虚函数,是构成重写的.因为不同的析构函数名字都被特殊处理为destructer同名.
如果不是虚函数是构成隐藏的。
对于普通对象,是否是虚函数都完成重写并且正确调用了
- 动态申请的父子对象+如果给了父类指针管理,那么需要析构函数是虚函数.
- 完成重写,构成多态,那么才能正确调用析构函数
需要虚函数场景代码:
Person* p1=new Person;//先operator new +构造函数
Person* p2=new Student;
// 先析构函数+operator delete
delete p1; // p1->destructor()
delete p2; // p2->destructor()
如果不构成多态,就看类型,此时p1调用person,p2也调用person去析构.但是我们想让p2去调用~Student(),就需要让二者构成多态.
不太规范的是:
虚函数的重写允许两个都是虚函数,或者父类的是虚函数,再加上满足三同,就可以构成重写,我们建议都写上virtual。加上父类指针Person*
的调用,实现了多态.
-
虽然子类没写,但是先继承的父类的虚函数之后完成得重写,他也算是虚函数。
-
尽管子类中的是私有的,也构成虚函数,继承父类的所有属性。多态可以调用私有,因为是直接去虚表里面找的,你这限定符没用啊.算是一个漏洞.
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
//加不加,私有与否都构成多态
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p1 = new Person();
Person* p2 = new Student();
Func(*p1);
Func(*p2);
delete p1;
delete p2;
}
- 本质上,子类重写的虚函数,可以不加virtual是因为考虑到析构函数的特殊性导致的,初衷是父类析构函数加上
virtual
,那么就不存在不构成多态,因为没调用子类析构函数造成内存泄漏场景.所以决定子类不加virtual也可以.建议,自己写都加上没毛病。 - 虚函数的作用就是用来实现多态的.模板属于编译时多态.
多态原理层
关键字
final用途
-
如何设计一个不能被继承的类?
-
C++98将构造函数搞成私有,在子类中不可见,子类构造不出对象出来了,子类构造函数无法调用父类的构造函数初始化父类的内一部分对象.
但是这样父类在外面构造对象也不能调用此时是私有的构造函数,我们可以用单例模式.父类外不可见,但是类内可见构造函数.
static A CreateOBJ(int a=0)//static解决先有鸡蛋的问题 //因为不加static的话是普通成员函数,成员函数的调用是需要对象来调用的. { return A(a); } int main() { A aa=A::CreateOBJ(10); //这样实现不用对象就可以调用函数初始化A类型。 }
-
C++11直接限制.类名后面加上 final.
-
-
限制重写:
override
放在子类重写的虚函数的后面,帮助检查是否实现重写,没有重写就会报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override
{cout << "Benz-舒适" << endl;}
};
重载-重写(覆盖)-隐藏(重定义)
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类
不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯
虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承.
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
纯虚函数一般是只声明不实现的,没有实现的价值.因为这个类不能实例化出对象,但是你可以定义指针.
-
为什么纯虚函数没有实现的价值?
这个类没有对象,没法用对象调用。可以用指针调用,但是会出现崩溃,空指针调用出现内存错误。
class Car
{
public:
virtual void Drive() = 0
{
cout << "virtual void Drive() = 0" << endl;
}
void f()
{
cout << "void f()" << endl;
}
};
void Test()
{
Car* p = nullptr;
//p->Drive();//崩溃
p->f();//void f(),只是把nullptr传给this,不解引用就不会出现问题.
//在公共代码区
}
- 哪个类适合设计为抽象类(包含纯虚函数)?
一个类型,在现实世界中没有具体的对应实物就定义为抽象类比较好,强制了子类去完成虚函数的重写,完成多态。纯虚函数更体现出接口继承。继承是实现继承,并不重写。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数
虚函数表
有虚函数,类的大小就会发生变化,多了虚函数表指针放在前面,相对地址从0开始。虚函数表指针指向的是一个指针数组,里面的指针指向的是虚函数。
- 如果有两个虚函数
虚表->多态
虚函数的重写/覆盖:
当传的是父类类型的对象,就会调用父类的虚表指针,找到虚函数
当传的是子类类型的对象,会发生切片,引用的仍然是继承下来的父类的部分虚表指针
但是虚表指针指向的虚函数发生重写,所以会实现多态
所以,多态的原理:基类的指针或者引用,指向谁,就去谁的虚函数表里面找到相应位置的虚函数进行调用.
看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
反汇编的指令都是相同的.
-
为什么不用对象实现多态?
拷贝构造时,
不会将虚表指针拷贝过去,内个虚表里面一直放的就是父类的虚表,没有办法实现虚函数的重写从而实现多态。指针和引用会直接成为继承父类内一部分的别名,不发生拷贝。对象切片:需要将子类的拷贝过去.虚表指针并不会赋值过去,存的一直是父类的虚表指针,但是你以为是子类的,造成混乱.
引用切片:不需要拷贝虚表内容的过程,直接是子类中,父类那一部分的别名(指针也一样)
-
同类型的两个对象的虚表指针是一样的,一个类按理说只有一个虚表.
-
普通函数和虚函数存储的位置是是否一样?普通的成员函数是编译时直接确定地址,因为在代码段.
虚函数只是虚函数要把地址存一份到虚表,方便实现多态.需要一个找的过程,是在运行时确定虚函数地址的.
-
如果不是多态,就编译时确定地址。符合多态的两个条件,才会到虚函数表中去找,再调用。多态在编译的时候是不能确定调用的是哪个函数。
-
强制调用虚函数也是有办法的。
-
虚函数表是存在于哪里?vs下常量区(不能因为某一个对象或者函数的结束而销毁)
-
如何取对象的头4个字节或者8个字节呢?
将类型强转为int类型再解引用,16进制进行打印就是指针类似.
-
单继承虚函数表
单继承时,打印虚表或者查看内存地址来查看监视下不可见的虚函数
内存角度:
虚函数表里面存的地址不是虚函数的真实地址,而是反汇编时jump跳转的地址
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
Base* p1 = &b;
p1->func1();
p1 = &d;
p1->func1();//发生重写,构成多态
}
多继承虚函数表
-
内存中查看到, 重写的func1放到了第一张虚函数表里面.
-
两个父类相同的func1函数,在子类中重写后,后续调用的是一个函数,虚表里面存放的仍然是两个jump指令内个地址,所以虚表里面的地址会有所不同。
- 为什么要有这个虚拟地址呢?为了完成一些准备工作,修正ecx的this指针的问题。最终还是调到了这个地址的虚函数.
强化
多态,虚表里面,指针或者引用指向谁就调用谁。
构成多态,运行时决议。不构成多态,编译时决议。
当我们是非指针或者引用,而是对象调用就是不构成多态的就是编译时决议,编译时决定调用谁。
函数重写(逻辑上),覆盖(实际上)
子类的虚函数直接覆盖从父类继承下来的父类的虚函数,从而构成多态。
- 虚表的指针何时初始化?在构造函数的初始化列表的时候。
子类和父类,在子类中重写继承的func1,添加未重写的func2,隐藏自己的func3
打印虚表
单继承
- 那如何取出续表里面的func3?–就是打印子类的虚函数表。
虚表本质上就是一个指针数组,所以我们要先获得子类中的前4个字节,那里是存放VF_PTR.也就是指向指针数组的指针,然后像我们取出数组元素一样,取出这个指针数组里面的指针元素
注意:虚表最后为nullptr,遍历时的截止条件。
32位机器:
64位机器:需要将int ->longlong
我们得到虚函数的地址,就可以根据地址直接调用虚函数.我们这是一种非法的方式调用的虚函数,所以尽管虚函数在类里面是private,也可以调用到。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
//定义一个函数指针typedef void (*pfun)(void);//pfun 类型是 void(*)(void)
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
void test1()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(long long*)&d);
//VFPTR* vTableb1 = (VFPTR*)(*(void**)&d);//2级指针,在32位下是4字节,64位下是8字节,自适应
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(long long*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
}
void test2()
{
Derive d;
#ifdef _WIN64//条件编译完成自适应
VFPTR* vTableb1 = (VFPTR*)(*(long long*)&d);
#else
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
}
int main()
{
test1();
test2();
return 0;
}
用类声明对象再调用的正规方式并不是一回事,虚表打印时的this指针也和对象不是一回事。
long long在32位下是8字节,在64位下才是8字节.然后VF_PTR*又是强转为4字节.相关类型之间才可以转换。整形和指针也是可以转换的,因为他俩也是相关的。
多继承
有两份虚表,先继承的放前面,自己的虚函数放在虚表的哪里呢?
直接打印虚表,打印的是Base1内个虚表。
//要打印Base2的,就要跳过Base1的大小。但是要注意,对象指针+1 跳过的是一个对象,造成越界。所以要强转为char*
PrintVF_table((VF_PTR*)*(void**)((char*)&d+sizeof(Base1)));//或者,采用切片的时候,自动确认到两个虚表的位置
Base1* p1=&d;
Base2* p2=&d;
要点陈列
-
有纯虚函数的类叫做抽象类,他不能用来定义对象,一半用于接口的定义。
-
子类不实现父类的所有纯虚函数,则子类还属于抽象类,仍然不能实例化对象。
-
抽象类可以申明函数指针和引用,目的是父类实现多态。
-
一个类的不同对象共享该类的虚表,虚表是在编译时期生成的。
-
虽然子类重写了父类的虚函数,但只要是用对象去调用,则只能调用相对类型的方法子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚表不需要存储,因为存储了也不能调用
-
虚函数的继承,是接口继承,重写的是函数的实现,接口还是用父类的。
-
子类接口写什么都不重要。
虚函数是在虚表里面的,跟私有公有无关。
test()不构成重写,在子类调用的时候找继承下来的父类的函数。
父类类型的指针,调用的是子类,构成多态。 -
内联函数不可以是虚函数,前面可以加上virtual 如果构成多态,加入到虚表了,忽略内联函数的自身属性,在调用的地方直接被替换不需要函数地址。如果不构成多态,这个函数就仍然是内联函数。
-
类里面定义的默认是内联函数,
virtual和static不能一起使用。静态成员函数没有this指针,无法访问虚表。 -
构造函数不能是虚函数,虚函数的意义是构成多态调用,对象中的虚函数表指针是在构成函数初始化列表阶段才初始化的。
-
如果不构成多态,都是编译器确定调用函数的地址,他们一样快。虚函数和普通函数.如果是多态,那么就是普通函数更快。
-
指针的类型决定他指向的部分大小。切片时发生指针偏移,指向属于他类型的那一部分。子类指针调用虚函数不构成多态。
-
重写即覆盖,针对多态;重定义是隐藏,两者都发生在继承体系之中
-
重载必须在一个作用域当中,不能再不同的类当中
-
重写需要函数完全相同,重定义(隐藏)只需要函数名相同即可
-
通过父类对象调用的都是父类的方法
-
友元函数不属于成员函数,所以不能是虚函数
-
静态成员函数与具体对象无关,属于整个类.
-
-
核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,
-
此时没有this无法拿到虚表(),就无法实现多态,因此不能设置为虚函数。
-
-
析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数。
-
实现多态是需要付出代价的,比如虚表,虚指针等,所以不实现多态就不要有虚函数了。
-
编译期间无法知道基类的指针或者引用到底运用那个累的对象.运行时才知道,然后通过虚表对应的虚函数实现多态。
函数不能是虚函数,虚函数的意义是构成多态调用,对象中的虚函数表指针是在构成函数初始化列表阶段才初始化的。
-
如果不构成多态,都是编译器确定调用函数的地址,他们一样快。虚函数和普通函数.如果是多态,那么就是普通函数更快。
-
指针的类型决定他指向的部分大小。切片时发生指针偏移,指向属于他类型的那一部分。子类指针调用虚函数不构成多态。
-
重写即覆盖,针对多态;重定义是隐藏,两者都发生在继承体系之中
-
重载必须在一个作用域当中,不能再不同的类当中
-
重写需要函数完全相同,重定义(隐藏)只需要函数名相同即可
-
通过父类对象调用的都是父类的方法
-
友元函数不属于成员函数,所以不能是虚函数
-
静态成员函数与具体对象无关,属于整个类.
-
-
核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,
-
此时没有this无法拿到虚表(),就无法实现多态,因此不能设置为虚函数。
-
-
析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数。
-
实现多态是需要付出代价的,比如虚表,虚指针等,所以不实现多态就不要有虚函数了。
-
编译期间无法知道基类的指针或者引用到底运用那个累的对象.运行时才知道,然后通过虚表对应的虚函数实现多态。
-
假设重写成功,必须是父类的指针或者引用调用虚函数才可以实现多态.
易错题目: