热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

EffectiveC++——条款27(第5章)

条款27:尽量少做转型动作MinimizecastingC++规则的设计目标之一是,保证类型错误绝不可能发生.不幸的是,转型(cast)破坏了类型系统.

条款27:    尽量少做转型动作

Minimize casting

    C++规则的设计目标之一是,保证"类型错误"绝不可能发生.不幸的是,转型(cast)破坏了类型系统.

    首先回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作.C风格的转型动作看起来像这样:
(T)expression;                    // 将expression转型为T
    函数风格的转型动作看起来像这样:
T(expression);                    // 将expression转型为T
    两种形式并无差别,纯粹只是小括号的摆放位置不同而已.
    C++还提供了四种新式转型:
const_cast(expression);
dynamic_cast(expression);
reinterpret_cast(expression);
static_cast(expression);
    各有不同的目的(详见static_cast, dynamic_cast, const_cast探讨):
    const_cast
通常被用来 将对象的常量性转除(cast away the constness).它也是唯一有此能力的C++ style转型操作符.
     dynamic_cast 主要用来 执行"安全向下转型"(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型.它是唯一无法由就是旧式语法执行的动作,也是唯一 可能耗费重大运行成本的转型动作.
     reinterpret_cast 意图 执行低级转型,实际动作(及结果)可能取决于编译器,这就表示它 不可移植性.
     static_cast 用来 强迫隐式转换(implicit conversions),例如将non-const 对象转为 const 对象(就像 条款3(尽量使用const)所为),或将 int 转为 double 等等.它也可以用来执行上述多种转换中的反向转换,例如将 void* 指针转换为typed指针,将pointer-to-base转为pointer-to-derived.但它无法将 const 转为 non-const——这只有 const_cast 才办得到.
    旧式转换仍然合法,但 新式转换更受欢迎.原因是:第一,它们 很容易在代码中被辨识出来,因而得以简化"找出类型系统在哪个点被破坏"的过程.第二, 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用.例如,如果打算将常量性(constness)去掉,除非使用新式转换中的 const_cast 否则无法通过编译.
    许多程序员认为, 转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型.这是错误的观念.任何一种类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的代码.例如:
int x, y;
double d = static_cast(x)/y;
    将 int x 转型为 double 几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int 的底层表述不同于 double 的底层表述.这或许不会令人惊讶,但请看下面这个例子:
class Base { ... };
class Derived : public Base { ... };
Derived d;
Base* pb = &d;                // 隐式地将Derived* 转换为Base*
    这里不过是建立一个base class 指针指向一个derived class 对象,但有时候上述的两个指针并不相同.这种情况下会有个偏移量(offset)在运行期间被施行于Derived* 指针上,用以取得正确的Base* 指针.
    上述例子表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如"以Base*指向它"时的地址和"以Derived*指向它"时的地址,实际上一旦使用多重继承,这种事情几乎一直发生,因此避免做出"对象在C++如何布局"的假设)行为.
    但请注意,只是有时候需要一个偏移量. 对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着"由于知道对象如何布局"而设计的转型,不一定在所有平台都行得通.
    另一件关于转型的有趣事情是:很容易写出某些似是而非的代码.例如许多应用框架(application frameworks)都要求derived class 内的 virtual 函数代码的第一个动作就先调用base class 的对应函数.假设有个Window base class 和一个SpecialWindow derived class,两者都定义了 virtual 函数onResize.进一步假设SpecialWindow的OnResize函数被要求首先调用Window的OnResize.下面是实现方式之一,它看起来是对的,但实际是错的:
class Window {
public:
    ...
    virtual void OnResize() { ... }
};
class SpecialWindow : public Window {
    ...
    virtual void OnResize() {                    // derived onResize实现代码
        static_cast(*this).OnResize();    // 将*this转型为Window,然后调用其OnResize
        ...        // 这里进行SpecialWindow专属行为
    }
};
    这段程序将*this 转型为Window,对函数OnResize的调用也因此调用了Window::OnResize.但是,它调用的并不是当前对象上的函数,而是稍早转型动作建立的一个"*this对象的base class成分"的暂时副本身上的OnResize!再次强调上述代码并非在当期对象身上调用Window::OnResize之后又在该对象身上执行SpecialWindow专属动作.不,它是在"当前对象的base class成分"的副本上调用Window::OnResize,然后在当前对象身上执行SpecialWindow专属动作.如果Window::OnResize修改了对象内容,当前对象其实没有被改动,改动的是副本.
     解决之道是拿掉转型动作,替换为真正想实施的动作.并不像哄骗编译器将*this 视为一个base class 对象,只是想调用base class 版本的OnResize函数,令它作用于当前对象身上.所以请这样写:
