首页 > 编程语言 >C++多态

C++多态

时间:2024-02-27 23:02:36浏览次数:27  
标签:虚表 函数 cout 多态 C++ 父类 指针

/** 多态/动态调用
*
*
* 1.虚函数/虚拟函数
* 语法:在函数前面加上virtual
*
* 1.1虚函数与虚继承没有关系,只是共用了关键字
*
* 2.重写/覆盖:虚函数且函数名字类型等完全一样
* $注:虚函数是覆盖,不是虚函数才构成隐藏
* $重写:指对函数体重写,函数体外的是直接使用父类的,接口继承;重写只有虚函数才有,非虚函数的叫做隐藏
* $只有虚函数才可以重写,非虚函数不能修改继承下来的函数体
*
* 2.1 虚函数重写的条件
* $三同:函数名,参数(平常说的参数都是说参数的类型,与缺省参数无关),返回值都要相同
*
* 2.2 重写体现 <<接口继承>>
*
* 2.3 接口继承
* $ 继承下来的函数的函数体外所有东西都是父类的,只有函数体是对象的
*
* 3.多态的条件(有多态的效果)
* a.虚函数的重写 -- 多态的基础
* b.父类的指针或引用去调用 -- 很重要,必须是父类类型的指针或引用(接收父类对象或子类对象)的对象去调用虚函数 -- 直接定义的对象不可以
* $一个不满足都实现不了多态
* $但是 只要是父类的指针或引用(指针引用的是父类对象也是)调用都进虚表中找(非多态,而是多态调用) -- 傻瓜式
*
* 3.1 多态对象和普通对象的区别
* $不满足多态时,只看类型
* $满足多态时,看指向对象的类型
*
* 4.必须使用多态的场景
* a.继承中对析构函数的特殊处理场景
* $引入:继承中析构的特殊处理引入 Destructor
* &如果是父类指针接收子类类型,则析构函数调用父类的,没调子类,
* $析构特殊处理后,构成隐藏的析构函数,类型是谁就调用谁的析构,目前不能解决
* $多态,虚函数,父类指针或引用调用虚函数,->彻底解决继承的析构问题
*
* 5.虚函数的例外
* a.子类可以不写virtual(重点记忆) -- 虚函数可以继承(隐式) -- 体现接口继承
* b.协变covariant -- 返回值可以不同,但必须是父子关系的指针或引用,可以是nullptr,不能是对象
*
* 5.1 协变 - 目前用途比较少 后续遇到再学习
*
* 6.多态的特殊继承
* a.发生多态后,会继承父类的缺省参数,即子类缺省参数无效 -- 非多态下,子类继承下来的成员与父类无关
* $ a的解释:多态的继承是一种接口实现,即函数体外所有东西都是父类的,只有函数体是对象的,多态继承的函数叫重写;重写:即重写函数体
*
* 7 说明符final (说明符不是关键字)
* 7.1 final的功能
* a.final可以修饰类,表示|最终类|,即不允许被继承的类
* b.final可以修饰虚函数,使虚函数不能被重写(可以继承) //但是很矛盾,虚函数的意义就是可以重写,重写的意义就是实现多态
*
* 8 说明符override:重写说明符
* 8.1 override的功能
* a.辅助用户检查是否完成重写; 语法:加载函数参数列表的括号后(),函数体花括号{}前: (//...)override{//...};
*
*/

//单继承虚表原理
/**
* 1.虚函数表指针(virtual function table pointer) /也叫虚表指针
* 如果在类中定义了虚函数,则对象中会增加一个指针,叫虚函数表指针__vfptr,虚函数表指针在成员的前面,直接占了4/8字节
* 虚表中存放着对象各自的虚函数
*
* 1.1.虚函数表/虚表
* 虚函数表本质是一个虚函数指针数组,由虚函数的数量决定大小,元素中虚函数指针顺序由声明顺序决定,
* $ 虚表数组最后面放了一个nullptr(VS才有,g++没有)
* $虚表在编译过程就生成了,每个类都有自己的虚表,虚表指针在构造函数中完成初始化
*
* 1.2 虚函数的另一个名字|覆盖|的来由
* $ 重写后就把重写后的函数的地址覆盖掉虚表里原来的未重写的地址:
* $ 不重写则地址一样,直接从父类拷贝过来
* $ 继承下来的,但不是虚函数的,不会进虚表
*
* 2.多态的原理:
* $ 父类对象和子类对象的虚表中存放了各自的虚函数
* 如果不是多态,则在编译时就已经确定调用各自的函数
* 如果是多态,则会在各自的虚表中去调用对应的虚函数,虚函数也不知道自己调用的是哪个对象的,因为切片,只要是多态,就会到虚表中去找,调用
*
* 3.由原理引出
* $ 为什么多态一定要父类的指针或引用:因为指针或引用能接收不同对象,再去找各自对象的虚表
* $ 为什么多态必须要重写: 因为有虚表存在,使各个对象都有自己的重写/覆盖,多态能根据虚表去找到对应的函数
*
* 4.虚函数表的特性
* a.子类会继承父类的虚函数表(开辟一个新的数组,浅拷贝)
* b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数,如果子类没有重写,则虚函数表和父类的虚函数表的元素完全一样,
* c.派生类自己新增加的虚函数,从继承的虚表的最后一个元素开始,按其在派生类中的声明次序增加到派生类虚表的最后。
* $ 派生类自己的虚函数放在继承的虚表的后面,没有继承就顺序从头开始放,总而言之,自己的虚函数位置一定比继承的虚函数位置后
* d.虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针
* e.虚表是在编译阶段就完成了,在初始化列表完成的是虚表指针的初始化
*
* 5.针对直接定义的对象分析
* a.同一类型直接定义的对象共享同一个虚表
* b.子类对象直接赋值给父类对象后就变成了父类对象,只拷贝成员,不拷贝虚表,虚表还是父类的
* 因为父类对象中有子类对象的虚表不合理,有的话会分不清是父类还是子类的虚表,父类对象一定是父类的虚表,子类对象一定是子类的虚表
*
*/

