企业网站备案,wap网站生成,深圳专业产品设计,小程序开发公司文案1.前言
C规则的设计目标之一是保证“类型错误”绝对不可能发生。理论上如果你的程序很顺利的通过编译#xff0c;就表示它并不企图在任何对象身上执行任何不安全#xff0c;无意义的操作。这是个极具价值的保证#xff0c;可别草率的放弃它。
不幸的是#xff0c;转型规则的设计目标之一是保证“类型错误”绝对不可能发生。理论上如果你的程序很顺利的通过编译就表示它并不企图在任何对象身上执行任何不安全无意义的操作。这是个极具价值的保证可别草率的放弃它。
不幸的是转型cast破坏了类型系统type system。那可能导致任何种类的麻烦有些容易识别有些容易隐晦。如果你来自c,java,c#阵营请特别注意因为那些语言中的转型cast比较必要而无法避免相对来说也不危险但c中转型是一个你会想带着极大的尊重去亲近的一个特性。
2.转型cast知识点的回顾
首先让我们回顾转型语法因为通常有三种不同的形式可写出相同的转型动作。c风格的转型动作看起来像这样
Texpression//将expression转型为T
函数风格的转型动作看起来像这样
Texpression//将expression转型为T
两种形式并无差别纯粹只是小括号的摆放位置不同而以。称此两种形式为“旧式转型”。
C还提供四种新式转型常被称为c style casts
const_castT(expression)
dynamec_castT(expression)
reinterpret_castT(expression)
static_catsT(expression)
上述转换类型各有不同的作用
const_cast通常被用来将对象的常量性转除cast away the constness,它也是唯一有此能力的c-style转型操作符
dynamic_cast主要用来执行“安全向下转型”也就是用来决定某对象是否继承体系中的某个类型。它是唯一无法由旧式语法执行的动作也是唯一可能耗费重大运行成本的转型动作
reinterpret_cast意图执行低级转型实际动作可能取决于编译器这也表示它不可移植。例如将一个pointer to int 转型为int。这一类转型在低级代码以外很少见。本来只使用一次那是在讨论如何针对原始内存写出一个调试用的分配器
static_cast用来强迫隐式转换例如将non-cast对象转为const对象或将int转为double等等。它也可以用来执行上述多种类型的反转换例如将void*指针转为typed指针将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const这个只有const_cast能办到
旧式转型仍然合法但新式转型较受欢迎。原因是
1很容易在代码中被识别出来
2各转型动作的目标愈窄化编译器越可诊断出错误的运用。比如你如果打算将常量性去掉除非使用新式转型中的const_cast否则无法通过编译。
目前唯一使用旧式转型的时机当我要调用一个explict构造函数将一个对象传递给一个函数时。例如
class Widget{public:explict Widget(int size);...
};
void doSomeWork(const Widget w);
doSomeWork(Widget(15));//以一个Int加上“函数风格”的转型动作创建一个Widget
doSomeWork(static_castWidget(15));//以一个int加上“c风格”的转型动作创建一个Widget
从某个角度来说刻意的“对象生成”动作感觉不怎么像“转型”所以我很可能使用函数风格的转型动作而不使用static_cast。但我要提醒下当我们日后出错导致coredump的代码时编写的时候我们往往觉得说的过去所以最好是忽略自己的主管想法始终理智地使用新式转型。
许多程序员认为转型其实什么都没做。只是告诉编译器把某种类型看作另一种类析。这是种错误的观念任何一个类型转换不论是通过转型操作而进行的显式转换或通过编译器完成的隐式转换往往真的令编译器编译出运行期间执行的码。比如在下面这段程序中
int x,y;
...
double dstatic_castdouble(x)/y;//x除以y使用浮点除法将int x转型为double几乎肯定会产生一些代码因为在大部分计算器体系结构中int的底层表述不同于double的底层表述。这或许不会让你惊讶但下面这个例子就有可能让你稍微睁大眼睛
class Base{....};
class Derived:public Base{...};
Derived d;
Base* pbd;//隐喻地将derived*转换成base*
这里我们不过是建立一个base class指针指向一个derived class对象但有时候上述的两个指针值并不相同。这种情况下会有个偏移量offset在运行期间被施行于derived*指针身上用以取得正确的Base*指针值。
上述例子表明单一对象可能拥有一个以上的地址例如“”以base指向它时的地址和以derived指向它时的地址。实际上一旦使用多重继承这事情几乎一直发生者。即使在单一继承中也可能发生。虽然这还有其它含义但至少意味你通常应该避免做出“对象在c中如何布局”的假设。当然更不应该以此为假设执行任何转型动作。例如将对象地址转型为char*指针然后在它们身上进行指针算术这样总会导致无定义不明确行为。
但请注意我说的是有时候需要一个偏移量。对象的布局方式和它们的地址计算方式随编译器的不同而不同那意味着由于知道对象如何布局而设计的转型在某一平台行的通再另一平台并不一定行的通。
另一件关于转型的有趣的事情是我们很容易写出某些似是而非的代码。比如许多应用框架都要求derived class内的virtual函数代码的第一个动作就是先调用base class的对应函数。假设我们有个Window base class和一个SpecialWindow derived class两者都定义了virtual函数的onResize进一步假设SpeciaWindow的onResize函数被要求首先调用Window的onResize,下面是实现方式之一相关程序看起来是对的实际上是有问题的
class Window{public:virtual void onResize(){....//base onResize实现代码}};class SpecialWindow:public Window{//derived classpublic:virtual void onResize(){ //derived onResize实现代码static_castWindow(*this).onResize();//将*this转型为Window然后调用其 //onResize;这不可行。...//这里进行 SpecialWindow专属行为}}
上面代码中强调了转型动作那是个新式转型但若使用旧式转型也不能改变以下事实。如预期的那样这段程序将*this转型为Window对函数onResize的调用也因此调用了Window::onResize。但实际上它调用的并不是当前对象上的函数而是稍早转型动作所建立的一个“this对象之base class成分”的暂时副本身上的onResize。再强调一次上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作它是在“当前对象之base class成分”的副本上调用Window::onResize然后在当前对象身上执行SpeciaWindow专属动作。如果Winodw::onResize修改了对象内容当前对象其实没被改动改动的是副本。然而SpecialWindow::onResize内如果也修改对象当前对象真的会被改动。这使得当前对象进入一种伤残状态其base class成分的更改没有落实而derived class成分的更改倒是落实了。
解决的方法是换掉转型动作而不是将*this视为一个base class对象你只是想调用base class版本的onResize函数令它作用于当前对象身上。所有这样编写代码
class SpecialWindow:public Window{public:virtual void onResize(){Window::onREsize();//调用Window::onResize作用于*this身上...}...
};
这个例子也说明如果自己打算转型可能会面临者将局面发展至错误的方向上。如果用的是dynamic_cast更是如此。
在研究dynamic_cast设计意义之前值的关注的是dynamic_cast的许多实现版本执行速度相当慢至少有一个很普遍的实现版本基于“class名称之字符串比较”如果在四层深的单继承体系内的某个对象身上执行dynamic_cast刚才说的那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcmp调用用于比较class名称。深度继承或多重继承的成本更高。
之所以需要dynamic_cast通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。
第一使用容器并在其中存储直接指向derived class对象的指针如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果试着不要这样做
class Window{...};
class SpecialWindow:public Window{public:void blink();...
};typedef
std::vectorstd::trl::shared_ptrWindow VPW;
VPW winPtrs;
...
for(VPW::iterator iterwinPTRS.begin();iter!winPtrs.end();iter){//不希望使用//dynamic_castif(SpecialWindow* pswdynamic_castSpecialWindow*(iter-get())){psw-blink();}
}
应该改为这样做
typedef std::vectorstd::trl::shared_ptrSpecialWindow VPSW;
VPSW winPtrs;
...for(VPSW::iterator iterwinPTRS.begin();iter!winPtrs.end();iter){(*iter)-blink();//这样写比较好不需要使用dynamic_cast
}
当然了这种做法使得你无法在同一容器内存储指针“指向所有可能之各种Window派生类”。如果真的要处理多种窗口类型你可能需要多个容器它们都必须具备类型安全性。
另一种做法可让你通过base class接口处理“所有可能的各种Window派生类”那就是在base class内提供virtual函数做你想对各种Window派生类做的事。举个例子虽然只有SpecailWindows可以闪烁但或许将闪烁函数声明于base class内并提供一份缺省实现码是有意义的
class Window{public:virtual blink()0;...
}
class SpecialWindow:public Window{public:override blink(){....}...
};
typedef std::vectorstd::trl::shared_ptrWindow VPW;
VPW winPtrs;//容器内含指针指向可能的Window类型
...
for(VPW::iterator iterwinPtrs.begin();iter!winPtrs.end();iter){*iter-blink();//注意这里没有dynamic_cast。
}
无论是哪一种写法-“使用类型安全容器”或“将virtual函数往继承体系上方移动”都并非放之四海皆准但在许多情况下它们都提供一个可行的dynamic_cast替代方案。当它们起作用时应该欣然拥抱它们。
绝对避免的一件事情是所谓的“连串dynamic_casts”也就是看起来像这样的东西
class Window{...
};
...//derived classes定义在这里
typedef std::vectorstd::trl::shared_ptrWindow VPW;
VPW winPtrs;
...
for(VPW::iterator iterwinPtrs.begin();iter!winPtrs.end();iter){if(SpecialWindow1* psw1dynamic_castSpecialWindow1*(iter-get())){...}else if(SpecialWindow2* psw2dynamic_castSpecialWindow2*(iter-get())){...}else if(SpecialWindow3* psw3dynamic_castSpecialWindow3*(iter-get())){...}...
}
这样产生的代码又大又慢而且基础不稳因为每次Window class继承体系一有改变所有这一类代码必须再次检阅看看是否需要修改。一旦加入新的derived class或许上述连串判断中需要加入新的条件分支这样的代码应该总是以某些“基于virtual函数调用”的东西取而代之。
良好的c代码很少使用转型但若说要完全摆脱它们又不切实际例如将int转型为double就是转型的一个通情达理的使用虽然它并非绝对必要就像面对众多蹊跷可疑的构造函数一样我们应该尽可能隔离转型动作通常是把它隐藏在某个函数内函数的接口会保护调用者不受函数内部任何动作影响。
3.总结
由于本文内容较多将以上内容总结为以下几点
1.如果可以尽量避免转型特别是在注重效率的代码中避免dynamic_cast,如果有个设计需要转型动作尝试发展无需转型的替代设计。
2.如果转型是必要的试着将它隐藏在某个函数背后。客户随后可以调用该函数而不需要将转型放进它们的代码内
3.一旦不得不使用转型宁可使用cstyle新式转型不要使用旧式转型。前者很容易辨识出来而且也相对职责分明。