模板编程和函数重载属于静态多态,也称为编译期多态,效率比较高,而虚函数多态,又称为动态多态,或者运行期多态。

我们或多或少对虚函数多态有所了解,这里我们讲一些稍微深度的东西和技巧。

类成员函数的本质

类成员函数本质仍然是函数,就是一个普通的函数,我们对类成员的函数进行如下强转。

通过这样你可以看出,类成员函数就是__thiscall修饰的普通函数,只不过它第一个参数就是this指针而已。

普通成员函数和虚函数

C++和C对比,不仅仅增加了函数重载,也增加了类,而类中的成员函数,除了有函数重载意外,还有另外两个概念,分别是函数覆盖和

虚函数重载。普通函数重载属于静态多态,而虚函数重载就是动态多态;首先这个三个概念有一个共同点,就是函数名一致,我们来对比

一下这3个函数的区别。

函数重载

函数重载,它要求函数名一样,参数类型或者参数个数不一样时形成重载,它属于静态多态,在类中除了析构意外,其它的任意函数都可以

形成重载,包括构造和运算符重载函数:典型的前++和后++就是用缺省int进行重载区分, 且类中函数const修饰也能形成重载。

例如:

class A {

public:

void func(int a);

void func(int a) const;

void func(double b);

};

class B : public A{

public:

void func(const char* c);

}

其中A中3个func函数和B中的func函数形成重载,值得注意的是const,只能在单个类中,跨类则形成覆盖。

函数覆盖

它要求函数名一样,且参数类型一致,且需要分别定义在基类和派生类中,派生类会覆盖基类的定义。

可能有人会觉得,这不是和虚函数覆盖规则一样吗?并不是哈,类成员函数,还有返回值,还是const修饰符,还有其它的一些修饰符。

函数覆盖之后,派生类对象调用该函数时,都会调用派生类覆盖之后的函数,即便应为一些限制报错,都会用覆盖之后的。

例如:

class A{

public:

void func(int a);

void func2(double b) const;

void func3(const char* c);

};

class B : public A{

public:

void func(int a);

void func2(double b);

int func3(const char*)

};

其中B中的3个函数,都对A中的3个函数形成函数覆盖,即便是使用const B类型调用func2函数,只会报错,它不会想着调用A中用const修饰的

func2函数。

虚函数重载

虚函数重载,是要求最多,它要求函数的声明和定义是一模一样的,包括函数名、参数类型、参数个数、参数的返回值,函数的修饰符等。

例如:

class A{

public:

virtual void func(int a) const;

virtual void func2(int a) const;

virtual void func3(int b);

virtual void func4(int b);

virtual void func5(int a) const;

};

class B : public A{

public:

void func(int a) const;   //形成虚函数重载

void func2(int  a);          //形成普通函数覆盖虚函数

int func3(int a);              //编译报错,不协同

virtual void func4(int b) const;  //虚函数覆盖虚函数

void func5(int a) overload;  //无法形成重载,不能使用overload关键字,报错。

}

通过上述的例子可以看出,虚函数重载要求派生类的函数和基类函数声明一模一样,且我们还知道overload关键字的作用,

有时候,你忘记写const,编译期是不会报错的,调试的时候,才发现,咋回事,甚至找了半天。

通过上诉的3个概念的对比,你会发现,其实虚函数重载,它与函数覆盖有点相似,其实我们可以理解虚函数重载其实就是一种

特殊的函数覆盖哈,只不过虚函数重载要求更严格,且最终会压入虚函数表,虚函数重载,可以理解,不仅仅是普通的函数覆盖,

虚函数表,也会进行覆盖。而有虚函数对于指针和引用的调用又比较特殊。

虚函数表

这玩意,大家都知道是个啥,如果一个类对象有虚函数,那么它的前4(8个)字节用于存储虚函数表的首地址,而虚函数表其实

就是用一个数组,将那些用virtual修饰的函数的地址列式的存储起来:

可以看出来,在虚函数表中是将虚函数的地址陈列出来,所谓的动态多态,指的就是运行时,动态的虚函数表去找,找到了就调用。

也正是因为这个过程,所以动态多态性能稍微差一些。

我们总结一下, 在运行时,类对象在代码区存储了3类东西:

  1.  函数,包括所有的函数,只不过类成员函数,第一个参数为this而已。
  2.  类信息,包括类本身的信息和类的继承信息。
  3.  虚函数表,存储当前类的所有virtual修饰的成员函数的地址。

