模板编程和函数重载属于静态多态,也称为编译期多态,效率比较高,而虚函数多态,又称为动态多态,或者运行期多态。
我们或多或少对虚函数多态有所了解,这里我们讲一些稍微深度的东西和技巧。
类成员函数的本质
类成员函数本质仍然是函数,就是一个普通的函数,我们对类成员的函数进行如下强转。
通过这样你可以看出,类成员函数就是__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类东西:
- 函数,包括所有的函数,只不过类成员函数,第一个参数为this而已。
- 类信息,包括类本身的信息和类的继承信息。
- 虚函数表,存储当前类的所有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:
// 定义其它的代码。
};
评论(0)