目录
7.析构函数的重写-析构函数名统一会被处理成destructor()
(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到
即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)
1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
2、父类指针或者引用去调用虚函数。
(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数

(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func

-
- class A
- {
- public:
- virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
- virtual void test(){ func(); }
- };
-
- class B : public A
- {
- public:
- void func(int val = 0){ std::cout << "B->" << val << std::endl; }
- };
-
- int main(int argc, char* argv[])
- {
- B*p1 = new B;
- //p1->test(); 这个是多态调用,下有讲解 二->6
- p1->func(); //普通调用
-
- A*p2 = new B;
- p2->func(); //多态调用
-
- return 0;
- }
- class Person {
- public:
- Person(const char* name)
- :_name(name)
- {}
-
- // 虚函数
- virtual void BuyTicket()
- {
- cout << _name << "Person:买票-全价 100¥" << endl;
- }
-
- protected:
- string _name;
- //int _id;
- };
-
- class Student : public Person {
- public:
- Student(const char* name)
- :Person(name)
- {}
-
- // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
- virtual void BuyTicket()
- {
- cout << _name << " Student:买票-半价 50 ¥" << endl;
- }
- };
-
- void Pay(Person& ptr)
- {
- ptr.BuyTicket();
- }
-
- int main()
- {
- string name;
- cin >> name;
- Student s(name.c_str());
- Pay(s);
- }

- class Person {
- public:
- Person(const char* name)
- :_name(name)
- {}
-
- // 虚函数
- virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }
-
- protected:
- string _name;
- //int _id;
- };
-
- class Student : public Person {
- public:
- Student(const char* name)
- :Person(name)
- {}
-
- // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
- virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
- };
-
- class Soldier : public Person {
- public:
- Soldier(const char* name)
- :Person(name)
- {}
-
- // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
- virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
- };
-
- // 多态两个要求:
- // 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
- // 2、父类指针或者引用去调用虚函数。
-
- //void Pay(Person* ptr)
- //{
- // ptr->BuyTicket();
- //}
-
- void Pay(Person& ptr)
- {
- ptr.BuyTicket();
- }
-
- // 不能构成多态
- //void Pay(Person ptr)
- //{
- // ptr.BuyTicket();
- //}
-
- int main()
- {
- int option = 0;
- cout << "=======================================" << endl;
- do
- {
- cout << "请选择身份:";
- cout << "1、普通人 2、学生 3、军人" << endl;
- cin >> option;
- cout << "请输入名字:";
- string name;
- cin >> name;
- switch (option)
- {
- case 1:
- {
- Person p(name.c_str());
- Pay(p);
- break;
- }
- case 2:
- {
- Student s(name.c_str());
- Pay(s);
- break;
- }
- case 3:
- {
- Soldier s(name.c_str());
- Pay(s);
- break;
- }
- default:
- cout << "输入错误,请重新输入" << endl;
- break;
- }
- cout << "=======================================" << endl;
- } while (option != -1);
-
- return 0;
- }
- void Pay(Person* ptr) //指针调用可以
- {
- ptr->BuyTicket();
- }
-
- void Pay(Person& ptr) //引用调用可以
- {
- ptr.BuyTicket();
- }
-
- // 不能构成多态
- //void Pay(Person ptr) //传值调用不可以
- //{
- // ptr.BuyTicket();
- //}
虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
- class A{};
- class B : public A {};
-
- // 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
- //
- class Person {
- public:
- virtual A* f() {
- cout << "virtual A* Person::f()" << endl;
- return nullptr;
- }
- };
-
- class Student : public Person {
- public:
- // 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
- // 重写父类虚函数实现
- // ps:我们自己写的时候子类虚函数也写上virtual
- // B& f() {
- virtual B* f() {
- cout << "virtual B* Student::f()" << endl;
- return nullptr;
- }
- };
- int main()
- {
- Person p;
- Student s;
- Person* ptr = &p;
- ptr->f();
-
- ptr = &s;
- ptr->f();
-
- return 0;
- }


p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1
- class Person {
- public:
- virtual ~Person() {cout << "~Person()" << endl;}
- };
- class Student : public Person {
- public:
- virtual ~Student() { cout << "~Student()" << endl; }
- };
- int main()
- {
- Person* p1 = new Person;
- Person* p2 = new Student;
- delete p1;
- delete p2;
- return 0;
- }
- class Person {
- public:
- virtual ~Person()
- {
- cout << "~Person()" << endl;
- }
- };
-
- class Student : public Person {
- public:
- // Person析构函数加了virtual,关系就变了
- // 重定义(隐藏)关系 -> 重写(覆盖)关系
- virtual ~Student() //这里virtual加不加都行
- {
- cout << "~Student()" << endl;
- delete[] _name;
- cout << "delete:" << (void*)_name << endl;
- }
-
- private:
- char* _name = new char[10]{ 'j','a','c','k' };
- };
-
- int main()
- {
- // 对于普通对象是没有影响的
- //Person p;
- //Student s;
-
- // 期望delete ptr调用析构函数是一个多态调用
- // 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
- Person* ptr = new Person;
- delete ptr; // ptr->destructor() + operator delete(ptr)
-
- ptr = new Student;
- delete ptr; // ptr->destructor() + operator delete(ptr)
-
- return 0;
- }


(2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错
示例:如果父类没写virtual能检查出来并报错

函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。
重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。
- 抽象类 -- 在现实一般没有具体对应实体
- 不能实例化出对象
- 间接功能:要求子类需要重写,才能实例化出对象
- class Car
- {
- public:
- virtual void Drive() = 0;
- // // 实现没有价值,因为没有对象会调用他
- // /*virtual void Drive() = 0
- // {
- // cout << " Drive()" << endl;
- // }*/
- };
- 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();
- }
被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
和菱形虚拟继承的虚基表不一样,那个存的是偏移量
- // 这里常考一道笔试题:sizeof(Base)是多少?
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Func1()" << endl;
- }
- private:
- int _b = 1;
- };
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Base::Func1()" << endl;
- }
-
- virtual void Func2()
- {
- cout << "Base::Func2()" << endl;
- }
-
- void Func3()
- {
- cout << "Base::Func3()" << endl;
- }
-
- private:
- int _b = 1;
- };
-
- class Derive : public Base
- {
- public:
- virtual void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
-
- void Func3()
- {
- cout << "Derive::Func3()" << endl;
- }
- private:
- int _d = 2;
- };
-
- int main()
- {
- cout << sizeof(Base) << endl;
- Base b;
-
- cout << sizeof(Derive) << endl;
- Derive d;
-
- Base* p = &b;
- p->Func1();
- p->Func3();
-
- p = &d;
- p->Func1();
- p->Func3();
-
- // /*Base& r1 = b; 引用也是多态调用
- // r1.Func1();
- // r1.Func3();
- //
- // Base& r2 = d;
- // r2.Func1();
- // r2.Func3();*/
- }

