目录
前言
运行时类型识别。在有多态行为发生的继承体系中,通过RTTI,程序能够使用基类的指针或者引用来检查这些指针或者引用所指的实际对象的类型。RTTI的实现是由dynamic_cast和typeid运算符来实现的。基类存在虚函数,且子类继承并实现了拥有自己行为的虚函数,那么在基类指针或者引用指向子类对象的时候,就会发生动态绑定,也叫做晚绑定。此时基类指针或者引用的实际类型是子类对象的类型。
一、dynamic_cast
1.向上转换(Casting Up)
向上转型或者说从派生类转换到基类都是可以的,只要不存在二义性。它还可以被隐式的执行。
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : A {
float mb;
int fb() { return 3; }
};
struct C : A {};
struct D : B, C {};
void f(A a) {}
void g(A& a) {}
void h(A* a) {}
int main()
{
B b;
f(b); //b转换为基类对象发生了切割,
g(b);
h(&b);
return 0;
}
在这三种情况下,对象b都被隐式转换成类型A的对象。注意,函数f不是多态的,因为多态类型必须以引用或(智能)指针的方式传递。只有当基类存在二义性的时候,向上转换才会失败。例如在这个例子中,我们无法将D 的对象转换到A,因为编译器不知道基类A是从B还是C来的。为了澄清这个二义性,我们需要明确指定中间的转换步骤:先将D对象明确转换为B或者C类型
D d;
f((B)d);
或者我们可以让A作为B和C的虚基类:
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : virtual A {
float mb;
int fb() { return 3; }
};
struct C : virtual A {};
struct D : B, C {};
void f(A a) {}
void g(A& a) {}
void h(A* a) {}
int main()
{
D d;
f(d);
return 0;
}
此时,A的成员在D中只会出现一次。在多数情况下,这通常是多重继承的最佳解决方案。
2.向下转型(Casting Down)
向下转型是将指针或者引用转换成子类型的指针或者引用。如果实际引用的对象不是转换的子类型,那么就会导致未定义的行为。因此,只有在绝对必要的条件下才能极为谨慎的使用它。
在上面向上转型的例子中,我们将类型B的对象传递给了引用A& 或者指针A*,在函数g和h中,尽管被引用的对象b是B类型,但是还是不能访问B的成员变量mb和成员函数fb()。在确保函数参数a确实引用了B类型的对象后,可以将a分别向下转型为B& 或者 B*,然后就可以访问mb和fb()了。
在我们准备向程序中引入向下转型之前,必须考虑以下问题:
1、我们如何保证传递给函数的参数确实是派生类的对象?
2、如果不能向下转型我们应该怎么办?
3、我们是否该为派生类编写一个函数?
4、为什么不为基类和派生类提供函数重载?
5、我们是否可以重新设计类,用虚函数的晚绑定机制来完成任务?
向下转换有两种形式可供选择:
1、static_cast:快速但不安全的转换
static_cast只能检查编译期的信息,这意味着它只能检查目标类型是否派生自基类类型。例如我们可以将函数g的参数a强制转换为类型B&,并且调用B类中的方法:
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : A {
float mb;
int fb() { return 3; }
};
struct C : A {};
struct D : B, C {};
void f(A a) {}
void g(A& a)
{
B& bref = static_cast<B&>(a);
std::cout << bref.fb() << std::endl;
}
void h(A* a) {}
int main()
{
B b;
g(b);
return 0;
}
编译器验证了B是A的子类,并且接受了转换,但是当参数a引用的不是B或者其子类的对象是,会导致程序有未定义的行为,最常见的就是崩溃。
在菱形继承的示例中,也可以将指针B向下转换到D。为此我们声明了B*类型的指针,它允许我们指向B的子类D的对象:
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : A {
float mb;
int fb() { return 3; }
};
struct C : A {};
struct D : B, C {};
void f(A a) {}
void g(A& a) {}
void h(A* a) {}
int main()
{
B *bbp = new B;
B *bdp = new D;
D* ddp = static_cast<D*>(bbp); //虽然能通过编译,但是错误的向下转换
D* ddp1 = static_cast<D*>(bdp); //正确的向下转换
return 0;
}
因为没有执行运行时检查,所以程序员有责任确保对象不去引用不正确的类型。bbp指向的是B类型的对象,因此在我们解引用的时候会面临数据损毁和程序崩溃的风险。这个例子中,可能有些编译器会给出警告,但是一般情况下,很难追踪引用的实际类型,尤其是在运行期才会确定类型:
B *bxp = (argc > 1) ? new B : new D;
2、dynamic_cast:转换是安全的,但是需要一定额外开销,并且只能用于多态类型。
dynamic_cast会在运行时检查被转换的对象是否真的是目标类型或目标的子类。但是它只能用于多态类型。
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : A {
float mb;
int fb() { return 3; }
};
struct C : A {};
struct D : B, C {};
void f(A a) {}
void g(A& a) {}
void h(A* a) {}
int main()
{
B *bbp = new B;
B *bdp = new D;
D* ddp = dynamic_cast<D*>(bbp); //虽然能通过编译,但是错误的向下转换
if (ddp == nullptr) {
std::cout << "错误的向下转换1" << std::endl;
}
D* ddp1 = dynamic_cast<D*>(bdp); //正确的向下转换
if (ddp1 == nullptr) {
std::cout << "错误的向下转换2" << std::endl;
}
return 0;
}
如果类型转换失败,则会返回一个nullptr,提供了处理转换失败的机会。引用的向下转换会引发异常std::bad_cast,可以在try-catch中处理它。这些类型的检查都是基于RTTI实现的,需要花费额外的开销。
dynamic_cast和虚函数的实现手段相同,因此只有至少定义了一个虚函数,类具有了多态性才可以使用。
3.交叉转换
当对象的类型派生自B和C两个类时,可以实现从B到C的转换:
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
struct B : A {
float mb;
int fb() { return 3; }
};
struct C : A {};
struct D : B, C {};
void f(A a) {}
void g(A& a) {}
void h(A* a) {}
int main()
{
B *bbp = new B;
B *bdp = new D;
C* cdp = dynamic_cast<C*>(bdp);
return 0;
}
因为bdp的类型时D,所以可以从B转换到C。但是如果是静态的从B向C转换:
C* cdp = static_cast<C*>(bdp);
这样是不被允许的,因为C既不是B的派生类,也不是B的基类。但是我们可以通过D来间接完成B到C的静态转换:
C* cdp = static_cast<C*>(static_cast<D *>(bdp));
但是这里要再次强调,程序员要确保对象以指定方式正确地进行转换。
动态转换更加安全,但是需要在运行时检查被引用对象的类型,故而比静态类型转换要慢一些。静态类型转换可以向上转换,而向下转换时则需要程序员正确处理被引用的对象。
二、typeid
返回指针或者引用所指对象的实际类型。 typeid(类型【指针/引用】); 也可能typeid(表达式);
typeid会返回一个常量对象的引用,这个常量对象是一个标准库类型 type_info。
1.typeid的使用示例
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
int ma;
};
int main()
{
A* p = new A;
A& ref = *p;
std::cout << typeid(*p).name() << std::endl; //struct A
std::cout << typeid(ref).name() << std::endl; //struct A
char a[10] {0};
int b = 1;
std::cout << typeid(a).name() << std::endl; //char [10]
std::cout << typeid(b).name() << std::endl; //int
std::cout << typeid(1.0).name() << std::endl; //double
std::cout << typeid("ABC").name() << std::endl; //char const [4]
return 0;
}
2.typeid运算符使用场景
通常情况下,我们使用typeid比较两条表达式的类型是否相同,或者比较一条表达式类型是否与指定类型相同。
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
};
struct B : A {};
int main()
{
B* p1 = new B;
A* bp = p1;
if (typeid(*p1) == typeid(*bp)) {
std::cout << "p1 和 bp 指向同一类型的对象" << std::endl;
}
if (typeid(*bp) == typeid(B)) {
std::cout << "bp 实际指向B对象" << std::endl;
}
return 0;
}
这里需要注意的是,typeid应该作用于对象,因此我们要使用*bp而不是bp:
#include <iostream>
struct A {
virtual void f() {}
virtual ~A() {}
};
struct B : A {};
int main()
{
B* p1 = new B;
A* bp = p1;
if (typeid(*p1) == typeid(*bp)) {
std::cout << "p1 和 bp 指向同一类型的对象" << std::endl;
}
std::cout << typeid(bp).name() << std::endl; //struct A *
std::cout << typeid(B).name() << std::endl; //struct B
if (typeid(bp) == typeid(B)) {
std::cout << "typeid应该作用于对象,此处代码不会执行" << std::endl;
}
return 0;
}
当typeid作用于指针时(非指针所指的对象),返回的结果时该指针的静态编译时类型。
三、使用RTTI
在某些情况下RTTI非常有用,比如当我们想为具有继承关系的类实现相等运算符时。 对于两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则我们说这两个对象是相等的。在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来。
要想实现有效的相等比较的操作,我们先要明确一个事实:那就是如果参与比较的两个对象类型不同,则比较结果为false。例如我们试图比较一个基类对象和一个派生类对象,则 == 运算符应该返回false。
我们可以使用RTTI来解决这个问题,我们定义的相等运算符的形参是基类的引用,然后使用typeid检查两个运算对象的类型是否一致。如果运算对象的类型不一致,则返回false,类型一致才调用用来比较成员的equal函数。
#include <iostream>
class Base {
friend bool operator==(const Base&, const Base&);
public:
//Base的接口成员
protected:
virtual bool equal(const Base&) const;
//Base的数据成员
};
class Derived : public Base {
public:
//Derived的接口成员
protected:
bool equal(const Base&) const;
//Derived的数据成员
};
bool operator==(const Base& lhs, const Base& rhs)
{
return (typeid(lhs) == typeid(rhs)) && lhs.equal(rhs);
}
bool Base::equal(const Base&) const
{
//执行比较Base对象的操作
return true;
}
bool Derived::equal(const Base& rhs) const
{
//如果执行到这里,说明operator==的两个实参的类型相同。且类型都是Derived
//所以在lhs.equal(rhs)执行时 会执行到这里
//此函数的第一步应该将实参的类型转为派生类类型
//这里的转换不会抛出异常,因为我们已经知道实参的类型就是Derived
//这里的类型转换必不可少,执行转换后就可以访问Derived中的成员
const Derived& res = dynamic_cast<const Derived&>(rhs);
// 接着比较两个Derived对象的成员,并返回结果
return true;
}
int main()
{
const Base& lhs = Derived();
const Base& rhs = Derived();
std::cout << (lhs == rhs) << std::endl;
return 0;
}