/** 多继承虚表
* 1.子类会继承所有父类的虚表,有多少个父类就有多少个虚表
* $ 自己的虚函数在VS中放在第一个继承的虚表的后面. (没有重写的虚函数进第一个继承的虚表)
*
* 2.当多继承的父类中有相同的虚函数时(两个父类都有fun1虚函数),子类重写会把相应的继承下来的父类的虚表都覆盖(合逻辑)
*
* $$ 3的总结:1.调用后面继承的父类虚函数时需要重定向this 2.后继承的父类的虚表可能会有虚函数指针不一样(被VS封装过)$$
* 3.在调用重写过的后继承的父类的虚函数时,如果是父类间相同的虚函数.会发生重定向(重新调整this)
* $ 相同虚函数的后继承的父类的虚表中该虚函数的指针是不一样的,原因是VS做了一些改动.
* $ 最先被继承的父类是正常的
* $ 其他后面继承的都需要重新定向this才能调用
*
*/

/** 与虚函数结合的其他语法情况
*
* 1.内联函数inline 和 虚函数 virtual
* $ 虽然: inline如果被编译器识别成内联函数,则该函数是没有地址的. 与 发生多态的虚函数要放入虚表冲突
* $ 事实: inline 和 virtual 可以一起使用 :
* 这取决于使用该函数的场景:如果发生多态,则编译器会忽略掉内联.如果没有发生多态,才有可能成为内联函数
* $ 即:多态和内联可以一起使用,但同时只能有一个发生
*
* 2.静态成员函数不能是虚函数,因为静态成员函数没有this指针,与多态发生条件矛盾(父类引用/指针去调用),且使用类::函数时无法访问虚函数表
* $ 报错:virtual和static不能一起使用
*
* 3.构造,拷贝构造不能是虚函数,赋值运算符重载建议不写成虚函数(编译器不报错),析构函数建议是虚函数
* $ 构造函数他们要帮助父类完成初始化,是一起的,不能像多态那样非父即子(父对象调父的,子对象调子的)
* 虚表指针初始化在构造函数的初始化列表中完成的,肯定是得先有构造函数才有虚表,才能有虚函数
* 并且构造函数多态没有意义
*
*
*/

/** |多态调用|
* 父类对象的父类类型的指针或引用 去调用自己的虚函数会被判定成多态调用,
* 就是进虚表嘛,自己的指针或引用调自己的虚函数,自己的虚函数丢进虚表里,方便多了
* $ 只要是父类的指针调用都进虚表中找
* $ 对象调用和普通函数一样
*/


#include<string>
#include<iostream>
using std::cout;
using std::endl;
using std::cin;
using std::string;

//test 入门演示
namespace test1
{

class Person
{
public:
//虚函数:在函数前面加virtual
virtual void BuyTicket() { cout << "买票-全价" << endl; }

virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
//重写/覆盖:虚函数且函数名字类型等完全一样
virtual void BuyTicket() { cout << "买票-半价" << endl; }

virtual ~Student() { cout << "~Student()" << endl; }
};

//测试用例

//多态的用法
void fun(Person& p)
{
p.BuyTicket();
}
void test_polymorphism1()
{
Person p;
Student s;
fun(p);
fun(s);
}

void test_polymorphism2()
{
//Person p;
//Student s;
cout << " ================ " << endl;

//多态中析构函数有virtual和无virtual区别 --
Person* p1 = new Person();
Person* p2 = new Student();
delete p1;
delete p2;
}

 

}