我们举一个例子:

可见当一个指针或者引用在调用一个虚函数时,压根就不关心当前这个指针或者引用是个啥类型,直接根据当前指针或者引用找的虚函数表。

而普通成员函数就反过来了,它就完全不关心,当前这个指针的指向的内容是啥,编译期就已经根据指针或者引用的类型,给其他匹配一个函数。

至于虚函数相关的其他关键字,例如: overload、final, 这些关键字的作用,就是加强代码的可读性和安全性,对于最终编译的结果来说,没有任何区别。

备注:vs的监视中只能看到虚函数表中部分函数,可能并非全部哈。

备注:  上诉说的虚函数表调用时,是指针或者引用去调用,那普通对象去调用虚函数用的哪套逻辑呢? 它用的普通函数那一套。

dynamic_cast

动态类型转换,专用于有虚函数的类对象指针或者引用的转换,其实它间接的借用了虚函数表。

虚函数表,往前8个字节(32位,4个字节), 记录了一个地址,而这个地址就是类信息的地址。我们Everything搜索

rtti.cpp(有好几个版本,找那个有64处理的版本), 打开该文件,搜索__RTDynamicCast, 这个函数就是dynamic_cast的底层逻辑。

从这个函数的实现,可以看出,dynamic_cast是如何从根据类对象指针,获取类信息地址的。

由于搜不到对应的头文件的实现,因此结构体是啥,不清楚,可以参考下面的博文:

https://blog.csdn.net/passion_wu128/article/details/38511957

这篇文章适合32位,从这里我们可以了解,如何基于虚函数表,一步一步的获取,类对象的类信息。

备注1: 任何类都有类信息,但关键点,在于虚函数表,只有有虚函数表的类对象,才能调用dynamic_cast, 否则编译器会报错。

备注2: 只有转换存在向上的转换时(只要是有这样的过程),我们就需要用dynamic_cast,转换更加的安全,如果是纯粹的向下进行转换

则用static_cast转换或者强转效率更高, qobject_cast借助的是Qt本身的元系统,其关键是在于Q_OBJECT的宏定义,简单的说Qt搞了

另外一套类信息和继承信息,当然它搞的那套,就不可能存在代码区,而是存在堆栈区。

如何获取有虚函数表的类对象的类名

我在编写QSpy时,有一个这样的问题,就是获取窗口类名时,使用Qt的元对象(MetaObject)获取类名,有一个问题,就是如果继承自QObject的类没有

声明Q_OBJECT宏时,则会向上找到一个申明了Q_OBJECT宏类的类名,显然获取的是错误类名,因此我们需要借助虚函数表,如果你看了上面一篇博文,

你已经知道如何根据虚函数表获取类名。

这是上面那篇博文的截图,也就是上面获取的类信息指针指向的内容,其中TypeDescriptor, 就是const type_info,熟悉typeid运算符的就知道,这是

C++11扩展的一个运算符,类似sizeof, 只不过它返回的是const type_info&,而这玩意里面就有类名,只不过它的类名带了前缀信息。

因此我们可以根据pTypeDescriptor获取类名。

但是上面博文讲的32位,64位这个结构体,并不是这样子。其实我们可以参考rtti.cpp中的实现。

这是64位的获取类信息的方法,仍然苦于没有源码,我自己测试得知,COL_SIG_REV0的值是0,而一般signature的值是1, 至少我目前,还没有发现0的情况。

所以,我们只需要看else的实现内容,就会发现,_ImageBase是根据本身的地址减去本身内容中的东西,COL_SELF是啥宏,咱也不知道,但是可以自己琢磨

一下,下面就是64位获取类名的方法:

类虚函数type

我们在实现业务中,在定义一些平行类时,例如: QEvent时,我们通常的写法,会写成这样子:

class QEvent{

public:

enum Type{ MouseEvent, KeyEvent, ….}

virtual Type type() = 0;

}

class QMouseEvent{

public:

virtual Type type() {

return MouseEvent;

}

};

class QKeyEvent{

public:

virtual Type type(){

return KeyEvent;

}

}

当然,Qt的Event是有完整规划的,而我们实际在开发时,也会类似的进行参考,但是有没有发现一个问题,就是

