C++_virtual虚函数, 多态, typeid, RTTI, dynamic_cast, typeid,动态类型

catalog

  • 动态/静态类型
  • virtual函数分类
  • 多态的2种使用
  • dynamic_cast
    • 效率
    • 多态数据获取
  • 虚函数/ 多态类
    • dynamic_cast 与 多态类
    • 多态类的 析构函数, 必须是虚函数
    • 在构造/析构函数里, 不要调用虚函数
    • 纯虚函数, 抽象类
  • -----------------------------
  • 多态类型
  • RTTI
    • dynamic_cast
    • typeid
  • 虚函数表

动态/静态类型

静态类型, 在 程序中声明时, 就已经确定了, 而且用于不会改变
动态类型, 他只针对指针, 可以变化 通过赋值来改变

(非指针)变量, 他只有 (静态类型) Foo f; 则f 的静态类型为Foo, 没有动态类型

(指针)变量, 他 既有(静态), 也有 (动态)类型
Fa * fa; (此时, fa的静态类型为: Fa *, 动态类型为 无 (或者说, 没有动态类型))

fa = new Son1; (此时, fa 的动态类型为 Son1 *)

fa = new Son2; (此时, fa 的动态类型为 Son2 *)

fa = nullptr; (此时, fa 又没有动态类型了)

virtual函数分类

一般, virtual函数分为: (pure virtual 纯虚函数, 即子类必须实现)
(impure virtual 非纯虚函数, 若子类实现 而覆盖, 否则使用 父类实现的版本)

多态的2种使用

  • 指针
    Son s;
    Fa * fa = &s;
    
    fa->func(); ' 会调用到: s.func() '
    
  • 引用
    Son s;
    Fa & fa = s; ' 或: const Fa & fa = Son();   <只有const引用, 可以绑定 临时对象> '
    
    fa.func(); ' 会调用到: s.func() '
    

对于, dynamic_cast (即获取他的子类对象), 也有2个方式:

  • 指针
    Fa * fa = new XX;
    Son * son = dynamic_cast<Son *>( fa);
    if( son != nullptr){
    	cout << "success cast";
    }
    
  • 引用
    try{
    	Fa * fa = new XX; 
        Son & son = dynamic_cast< Son &>( *fa);
    }
    catch( const std::bad_cast &){
    	cout << "failed cast";
    }
    
    与指针方式不同, (指针可以通过返回值是否是nullptr来判断, 因为如果转换失败则返回nullptr)
    但引用方式, 转换失败, 会throw异常

dynamic_cast

效率

dynamic_cast的效率很慢!!!

dynamic_cast的原理是: 基于class名称的 字符串比较
即, 他会将内存对象继承体系里的每个子类, 进行strcmp匹配

多态数据获取

Fa * fa = new Son 我们令, 这个new Son对象 为 son
这个fa

  • fa->data, 一定对应为: son->Fa::data, 而不是son->data!!!
  • fa->func, (如果func为虚函数, 且Son也有自己的实现: 则fa->func会调用son->func()) (否则: fa->func()会调用son->Fa::func())

即, 我们可能会误以为: 多态, 只是针对func的多态; 对于数据data, 并无法使用多态

简单来说, 这种说法是正确的;

但是, 借助dynamic_cast, 可以实现 对data的多态!!!

Fa * fa = new Son; 我们令, 这个new Son对象 为 son, 且: son->data = 1, son->Fa::data = 0

  • fa->data 可以来获取: son->Fa::data
  • 那么: son->data 如何通过 fa来获取呢??
    Son * s = dynamic_cast< Son *>( fa);, 此时, 这个s对象, 和上面的son对象, 是完全相同的!!
    自然, s->data 就是 son->data

虚函数/ 多态类

如何判断, 一个类是否是(多态类)呢

  • 方法1:
    • 当该类不是子类: 如果, 该类中 有>=1个 虚函数, 虚函数: 虚成员函数 或 虚析构函数, 则该类是多态类
    • 当该类是子类: 如果, 其父类是多态类, 则该类是多态类
  • 方法2: 通过dynamic_cast, 下面会谈到