//单继承打印虚表
namespace test2
{
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;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};

////程序打印虚表
///
typedef void(*VF_PTR)();//重命名一个函数指针,要用函数指针,需要重命名//定义一个虚函数指针
void PrintVFTable(VF_PTR* p) //形参:虚函数指针数组
{
for (int i = 0; p[i] != nullptr; ++i)
{
cout<< p[i]<<endl; //打印地址方法1
//cout<< (void*)p[i]<<endl; //打印地址方法2 //cout自动识别类型,p是void*类型

//调用虚函数方法1:函数指针,因为p是void,所以定义一个void的函数指针f,再调用
//void (*f)() = p[i];
//f();

//调用虚函数方法2 直接通过函数指针调用 ,需要编译器推导类型
//p[i]();

//调用虚函数方法3 :实际上和2一样,是2的正确版,显式给出了正确类型的指针
VF_PTR f = p[i];
f();
}
}

/** 注意
* 1.打印过程注意bug
* 建议在打印前重新生成项目,
* 程序在运行过程中虚表指针位的nullptr位置可能会变动,导致打印过程中有可能会打印出多个指针,或者调试过程中00 00 00 00在很后面
*
* 2.内存窗口
* a.内存窗口太小的话可能会隐藏搜索功能
* b.内存窗口最右上角有个小小按钮可以选择显示字节,建议选择1字节,就能显示成 00 00 00 00 ,因为每一对数都是1字节
*
*/

void test_VFTable1()
{
//&d 就是拿到了d对象的首元素的地址
Base b;
Derive d;
//传参方法1
//1.32位DEBUG中指针是4字节,int符合,所以用int
//2.在对指针操作时,首先要确定指针的位数,所以,先取地址,然后强转为int*(指针4字节).再解引用,得到4字节的首地址.不然得到的还是对象,一大块
//3.得到b对象的首地址后,由于虚表指针就位于b对象的首地址,故将其强转成VF_PTR类型的指针后,就是虚表的首地址了
PrintVFTable((VF_PTR*)(*(int*)&b));
//
cout <<"============="<< endl;

//传参方法2 -- 一步到位,二级指针厉害,自动分析,任何场景都能用,32位或64位都可以
//1.由于PrintVFTable的参数是指针,所以实参必须是指针,因此需要&d得到指针
//2.对象b原本指向就是虚表(首地址),即b的首地址的类型是VF_PTR*,所以&b后就变成VF_PTR**,再解引用就是VF_PTR*,**解引用过程自动转化成指针而不是对象(一大快)
PrintVFTable(*(VF_PTR**)&d);
}

//技巧:通过指针位置推导变量的空间逻辑位置划分
void test_VFTable2()
{
typedef void(*VF_PTR)();
Base b;
Derive d;

int x = 0;//局部变量 -- 栈
int* y = new int;//指向堆中的整型指针 --- 堆
static int z = 0; //静态变量 --静态区,
const char* str = "xxxxxxxxxxxxxxxxxxx";//字符串常量 -- 常量区

cout << &x << endl; //局部变量:0113FC8C 栈 区
cout << y << endl; //动态申请:0118E7E8 堆 区
cout << &z << endl; //静态变量:006CE45C 静态区(数据段)
cout << (void*)str << endl; //常 量:006CBDD0 常量区(代码段)
cout << *(VF_PTR**)&b << endl; //父类虚表:006CBBC0 容易推出,虚表在常量区
cout << *(VF_PTR**)&d << endl; //子类虚表:006CBBF0
//分析:虚表在编译后基本不会改变了,因为继承都是我们在编译阶段(写代码阶段)完成,在程序运行过程中基本不会再发生变化,所以放在常量区合适
//-->> 有的编译器可能会放在静态区(符合共享性质).
}
}