--语法层的概念: 派生类对继承基类虚函数实现进行了重写
--原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数
无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。
Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址

C++语言的多态性分为编译时的多态性和运行时的多态性
①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。
②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载和模板实现。
-在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
- class Person {
- public:
- virtual void BuyTicket() { cout << "买票-全价" << endl; }
-
- void Buy() { cout << "Person::Buy()" << endl; }
- };
-
- class Student : public Person {
- public:
- virtual void BuyTicket() { cout << "买票-半价" << endl; }
-
- void Buy() { cout << "Student::Buy()" << endl; }
- };
-
- void Func1(Person* p)
- {
- 跟对象有关,指向谁调用谁 -- 运行时确定函数地址
- p->BuyTicket();
- 跟类型有关,p类型是谁,调用就是谁的虚函数 -- 编译时确定函数地址
- p->Buy();
- }
- int main()
- {
- Person p;
- Student s;
-
- Func1(&p);
- Func1(&s);
-
- return 0;
- }


重点总结:
多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Base::Func1()" << endl;
- }
-
- virtual void Func2()
- {
- cout << "Base::Func2()" << endl;
- }
-
- void Func3()
- {
- cout << "Base::Func3()" << endl;
- }
-
- private:
- int _b = 1;
- };
-
- class Derive : public Base
- {
- public:
- virtual void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
-
- void Func3()
- {
- cout << "Derive::Func3()" << endl;
- }
- private:
- int _d = 2;
- };
-
- int main()
- {
- cout << sizeof(Base) << endl;
- Base b;
-
- cout << sizeof(Derive) << endl;
- Derive d;
-
-
- // 父类赋值给子类对象,也可以切片。为什么实现不了多态?
- Base r1 = b;
- r1.Func1();
- r1.Func3();
-
- Base r2 = d;
- r2.Func1();
- r2.Func3();
-
- return 0;
- }


我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1


- class Derive : public Base
- {
- public:
- // 重写
- virtual void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
-
- void Func3()
- {
- cout << "Derive::Func3()" << endl;
- }
-
- virtual void Func4()
- {
- cout << "Derive::Func4()" << endl;
- }
- private:
- int _d = 2;
- };
-
- // 取内存值,打印并调用,确认是否是func4
- //typedef void(*)() V_FUNC; // 不支持这种写法
- typedef void(*V_FUNC)(); // 只能这样定义函数指针
-
- // 打印虚表
- //void PrintVFTable(V_FUNC a[])
- void PrintVFTable(V_FUNC* a)
- {
- printf("vfptr:%p\n", a);
-
- for (size_t i = 0; a[i] != nullptr; ++i) //VS下的虚表以空指针结束
- {
- printf("[%d]:%p->", i, a[i]); //打印虚表中的所有函数的地址
- V_FUNC f = a[i]; //调用函数中打印函数,可以知道是哪个func函数
- f();
- }
- }
-
- int c = 2;
-
- int main()
- {
- Base b;
- Derive d;
- PrintVFTable((V_FUNC*)(*((int*)&d))); //下有解释
- }
PrintVFTable((V_FUNC*)(*((int*)&d))); 解释:
因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参
监视窗口和内存窗口:


一个类型公共一个虚表,所有这个类型对象都存这个虚表指针

不可能存在栈,栈区是建立栈帧,出作用域栈帧销毁, 虚表是一个永久的存在,排除栈。
不可能存在堆区,堆区是动态申请,最后动态释放
可以存在静态区或者常量区,最可能存在常量区。通过下面打印地址可见虚表存储地址离常量区地址最近


大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上
- 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(*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;
- }
- int main()
- {
- printf("%p\n", &Derive::func1);
-
- Derive d;
- //PrintVTable((VFPTR*)(*(int*)&d));
- PrintVTable((VFPTR*)(*(int*)&d));
- PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
- }
PrintVTable((VFPTR*)(*(int*)&d));
因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参
PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )



结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值
(1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上
(2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
(3)友元函数不属于成员函数,不能成为虚函数
(4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数)
(5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)