class SpecialWindow : public Window {
public:
    virtual void OnResize() {
        Window::OnResize();        // 调用Window::OnResize作用于*this身上
        ...                        // SpecialWindow的专属动作
    }
};
    在探究 dynamic_cast 设计意涵之前,值得注意的是, dynamic_cast 的许多实现版本执行速度相当慢.例如至少有一个很普遍的实现版本基于"class名称的字符串比较",如果在四层深的单继承体系内的某个对象身上执行 dynamic_cast,则那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcpm调用,用以比较 class 名称.因此,除了对一般转型保持机敏与猜疑,更 应该在注重效率的代码中对dynamic_cast保持机敏与猜疑.
    之所以需要dynamic_cast,通常是因为想在一个认定为derived class 对象上执行derived class 操作函数,但手上却只有一个"指向base"的pointer或reference,只能靠它们来处理对象. 有两个一般性做法可以避免这个问题.
     第一,使用容器并在其中存储直接指向derived class 对象的指针(通常是智能指针,详见 条款13(以对象管理资源)),如此便消除了"通过base class接口处理对象"的需要.
    当然了,这种做法使得无法在同一个容器内存储指针"指向所有可能的各种派生类"。如果需要处理多种类型,可能需要多个容器,它们都必须具备类型安全性.
     另一种做法可让程序员通过base class 接口处理"所有的各种派生类",那就是在base class 内提供 virtual 函数做想做的对各种派生类做的事.
    不论哪一种写法——"使用类型安全容器"或"将virtual函数往继承体系上方移动"——都并非完全正确,但在许多情况下它们都提供一个可行的dynamic_cast替代方案.当它们有效时,就应该欣然拥抱它们.
    绝对必须避免的一件事就是所谓的"连串(cascading)dynamic_cast".
     优秀的C++代码很少使用转型,但若要完全摆脱它们又太过不切实际.因此,应该尽可能隔离转型动作,通常把它隐藏在某个函数内,函数接口会保护调用者不受函数内部任何动作的影响.
    注意:
    如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast.如果有个设计需要转型动作,试着发展无需转型的替代设计.
    如果转型是必要的,试着将它隐藏于某个函数背后.客户随后可以调用该函数,而不需将转型放进他们自己的代码内.
    宁可使用C++ style(新式)转型,不要使用旧式转型.前者很容易辨识出来,而且也比较有着分类的执掌.


推荐阅读
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了源码分析--ConcurrentHashMap与HashTable(JDK1.8)相关的知识,希望对你有一定的参考价值。  Concu ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Java集合库的使用方法,包括如何方便地重复使用集合以及下溯造型的应用。通过使用集合库,可以方便地取用各种集合,并将其插入到自己的程序中。为了使集合能够重复使用,Java提供了一种通用类型,即Object类型。通过添加指向集合的对象句柄,可以实现对集合的重复使用。然而,由于集合只能容纳Object类型,当向集合中添加对象句柄时,会丢失其身份或标识信息。为了恢复其本来面貌,可以使用下溯造型。本文还介绍了Java 1.2集合库的特点和优势。 ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • 学习笔记17:Opencv处理调整图片亮度和对比度
    一、理论基础在数学中我们学过线性理论,在图像亮度和对比度调节中同样适用,看下面这个公式:在图像像素中其中:参数f(x)表示源图像像素。参数g(x)表示输出图像像素。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了VoLTE端到端业务详解|VoLTE用户注册流程相关的知识,希望对你有一定的参考价值。书籍来源:艾怀丽 ... [详细]
author-avatar
ZHANGQI0001234
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有