namespace test3
{


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(*VF_PTR)();
void PrintVFtable(VF_PTR table[])
{
for (int i = 0; table[i]!=nullptr; i++)
{
cout << (void*)table[i]<<" ";
VF_PTR f = table[i];
f();
}
}

void test_VFTable1()
{
//观察/打印多继承各虚表位置情况
Derive d;
//方法1
//PrintVFtable(*(VF_PTR**)&d);//Base1的虚表
//cout << " ==================== " << endl;
//PrintVFtable((*(VF_PTR**)((char*)&d + sizeof(Base1))));//偏移到Base2.

//方法2 -- 切片自动定位到对应父类对象的首元素的地址,直接就是指针了,不需要像d一样先取地址
Base1* ptr1 = &d;
Base2* ptr2 = &d;
PrintVFtable(*(VF_PTR**)ptr1);
cout << " ==================== " << endl;
PrintVFtable((VF_PTR*)*(int*)ptr2);

//结果
/**
* 1.子类自己的虚函数在第一个先继承的父类的虚表中,并且在末尾位置
*
* 2.发现两个父类的虚表中被子类重写的func1的地址不一样:理论上说,子类对象的父类虚表中的func1是子类对象的才对,但为什么不一样?,
* ,决定进行验证
*
*/

}

void test_VFTable2()
{
Derive d;
Base1* ptr1 = &d;
Base2* ptr2 = &d;
ptr1->func1();
ptr2->func1();
/**验证结果
*
* 通过反汇编验证发现,最终调用的func1的地址都是一样的,只是base2中需要一些特殊处理
*
* ($ 类的this一般就是首地址)
* 对于ptr1,ptr1的this刚好和d的this重叠,所以可以直接调用func1
* 而对于ptr2,ptr2的this在下方,偏移sizeof(base1)位置.而VS对此做了一些封装调整(汇编层次),,导致虚表地址看起来不一样
* 我们要想通过ptr2调用func1,则需要调整this回到d或ptr1.重定向,所以在通过ptr2调用func1时,汇编会多一个sub和call或jump调整this
*
*
* 重定向
*/

//eax寄存器:一般存函数地址
//ecx寄存器:一般存this地址
}

}

标签:虚表,函数,cout,多态,C++,父类,指针
From: https://www.cnblogs.com/DSCL-ing/p/18038639

相关文章

  • C++继承
    //继承/***继承受权限影响*有publicprotectedprivate三种继承**基本语法:*classstudent:publicPerson*{**详细:跳转20inheritance--C++NO.23--3_27**///继承和组合/***组合即为在类成员是其他自定义类型:如适配器中栈,队列,反向迭代器.**继......
  • C++ STL 容器 forward_list类型
    C++STL容器forward_list类型介绍std::forward_list是C++标准模板库(STL)中的一个单向链表容器。与std::list不同,std::forward_list只允许从头部到尾部的单向迭代,不支持反向迭代。因此,std::forward_list在某些操作上可能比std::list更高效,尤其是在插入和删除元素时......
  • C++特殊类的设计与单例模式
    //1.设计一个不能被拷贝的类/*解析:拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。C++98;将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即......
  • C++11的类型转换
    //C类型转换/*C语言:显式和隐式类型转换1.隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败2.显式类型转化:需要用户自己处理.隐式类型:界定:相关类型,相近类型,意义相似的类型可以发生类型转换,如整型和浮点可以发生类型转换,因为他们都表示数据的大......
  • 【现代C++】2.强化语言运行期的强化
    1.Lambda表达式lambda表达式实际提供了一个类似匿名函数的特性,匿名函数是在需要一个函数,但是又不想费力去命名一个函数的情况下使用的。1.1Lambda表达式基本语法[捕获列表](参数列表)mutable(可选)异常属性->返回类型{//函数体}捕获列表分为以下几种:1.1.1值捕......
  • C++11新特性的一些用法举例①
    //字符串字面量/*常用:1.原始字符串字面量---括号内保持原样输出---没有转义字符,如\n不再是换行,而是直接输出字面量\nR"(str)";实例:R"(aa\a"b"bb)";//print:aa\a"b"bb注意:constchar*s1=R"foo(HelloWorld)foo";打印结果:HelloWorld;----//&qu......
  • C++ STL 容器 list类型
    C++STL容器list类型list对于异常支持很好,要么成功,要么不会发生什么事情以下是std::list在异常处理方面表现良好的几个原因:动态内存管理:std::list使用动态内存分配来存储元素,这意味着它会在需要时自动分配内存,并在不再需要时释放内存。这种自动管理可以减少内存泄漏和悬......
  • C++ STL 容器-Deque
    C++STL容器-Dequestd::deque(双端队列)是C++标准模板库(STL)中的一个容器,它支持在序列的两端快速插入和删除元素。与std::vector和std::list等其他序列容器相比,std::deque在某些特定场景下具有独特的优势。元素的访问和迭代比vector慢,迭代器不是普通的指针。以下是std::deque的一......
  • 《黑暗欺骗》c++控制台 2D 版!Alpha 0.2.1
    现在只打了设置这些个东西,游戏主体还没打,那才是难点已实现功能游戏未实现设置有音量设置和键盘设置两个功能存档(这好像是最简单的功能吧?)注意事项!!!没错,如你所见,这个游戏我是使用了MCI来播放声音的!因此,你的DEV-C++需要链接到一个库打开工具->编译选项->编译器-......
  • [1] C++编程语言
    week9day1 输出指令//控制台打印std::cout<<"HelloWorld";//简化std命名空间usingnamespacestd;//转义字符cout<<"\n";//\n会被渲染成前面有\的前提下\n不会被渲染cout<<"\\n";\n;<<endl;换行; 系统指令//system()可以调用CMD......