当我们新增一个类型定义时,就需要在基类,或者在一个公共类型定义的头文件,增加一个宏或者一个枚举。而这样一修改

往往,就是需要编译整个工程。

我们反过来观察一下这个Type的定义,你会发现,你压根就不关心Type的值,定义在一起,无非就一个原因,为了不同类的

Type值不一样,例如:我们实现成如下方法:

class QEvent{

public:

virtual Type type() = 0;

}

class QMouseEvent{

public:

enum{Type = 1}

virtual Type type() {

return Type;

}

};

class QKeyEvent{

public:

enum {Type = 2}

virtual Type type(){

return Type;

}

}

你会发现这样定义,我们就不需要往往公共的头文件增加类型定义,而且我们可以通过:

QMouseEvent::Type获取事件Type,代码的写法关联型更强,但是却有致命的缺陷,就是里面有魔法数字。

我们集成开发时,很容易出现的一个问题,就是这个Type定义冲突了。

那我们有没有什么办法,让Type自动就变成唯一的值呢?当然没有,除非你用__COUNTER__,或者所有

事件定义在同一个头文件中,然后使用__LINE__,但是话说回来,这样子,增加一个事件还不是一样全编。

虽然我们有好的办法直接让Type变得唯一,但是反过来观察Type, 它就是一个整形,用于区分类, 使用时甚至

可以作为std::map等容器的键。因此我们换一种方式,用代码区的地址,这个地址是与类关联的。

其实总结而言,上述的代码定义,开闭性不好,我们需要统一的进行类型的定义,且不说,可能会存在代码

的提交冲突,公共头文件修改,一定会有的一个问题就是修改之后,就需要几乎完整性工程的重编,非常影

响我们编码效率,下面的这3种方法,是一种技巧,可以让我们规避这些问题。

备注1: 开闭性,指的是开闭原则原则,对扩展开放,对修改关闭,我们定义的任何基类,都不应该提供修改的可能,至少

在明面的意思上,例如,上诉的类型定义明明存在扩展,却需要在对应的文件增加类型定义,有时候一旦有这种口子,

就会不断的对其进行修改,最终会完全破坏该类的单一性。

备注2:  下面的这些策略,尤其是在扩展者和调用者完全不关心Type方法时,因为Type仅仅是作为封装者会对其进行调用时,

尤为重要。

备注3:  我有一个封装原则,就是让扩展者和调用者能少写一行代码就少写一行代码,我会尝试用各种技巧和方法:模板、宏、函数式编程以及

下面的这些编程技巧,简直是无所不有其极,总之让他人在扩展和使用时,越简单越好,当然我们要有目的的去使用这些技巧。

常量字符串的地址

const char* a = “abc”;   //“abc”是存储在代码区的,不同的常量字符串有不同的地址。

因此我们可以实现如下:

class QEvent{

public:

using Type = const char*;

virtual Type type() = 0;

}

class QMouseEvent{

public:

static  Type EventType() {  return “QMouseEvent” ;}

virtual Type type() {  return EventType();   }

};

class QKeyEvent{

public:

static  Type EventType() {  return “QKeyEvent” ;}

virtual Type type() {  return EventType();   }

}

也可考虑定义宏:

DeclareEvent(ClassName)\

public:\

static  Type EventType() {  return #ClassName ;}\

virtual Type type() {  return EventType();   }

class QMouseEvent{

DeclareEvent(QMouseEvent)

public:

}

class QKeyEvent{

DeclareEvent(QKeyEvent)

public:

}

备注: 我们比较Type,不是比较字符串哈,比较的是字符串的地址。

静态函数地址

函数地址,同样存储在代码区。

class  QEvent{

public:

typedef void* (*Func)();

using Type = void*;

virtual Type type() = 0;

};

class QMouseEvent{

public:

static Type EventType(){

//这玩意返回的就是自身的地址。注意这玩意可能会被优化, 理论上是没毛病

//可以考虑加一些防优化的代码。

return Type(&EventType);

}

virtual Type type(){

return EventType();

}

};

类信息地址

class QEvent{

public:

using Type = const type_info*;

virtual Type type() = 0;

};

template<typename T>

class InheirtEvent<T> : public QEvent{

public:

static Type EventType(){

return &typeid(T);

}

virtual Type type() {

return EventType();

}

};

class QMouseEvent : public InheiritEvent<QMouseEvent>{

public:

// 定义其它的代码。

};

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。