Casting Basics - 在您的 VC++.NET 程序中使用 C++ cast






4.93/5 (19投票s)
演示并比较了各种可用的 casting 运算符。还建议了何时使用以及使用什么!
序言
如果有人问我,让我说出 C++ 最令人兴奋的一个特性,我会回答是能够将一个类型转换为另一个类型。当然,这只是我对 cast 的一种有些异想天开且极不理性的喜爱,并不代表我对 cast 及其有用性进行了任何伟大的技术思考。每次我看到类似 ((CNishApp*)AfxGetApp())->CustomFunc()
或 (LPSTR)(LPCTSTR)m_str
这样的代码时,我都会在心里欢呼雀跃。当我使用 VC++ 6 时,这一切都很好,但当我开始将我的一些旧应用程序迁移到 VC++.NET 时,看到那些关于我的 C 风格 cast 现在已被弃用的编译器警告,我感到有些不安。
那时我查看了新的 C++ casting 运算符,并很快发现它们是更安全、更智能的 cast 方法。本文将介绍各种可用的 casting 运算符,并建议何时何地可能希望使用这些 casting 运算符。整篇文章都是从托管扩展的上下文中撰写的,因此本文只讨论托管类。这有一些有趣的推论,我将在文章后面提到。
C 风格 cast 的危险
C 风格 cast 最主要的问题是它们绝对且完全不安全。不会进行任何编译时或运行时检查,您完全可以自己挖坟墓,挖得有多深都可以。没有什么能阻止您将指向基类对象的基类指针转换为派生类指针,这意味着当您调用派生类方法时,灾难几乎是肯定的。
另一个问题是,您永远不知道您试图实现的是哪种类型的 cast。就语法而言,所有您可以进行的 cast 都看起来一样。显然,这在调试时会很麻烦。此外,有些人使用函数式 cast,例如 int(x)
而不是 (int)x
,这会导致代码进一步混淆,因为 C/C++ 代码几乎总是充斥着函数及其伴随的括号。
C++ Casting Operators
提供了五个 casting 运算符,用于替换旧的 C 风格 cast。每个运算符都有一个特定的目的和预期用途,我将在文章后面解释。在此之前,我想谈谈多态类。任何具有虚函数的类都是多态类。这在旧的非托管世界中标记了类之间的主要区别。但在 C++ 编码的新托管世界中,每个类都是多态的,因为所有托管类都隐式派生自 System::Object
,而 System::Object
本身就具有虚方法。因此,这使得每个 __gc
类都成为多态类。这使得我们几乎可以在所有地方专门使用更安全的 dynamic_cast
而不是 static_cast
,这在非托管 C++ 中是不可能的。
static_cast
dynamic_cast
const_cast
reinterpret_cast
__try_cast
(仅限托管扩展)
static_cast
static_cast
运算符仅依赖于编译时信息。不进行运行时检查以确保安全 cast。您可以使用 static_cast
进行向下转换和向上转换,但其中存在危险,我将在文章后面进行演示。static_cast
运算符的语法如下:
T t = static_cast<T>(expression);
用法示例可能如下所示:
CMainFrame* pMF = static_cast<CMainFrame*>(AfxGetMainWnd());
dynamic_cast
dynamic_cast
运算符同时依赖于编译时和运行时信息。当您尝试使用 dynamic_cast
运算符进行 cast 时,会进行运行时检查以查看此 cast 是否安全,如果 cast 不安全,则返回 NULL
。因此,通过检查 NULL
,我们可以确定 cast 尝试是否成功。语法是:
T t = dynamic_cast<T>(expression);
dynamic_cast
的一个样本用法可能如下所示:
void Hello(Base* p1)
{
//...
Child *c1 = dynamic_cast<Child*>(p1);
if(c1)
{
// we can safely use c1
}
//...
}
const_cast
const_cast
运算符用于移除类上的 const
、volatile
和 __unaligned
属性。(__unaligned
是 Microsoft 对 C++ 的扩展,我不确定它的用途)。您只能使用 const_cast
在仅因 const 性或易变性而异的类型之间进行 cast。其用法语法与其他 cast 类似:
T t = const_cast<T>(expression);
样本用法可能与以下内容类似:
void Abc(const Base* pB)
{
//...
Xyz(const_cast<Base*>(pB));
}
void Xyz(Base* pB)
{
//...
}
reinterpret_cast
老实说,我不知道为什么会有这个运算符,因为在我看来,它和旧的 C 风格 cast 一样糟糕,只是它明确地声明了其不安全性。您可以使用 reinterpret_cast
运算符将一种类型转换为任何其他类型。它的语法是:
T t = reinterpret_cast<T>(expression);
基本上,唯一可以安全使用它的地方是在我们从 Type-1 cast 到 Type-2,然后又将 Type-2 cast 回 Type-1 的情况下。这样,我们就获得了与开始这个 reinterpret_cast
过程时完全相同的对象。
/* A and B are unrelated types */
A *a = reinterpret_cast<A*>(new B());
B *b = reinterpret_cast<B*>(a); //safe to use b
__try_cast
__try_cast
是一个托管扩展关键字,在非托管 C++ 中不可用。它本质上类似于 dynamic_cast
,但它在失败时抛出异常,而 dynamic_cast
返回 NULL
。语法是:
T t = __try_cast<T>(expression);
抛出的异常是 System::InvalidCastException
,如果您使用 __try_cast
,最好使用 try
-catch
块,否则您可能会在程序中间看到它崩溃,尽管在开发过程中这可能是件好事。下面是一个样本用法:
try
{
p2 = __try_cast<Base*>(p1);
//use p2 safely now
//...
}
catch(System::InvalidCastException*)
{
Console::WriteLine("blast!");
}
何时以及使用什么?
好了,我们已经看到了 5 种不同的 casting 运算符,它们应该取代旧的 C 风格 cast。在编码时选择正确的 cast 运算符可能会有点令人困惑。老实说,直到最近我才不知道何时使用什么!不管怎样,我创建了一些虚构的 casting 场景,并尝试使用每个 cast 运算符来执行 cast(在编译器允许的情况下),我试图演示为何以及为何某些 cast 运算符不合适,以及何时它们合适。当然,这些建议完全是我个人的,我不敢说它们是最合适的选择,但我打算不断更新本文,以包含那些碰巧看到本文的 C++ 大师的有价值的输入。
测试类
这些是我在实验中使用过的测试类和枚举。
/*
Our test base class
*/
__gc class Base
{
public:
int dummy;
};
/*
Our test derived class
*/
__gc class Child : public Base
{
public:
char anotherdummy;
};
/*
An independent test class
*/
__gc class Base2
{
};
enum CastType
{
tdynamic_cast,
tstatic_cast,
tconst_cast,
treinterpret_cast,
ttry_cast
};
向下转换 (Downcasting)
向下转换是从基类指针转换为派生类指针。我编写了一个名为 BaseToChild
的函数,该函数根据传递给它的 CastType
枚举参数执行各种向下转换。显然,我不能在这里使用 const_cast
,因为我们是在不同类型之间进行转换。
Child* BaseToChild(Base* p,CastType casttype)
{
Child* retptr = NULL;
switch(casttype)
{
case tdynamic_cast:
retptr = dynamic_cast<Child*>(p);
break;
case tstatic_cast:
retptr = static_cast<Child*>(p);
break;
case tconst_cast:
/* This won't compile */
//retptr = const_cast<Child*>(p);
break;
case treinterpret_cast:
retptr = reinterpret_cast<Child*>(p);
break;
case ttry_cast:
try
{
retptr = __try_cast<Child*>(p);
}
catch(System::InvalidCastException*)
{
//...
}
break;
}
return retptr;
}
不安全的向下转换
Base *pBase1 = new Base();
Child *pChild1 = NULL;
我有一个 pBase1
,它保存一个 Base
对象,现在我将尝试将其向下转换为 Child
对象。显然,这是不安全的,是我真的不应该做的。我调用了 BaseToChild
四次,每次针对一种类型的 cast(const_cast
在这里不可用)。
//dynamic_cast
pChild1 = BaseToChild(pBase1,tdynamic_cast);
if(!pChild1)
{
Console::WriteLine("dynamic_cast - downcast failure");
}
else
{
Console::WriteLine("dynamic_cast - downcast success");
}
我只展示了 dynamic_cast
的示例代码,但其他三种 cast 的代码非常相似。我在下表总结了我的结果。
Cast | 结果 | 注释 |
---|---|---|
dynamic_cast |
失败 | 失败并返回 NULL |
static_cast |
成功 | 进行了不安全的 cast |
reinterpret_cast |
成功 | 进行了不安全的 cast |
__try_cast |
失败 | 失败并抛出异常 |
安全的向下转换
Base *pBase1 = new Child();
Child *pChild1 = NULL;
好了,现在我们有一个 Base 对象持有一个 Child 对象。现在向下转换为派生类是完全安全的。同样,我调用了 BaseToChild
四次,每次针对一种类型的 cast,除了 const_cast
。下表显示了我得到的结果。
Cast | 结果 | 注释 |
---|---|---|
dynamic_cast |
成功 | 安全的 cast |
static_cast |
成功 | 安全的 cast |
reinterpret_cast |
成功 | 安全的 cast |
__try_cast |
成功 | 安全的 cast |
向下转换的建议
如果您绝对确定 cast 将是安全的,您可以使用上面四种 cast 运算符中的任何一种,但我建议您使用 static_cast
,因为那是最高效的 cast 方法。如果您对 cast 的安全性有一点点的不确定,您绝对应该*避免*使用 static_cast
和 reinterpret_cast
,这两种在这里都相当危险。您可以根据您喜欢检查 NULL
还是喜欢引发并处理异常,来选择使用 dynamic_cast
或 __try_cast
。
向上转换 (Upcasting)
向上转换是从派生类转换为继承链中的一个父类。通常向上转换是相当安全的,除非在某些罕见情况下,这些情况实际上是由于糟糕的编码而不是其他任何原因。我将在下面演示这两种情况。就像我有一个用于向下转换的函数一样,我也有一个用于向上转换的函数,名为 ChildToBase
。和以前一样,我不能在这里使用 const_cast
,因为我们是在不同类型之间进行转换。
Base* ChildToBase(Child* p,CastType casttype)
{
Base* retptr = NULL;
switch(casttype)
{
case tdynamic_cast:
retptr = dynamic_cast<Base*>(p);
break;
case tstatic_cast:
retptr = static_cast<Base*>(p);
break;
case tconst_cast:
/* This won't compile */
//retptr = const_cast<Base*>(p);
break;
case treinterpret_cast:
retptr = reinterpret_cast<Base*>(p);
break;
case ttry_cast:
try
{
retptr = __try_cast<Base*>(p);
}
catch(System::InvalidCastException*)
{
//...
}
break;
}
return retptr;
}
安全的向上转换
Base *pBase2 = NULL;
Child *pChild2 = new Child();
我正在从 Child*
转换为 Base*
,这是完全可以且安全的。下面我展示了每个 cast 运算符的结果。
Cast | 结果 | 注释 |
---|---|---|
dynamic_cast |
成功 | 安全的 cast |
static_cast |
成功 | 安全的 cast |
reinterpret_cast |
成功 | 安全的 cast |
__try_cast |
成功 | 安全的 cast |
不安全的向上转换
Base *pBase2 = NULL;
/* Intentionally create a bad pointer */
Child *pChild2 = reinterpret_cast<Child*>(new Base2());
在这里,我故意创建了一个 Base2
对象,并使用 reinterpret_cast
将其转换为 Child
对象。这是一个完全不安全的 cast,但我这样做是为了演示执行不安全的向上转换会发生什么。我在下表中展示了我的结果:
Cast | 结果 | 注释 |
---|---|---|
dynamic_cast |
失败 | 失败并返回 NULL |
static_cast |
成功 | 进行了不安全的 cast |
reinterpret_cast |
成功 | 进行了不安全的 cast |
__try_cast |
失败 | 失败并抛出异常 |
向上转换的建议
在大多数情况下,向上转换应该是相当安全的,除非您有一个糟糕的派生类指针(糟糕是指它指向了错误的对象)。因此,我建议向上转换使用 static_cast
,它应该是最高效的。如果您的向上转换不安全,那很可能是时候坐下来分析您的代码中出了什么问题,而不是使用 dynamic_cast
或 __try_cast
。另外请记住,在大多数情况下,向上转换可能是隐式进行的。
const_cast 的用法
请看下面的代码:
const Base *pB = new Base();
/* won't compile */
pB->dummy = 100;
您会收到一个编译器错误,因为您试图修改一个 const
对象。使用 const_cast
,您可以快速克服这种情况。
const_cast<Base*>(pB)->dummy = 100;
/* should show 100 on the console */
Console::WriteLine(pB->dummy);
const_cast
的另一个有用应用是 const
成员函数。当您将类成员标记为 const
时,您就是在告诉编译器该函数是只读函数,不会修改对象。这意味着您的 this
指针现在是 const Class * const
而不仅仅是 Class * const
。现在假设由于某种原因,您想从这个成员函数中修改某个成员。您可以按如下所示使用 const_cast
。当然,如果您不得不取消所有 const
成员函数的 const 性,您可能也需要重新考虑您的设计。
void A::abc() const
{
const_cast<A* const>(this)->m_total++;
}
reinterpret_cast 的用法
正如我之前提到的,这是所有 C++ cast 运算符中最不安全的,最好避免使用它。但话说回来,在移植旧代码时,您可能希望将旧式 cast 转换为 reinterpret_cast
。在我的例子中,Base2
和 Base
是两个托管类,它们除了自动派生自 System::Object
之外,没有共享继承链。假设由于某种原因,我们需要将一个 Base2
对象转换为一个 Base
对象,然后再将其转换回 Base2
对象。
Base *pB1 = NULL;
Base2 *pB2 = new Base2();
让我们先尝试使用 dynamic_cast
:
/* Compiles, but fails */
pB1 = dynamic_cast<Base*>(pB2);
if(!pB1)
Console::WriteLine("dynamic_cast failed");
它编译得很好,但在运行时失败。现在让我们尝试使用 static_cast
:
/* Won't compile */
//pB1 = static_cast<Base*>(pB2);
好的,太棒了!这甚至无法编译,因为编译时检查失败。那么 __try_cast
呢:
/* Compiles, but throws exception */
try
{
pB1 = __try_cast<Base*>(pB2);
}
catch(System::InvalidCastException*)
{
Console::WriteLine("__try_cast has thrown an exception");
}
就像 dynamic_cast
一样,这也能编译通过,但在运行时抛出异常。现在只剩下 reinterpret_cast
了。
/* This is a sort of blind man's cast */
pB1 = reinterpret_cast<Base*>(pB2);
哦,天哪!这既能编译也能运行。没有错误。pB1
,它被声明为一个 Base
对象,现在保存了一个 Base2
对象。为了测试这一点,我们可以尝试使用 dynamic_cast
再次转换它。
/* Now we cast it back */
Base2* pDest = dynamic_cast<Base2*>(pB1);
if(pDest)
{
Console::WriteLine("Original pointer has been obtained");
}
嗯,正如您现在可能已经理解的,最安全的方法是避免使用 reinterpret_cast
,但它确实有其用途,没有它,在移植旧代码时我们仍然不得不使用 C 风格的 cast。
结论
好吧,casts 的魔力还没有消失,casts 通过 C++ cast 运算符继续存在,并继续吸引着全世界的 C/C++ 爱好者。我不确定我在文章中提出的所有建议是否都很好很正确。但我相信我会得到必要的批评性反馈来纠正我可能犯下的任何错误陈述和建议。