完备地实现C++多态性
扫描二维码
随时随地手机看文章
多态性是C++的一个重要特征。从广义上说,多态性是指一段程序能够处理多种类型对象的能力;具体地讲,多态性就是对不同对象发出同样的指令时,不同对象会有不同的行为。
如果程序员充分利用C++的多态性,设计程序的运行方式会更加灵活多样,但是会带来一些暗藏的细节问题。这些细节的漏洞也许会通过编译,但是在某些情况下,不可预测的结果或者背离编程者初衷的结果都会导致程序变得混乱不堪,甚至产生较大的风险。为了规避这些风险,MISRA C++推荐了一些编程规则。这些规则能够帮助程序员更加完备或者完美地实现多态性,充分体现C++相比于传统C语言的一些优势。
本文主要介绍两类在实现形式的多态性中需要注意的一些问题:一是运算符的重载,这是编译时的多态性,即程序在编译时就能根据重载的情况确定需要调用的函数;二是虚函数的使用,这是运行时的多态性,即在程序执行前,无法根据函数名和参数来确定调用哪个函数,必须在程序执行过程中,根据执行的具体情况来动态确定。
1 运算符的重载
运算符重载就是定义某个运算符对于某个类的具体含义。通过运算符的重载,程序员可以针对一些特定的类型使用重载的运算符含义。
规则5-2-11(强制):逗号(,),与(&&)以及或(||)运算符不允许被重载。
如果getValue和setValue的返回类型使用重载运算符&&,则这两个函数都需要计算。
C++的内部规定是,&&和||都是在已知结果的情况下不再计算后面的值,比如0&&(a--)&&(b++)。然而重载&&运算符和||运算符导致了程序运行时要计算所有的表达式。这对于一些使用&&做判断的运算来说,会导致一些错误。比如getchar()&&putchar(),在读取文件时,如果读到文件尾部,即得到getchar()为0时,就不需要再执行putchar()了,这样才能正确地读取并输出文件。如果重载&&运算符,那么先需要计算getchar()和putchar()的结果,再执行&&运算符的重载定义,这样可能会导致一些不可知的错误。这样的重载,会导致编译器在处理&&和||运算符时产生混乱,所以是比较危险的。
对于逗号表达式来说,默认情况下,编译器按照逗号表达式规定的顺序计算各个表达式。但是如果重载操作逗号表达式,因为需要先检查逗号两边的表达式类型,来判断是否使用重载定义的类型,所以会导致计算顺序的混乱。这样比较危险,会产生一些不可知的错误。虽然在C++并没有限制这3个运算符的重载问题,但是从这个例程和MISRA C++的规则来看,有些时候会产生一些不可预知的错误,所以MISRA C++不允许重载上面3个运算符。[!--empirenews.page--]
规则5-3-3(强制):单目运算符&不允许被重载。
f1.cc和f2.cc的区别就在于f1.cc只声明了A类,而f2.cc包含了A.h。f1.cc仅声明A类,不会使用A类定义的重载运算,所以f1.cc的8L运算符使用C++内部的取地址定义。f2.cc包含了头文件A.h,因为A.h包含了A类的完整定义,所以f2.cc的&运算符就会使用用户定义的重载操作。在同样一个工程中,仅仅是对A类的声明不同,就导致了在f2.cc中,&a使用用户定义的&运算符含义,而在f1.cc中,&a使用C++内部定义的&运算符含义。
这样差别会导致程序员在重载&运算符后,无法得知&运算符有没有使用重载的定义。这样做是比较危险的,可能会产生与程序员意愿不同的结果。虽然在C++中并没有限制对单目运算符的重载操作,但是从上面的例程可以看出,MISRA C++不允许重载&运算符是很有必要的。
2 虚函数的使用
虚函数是C++中一类特殊的函数。在基类中定义一个虚函数,就说明该函数在派生类中可能有不同的实现方式。当派生类的实例调用这个虚函数时,首先会在派生类中去查看该函数有没有被定义。如果派生类定义了这个函数,则执行派生类的函数;否则,在派生路径上寻找最近的该函数的定义,并调用该函数。
如果从基类派生出多个派生类,那么每个派生类都可以重新定义这个虚函数。如果通过基类的指针指向派生类的对象,并访问该虚函数,会对应地调用每个派生类的函数定义。这样通过基类类型的指针,就可以使属于不同派生类的对象产生不同的行为,从而实现了运行过程的多态。[!--empirenews.page--]
关于虚函数,MISRA C++有以下几条规则:
规则10-3-1(强制):在每一个继承路径上,虚函数只能有一个定义。防止按优先度调用。
例外:析构函数可以定义为虚函数,在每一个派生类上都可以有定义。
如果一个函数在同一个类中被声明为纯虚函数,但是还有定义,这样的定义就会被忽略。
在例程的后半段是关于按优先度调用的解释,表1显示的是例程中每个函数的调用和定义关系。b2.f1()是按照正常的继承关系来调用foo()函数,并且调用的是V类中foo()的定义。d.f2()和d.f1()都是按照优先度调用的。它们虽然最后都是调用了foo()函数,但是经过的继承路径却不相同,而且它们最后只能调用到B1类中foo()的定义。为了防止这种情况发生,所以Misra C++规定,虚函数在一个继承路径上,只能有一个函数定义。
例程的前半部分描述了多个类的继承关系,每个类都包括对几个函数的定义和声明。这里简单介绍一下f1()函数,读者可以通过表2的内容来理解其他函数。f1在A类中是虚函数,而且有定义,在C类中有定义,所以当D类继承C类时,D类中就不能再有定义(“√”表示可以定义,“*”表示不推荐再继续定义)。例外是f4,虽然它在A类中有定义,但是因为它是纯虚函数,所以它的定义会被忽略。
这个规则说明,如果在一个继承路径上有两个函数定义,在调用函数时,有可能按照继承的优先度调用函数。这样就会导致函数调用的混乱,可能会调不到程序员希望的函数。这是在实现多态时需要特别注意的地方。关于继承路径上的函数定义,C++并没有明确限制。[!--empirenews.page--]
从上面的例程可以看出,如果没有这样的限制,就会产生一些混乱,虽然程序能够正常运行,但是不一定能够按照程序员所设计的方式运行。这样的运行方式会出现很多漏洞,所以MISRA C++强制规定在每一个继承路径上,虚函数只能有一个定义。
规则10-3-2(强制):每一个重载的虚函数应该用关键字virtual来声明。
这样做不需要检查基类,就可以确定函数是否为虚函数。MISRA C++推出这样的规则是为了使C++程序更加完善。
规则10-3-3(强制):只有被声明为纯虚函数的虚函数,才能被纯虚函数重载。
foo函数在A类中定义为纯虚函数,在B类中被重载为普通虚函数。而C类使用纯虚函数重载foo函数。这样做是不行的。
B类中foo函数重载A类的foo函数时,是用有定义的虚函数重载纯虚函数,这样做是可以的。
C类中的foo函数重载B类的foo函数时,是用纯虚函数重载一个非纯虚函数,这样是不行的。在C类中,foo被定义为纯虚函数,在C类的对象调用foo函数时无法调用到B类中的定义。这样的重载导致B类中对foo函数的定义丢失。
所以MISRA C++不允许使用纯虚函数重载非纯虚函数,这样做的目的也是为了使C++程序更加安全。
3 小 结
正确并完备地实现C++的多态性,能够充分发挥C++的优势,并且提高程序的可读性和可维护性。如果使用不当,会导致一些想象不到的程序漏洞。MISRA C++针对使用多态性可能产生的一些漏洞,提出了规避的方法与建议。本文列出了其中几条比较关键和实用的规则。关于多态性的其他规则,读者可以查看。MISRA C++(2008),以避免不正确使用多态性所导致的一些程序漏洞。