如果一个类有虚成员函数, 则你必须显式的 将他的 析构函数, 也写成 虚函数!!! 下面会解释为什么

即, 只要是多态类,则你必须显式的 将他的 析构函数 写成 虚函数


虚函数, 在类内 最好是声明, 不要在类中实现出来!!!


dynamic_cast 与 多态类

dynamic_cast< A *>( B *)
要求:

  • B类, 必须是 多态类, 否则报错!!!

dynamic_castA类, 没有任何限制!!! A 和 B, 可以是完全毫无任何关系!!! 至少编译通过; 只是会强转失败而已


' 继承关系:  SSON -> SON -> FA '
FA * ptr = new SSON;

using T = FA / SON / SONN;
T * ptr = new SSON;

using TO = ?;
TO * p = dynamic_cast< TO *>( ptr);
bool ret = (p == nullptr);
  • 首先, ptr 一定是 多态类的 指针, 否则编译报错; 因为dynamic_cast<>( ?) 参数?, 必须是多态类指针.

  • T的类型, 可以是FA / SON / SSON, 都可以.

  • TO的类型, 任意!! ST也可以; 编译不报错.

  • 比如T = FA, 即ptr 是一个 FA*的指针. dynamic_cast 完全不关注, ptr 的类型!!! 所以`T 是 这3种的任意都可以
    因为, dynamic_cast的作用, 就是 显现原型; 不管你这个ptr, 是哪位牛鬼蛇神, dynamic都会将他 显现原形
    什么是 (原形)呢? 即, 他最初new的类型; 即(SSON)
    即, dynamic 会将这个 ptr, 转换为 SSON* 类型. (因为你最初就是new SSON)
    然后, 看TO类型, 是否是 SSON的 父类(含自己)!!! 这一点, 是完全根据 C++的强转规则的!!!
    因为, 子类(指针/对象) 转换为 父类; 这是可以的; 但是反过来, 是绝对不可以的!!! 在任何情况下都是 违反c++强转规则的!!!
    即, 只要TO是(FA / SON / SSON), 转换就是成功的; ret = true

  • 举个例子:

    using T = FA / SON;
    T * ptr = new SON;    
    ' T绝对不可以是SSON, 这和多态没有关系;  父类(指针/对象) 绝不可能转换为 子类!!! '
    ' 其实, 你的FA * ptr = new SON;(标准的多态代码), 这个代码 也和多态 没有关系!!! '
    '      这只是一个(指针强转), 即将 子类(指针/对象) 转换为 父类; 这是可以的 '
    '      这与多态没有关系, 多态也得符合C++的强转规则; 这里, 完全是C++的强转技巧 '
    using TO = ?;
    TO * p = dynamic_cast< TO *>( ptr);
    bool ret = (p == nullptr);
    

    此时, 只有TO = (FA / SON)时, ret = true; 因为, ptr 不管是FA/SON, dynamic将它还原成 SON类型
    那么, SON
    , 根据 C++强转规则, 只能强转给 SON/FA *;
    所以, 如果TO = SSON, ret = false !!!

  • dynamic一个场景是: 给你一个FA * ptr的指针, (你不知道 他当初是怎么new的), 且FA是多态类
    问, 他是否可以 强转到 SON类型;
    好了, 比如说, 这个ptr 最初是 new SSON;
    那么, SON * my = dynamic_cast< SON *>( ptr); 这就是可行的.
    然后多说一句, my->func() 会调用 (SON的) 还是 (SSON的) 呢???
    当然, 这个问题 和dynamic没有任何关系; 这完全是 多态的知识;
    虽然SON 和 SSON, 都是多态类;
    但是, 如果func 不是虚函数!!! 则my->func()调用 SON自己的
    否则, func是虚函数, 则my->func()调用 SSON多态的

多态类的 析构函数, 必须是虚函数

多态的操作是: FA * fa = new SON();
这样,即使这个FA,派生出 超级超级多的子类, 我们/工厂 只需要保存一根FA*的指针即可。
这样,调用FA里的某个虚函数,就会优先调用他子类的实现。

思考: 当delete fa时,fa肯定是调用的FA的析构函数。
但是,我们毕竟调用了 new SON(),他里面有很多 他自己的data。 这就造成了: 内存泄漏(即,只释放了FA的data,而没有释放SON的data)

我们知道,虚函数的重写,是对 父类的同名、同参、同返回值的函数。
即:virtual void FA::f(int); 和 virtual void SON::f(int); 这样,fa.f(int),他会优先调用: SON实现的。

但是,为了解决上面的内存泄漏问题。

虚函数的重写,不仅是对同名、同参、同返回值的函数,他在处理析构函数时,也是有效的!(虽然函数名不同,但也属于重写)


首先,我们先回顾: 不带vitual的 继承时, SON的析构函数。

class FA{
	~FA();
}
class SON : FA{
	~SON();
}

SON * s = new SON();
delete s; ' 先调用~SON,后调用~FA() '

delete (FA *)s;  ' 只调用~FA() '

则与虚函数无关,这就是c++对class的 构造和析构的定义。

SON():虽然只调用了SON,但其实在他执行前 会先调用FA()构造!! 只是你看不到,其实在初始化列表里。
~SON();:虽然只调用了~SON,但其实他执行完 会再调用~FA()!!这是你看不到。


因此,在虚函数这里 也是同理。 FA * fa = new SON();
我们只要保证: fa这个指针,他可以调用到~SON即可!!! 不用让fa再调用~FA()因为子类的析构,一定会调用到父类的析构(与虚函数无关)

因此,虚函数的重写,不仅可以对同名、同参、同返回值的函数 的重写,也可以对 析构函数的重写!!!(可以说,这是个例外)

但道理是一样的。 FA * f = new SON();
你的delete f; 虽然看似调用的是:f.~FA(),但因为他是虚函数,所以会调用子类的析构!! 即,其实是:f.~SON()
~SON(): 即调用了~SON,也调用了~FA()

class FA{
	virtual ~FA(){}
};
class SON : FA{
	virtual ~SON() override {}
};

假如说,不涉及到虚函数的 继承,有必要把析构 也变成virtual吗??
即使没有虚函数:FA * f = new SON()delete f同样会导致:只调用~FA!!

因此,只要有多态,父类的析构 必须是virtual!!


但是,当不涉及到 多态的继承时,不要对父类析构 设置成虚函数。
这样增大该类的sizeof!!!!
虚函数开销是很大的。

总结:

  • 当涉及到 多态polymorphic时,父类的析构函数 必须声明为:virtual
    或者这么说: (带虚函数的继承,父类析构必须是虚函数)(不带虚函数的继承 但涉及到多态,父类析构也必须是虚函数)
  • 不要去继承STL的容器string, vector...,他们里面都是没有虚函数的!!!
    或者说,即使继承了,但不要使用多态。

在构造/析构函数里, 不要调用虚函数

class FA{
	virtual void f(){}
	
	FA(){
		f();
	}
};
class SON : FA{ ... }

SON s时,由于FA的构造 是优先于 SON的构造,当FA的构造里 使用了f()这个虚函数。
那么,此时的FA构造函数里的 这个f虚函数,他是指的:FA的、还是SON的呢?
从虚函数的使用来看,用户肯定是想让他 代指 SON实现的版本,毕竟虚函数的作用就在于此。

但是,此时SON还没有构造出来!!!
FA构造函数里 所使用的虚函数,一定是FA实现的版本,不会是子类版本!!

其实最本质的原因是: SON s,这个s,会经历两个阶段:(先是FA父类的构造函数)(然后是SON子类的构造函数)
在第一个阶段,其实这个s对象,他的类型是: FA,而不是SON!!!
到了第二个阶段,s才是SON类型。

所以,妄想在 父类构造里,调用子类的虚函数,这是不可能的!!!

同样,当在析构函数里 调用虚函数:
SON s; 当s销毁时,会经历两个阶段:(先是SON子类的析构函数)(然后是FA子类的析构函数)
在第一个阶段,s的类型是 SON。
但到了第二个阶段,s的类型是:FA!!! 你在FA析构函数里,调用虚函数,他肯定不会是子类的,因为当前s 就是父类类型!


即,不要在构造/析构函数里,调用虚函数; 这个虚函数,并不会调用“子类”的实现!!!

纯虚函数, 抽象类

一旦一个类有 virtual void func() = 0;纯虚函数, 这个类, 就是抽象类abstract

class Fa{
	virtual void func() = 0;
}

Fa是抽象类, 则: Fa f;new Fa 都是不允许的!!!

如果 子类不实现 纯虚函数, 则子类 也是抽象类

class Son : Fa{
};

此时, Son也是抽象类; 因为他没有实现func

-----------------------------

多态类型

class FA{
	virtual void func(){ DE<< "im fa..."; ED; }
};

class SON : public FA{
	virtual void func(){ DE<< "im son..."; ED; }
};

我们知道,最简单的多态是:
SON * son = new SON;
FA * fa = son;
此时fa->func() 会调用 子类SON的func函数!!

这种情况是: 子类的 指针, 强转为: 父类的 指针


但注意:
SON son;
FA fa = son;
这不是多态!!! fa.func() 还是会调用父类的func函数,这是普通继承情况下的 “强转”
(即,将son.FA::data 赋值给 fa.data

FA fa = *(new SON); 这也不是多态!!

虽然,FA里有虚函数,但这些写法 都不是多态!!


还有一种多态:
SON * son = new SON;
FA & fa = *son;
此时fa.func() 会调用 子类SON的func函数!!

这种情况是: 子类的 指针的 对象,强转为:父类的 对象


即,一共有2种 多态的形式。

形式1: FA * fa = new SON;   		(等价于: SON s; FA * fa = &s; )
形式2: FA & fa = *( new SON );		(等价于: SON s; FA & fa = s;

注意一种特殊情况: FA fa = *(new SON); ,这不是多态!!!

总结是:

  • fa必须是指针!! (引用,本质也是一种指针!)
  • 前提是:FA里必须有“虚函数”!!!
    如果FA里没有虚函数,即使你FA * fa = new SON* fa的类型 依然是FA类型!!!

分析这个原因。

  • FA t: t的类型,一定是唯一的
    什么意思呢? 不管你代码怎么写t的类型 一定是FA类型!!
    这种类型,也称为: 编译期 就可以 确定的类型(不管编译期/运行期,他的类型 都是唯一固定的)

    即使你:FA t = *(new SON),这并不是多态!! !
    因为,t的类型是:FA。 他的类型FA 就决定了:t的类型 是固定唯一的。

  • FA * t:虽然t的类型,肯定是:FA *类型。这肯定是不变的!!符合上面的规则
    (不管你代码怎么写,t的类型 肯定是:FA *
    但是,唯一不同的是: * t,即他所指向的内存的 类型。* t的类型,是可以有 多种情况的
    (当然,同一时刻 * t的类型是固定唯一的。但* t的类型,是可以动态的一直变化!)
    什么意思呢?

    FA * t = new SON1;
    t = new SON2;
    t = new SON3;
    

    * t的类型typeid(* t),他是一直在变化的!!! * fa先是SON1类型,然后是SON2类型,最后是SON3类型。
    即使到了运行期,*t的类型 也不是固定不变的!!
    但注意,以上说法的前提是:FA里有虚函数!!!如果FA里没有虚函数,*t不管是new SON*t的类型 始终为FA!!!

    FA * t;
    if( rand() & 1 )	t = new SON2;
    else				t = new SON3;
    

    甚至这种写法,每次运行,*t的类型 还说不准,可能是SON2,可能是SON3(当然,前提是:FA里有虚函数。否则,*t始终是FA类型)

  • FA & fa = *(new SON); 其实从上面的说明,也可以推导出 引用的情况
    (因为:引用的本质,就是指针!!!)
    既然,指针所指的对象 不一定是该指针的类型,自然引用所引用的对象 也不一定是该引用的类型。
    比如上面这行代码,(如果FA里有虚函数,则fa类型是SON。否则,fa类型是FA)

当出现(FA * fa* fa所指对象的 类型 不唯一、FA & fafa所引用的对象 类型 不唯一)的情况时(我们暂称这种情况为:动态类型)

当出现 “动态类型”时,一定有: FA里面 肯定有:虚函数!!!

即,只有“多态” 只有“FA里有虚函数”,才有 “动态类型”。否则,即使是FA * fa = new SON* fa也是 静态类型。

RTTI

Run Time Type Identification (其实就是我们上面定义的: “动态类型”,他只有在“多态”的情况下 会存在这种现象)

由于他的类型,是在动态变化的。所以,我们如何得到他 此时此刻的类型呢? 有2种方式

  • dynamic_cast
  • typeid

dynamic_cast

比如我们有个需求(当然是在“多态”的环境下,即FA里有虚函数) dynamic_cast的对象, 必须要有 (虚函数)!!!

FA * fa;
if( rand() )	fa = new SON1;
else			fa = new SON2;

我们想要确定,最终这个fa,到底是指向SON1,还是指向SON2呢???

这是第1个需求


第2个需求(重要)是:

FA * fa = new SON
new SON这块内存里有: (FA::data)(SON::data)(FA:: func)(SON::func<非虚函数>)
而我们仅通过fa,无法访问:SON::dataSON::func<非虚函数>

这不太好,毕竟,我们确实是new的这块内存,这块内存里 确实是有这些东西的!!!

即, 按理说,此时 将faFA *类型 转换为 SON *类型,是合乎情理的!

虽然,直接SON * son = (SON *) fa后,son是可以访问SON里独有的数据/函数的!!
但有一点: 由于fa可以指向很多对象SON1, SON2, ...
如果你并不知道 这个fa 具体指向哪种类型,那么,显然你无法写出: (SON *)fa 这样的代码。
这些写法也很危险,(假如,fa实际指向SON1,那么(SON *)fa 是会报错的! 不同子类 不能强转,数据等都不一样)


基于以上的两个需求,引入:dynamic_cast
他的作用是: 判断fa这个指针,具体是指向哪个类型

FA * fa = new SON;

SON1 * son1 = dynamic_cast< SON1 * >( fa );
if( son1 == nullptr ) ' 说明:fa指向的内存类型,并不是SON1 '

SON * son = dynamic_cast< SON * >( fa );
if( son != nullptr ){ ' 说明:fa指向的内存类型,是SON类型 '
	son->son_data; 
	son->son_func(); 
	' 调用son里,自己独有的数据/函数。这是通过fa无法做到的。'
}

----------------------------

SON s;

FA * fa = &s;	' 这个是多态!(FA里有虚函数) '

SON * ss = dynamic_cast< SON* >( fa ); ' 可以转换回去! (毕竟SON s,本来就是SON *) '

注意这种强转,与以往都不同。

以往的强转是: (从大)可以 往(小的)转换。
因为,我很大(子类) 本来就包含(父类),自然可以往小的(父类) 去转换。

但这里是: 从(小的)FA*,转换为: SON*(大的)
这很危险。 显然,只能发生在: 多态的情况下。

而且,虽然他是FA*类型 很小,但其实他指向的内存 是SON很大
所以,这块内存 本来就是SON类型,再转换到SON类型,这显然是合理的。(有点,物归原主的感觉

typeid

FA * fa = new SON;

if( typeid( *fa ) == typeid( SON ) ) 
	' 说明,fa指向的内存 是SON类型 '

虚函数表

只要类中有 >=1个 虚函数,这个类就会产生 指向各个虚函数的指针。
有X个虚函数,就有X个指向各虚函数的指针。
这X个指针,会被放到 虚函数表里(virtual table)
这个虚函数表,保存在最后生成的exe文件里,程序执行时 载入内存
系统,会自动 往类/对象中,添加一个 “指向 虚函数表”的 指针。
这个 指向虚函数表的指针,不是static的,每个对象都有 他是占用类内存的
’ 只要有 虚函数, 则sizeof(ST)里 一定有8字节, 为 指向虚函数表的 指针 ’
X个虚函数 (不占用 类内存)
-> 产生 X个虚函数指针 (不占用 类内存)
-> 产生 1个 虚函数表 (不占用 类内存)
-> 产生 1个 指向该虚函数表的 指针 (占用 类内存)

你可能感兴趣的:(计算机知识,c++)