Effective C++ 条款 39、40、41_in turns of-程序员宅基地

技术标签: C++进阶  

条款三十九:明智而审慎地使用private继承

private继承的意义在于“be implemented in turns of”,这个与上一条款中说的复合模型的第二层含义是相同的,这也意味着通常我们可以在这两种设计方法之间转换,但书上还是更提倡使用复合来进行类的设计。

private继承与public的继承是完全不同的,主要体现在两个地方:

其一,public继承在子类中保持父类的访问权限,即父类中是public的成员函数或成员变量,在子类中仍是public,对private或者protected的成员函数或成员变量亦是如此;但private继承则不是这样了,它破坏了父类中的访问权限标定,将之都转成private,这对子类本身并无影响(照常访问),但却影响了子类的子类,子类的子类将无法访问这些声明/定义在爷爷辈类的成员变量或成员函数。

其二,Liskov法则不再适用,也就是说“一切父类出现的地方都可以被子类所替代”的法则在private这里不成立,请看下面的例子(来源自书上):

#include <iostream>
using namespace std;

class Person{};

class Student : private Person{};

void eat(const Person& p){}

int main()
{
    Person p;
    Student s;
    eat(p); // OK
    eat(s); // 编译报错:error C2243: “类型转换”: 从“Student *”到“const Person &”的转换存在,但无法访问
}

但如果令Student公有继承Person,则编译器不会报错。这正是Liskov的可替代原则在private继承中不适用的体现。

private继承使用的地方实在不多,除非有一些奇葩的设计需求,书上说了一个例子:

class TypeDefine
{};

class SimpleClass
{
    int a;
    TypeDefine obj;
};

class SimpleDerivedClass : private TypeDefine
{
    int a;
};

int main()
{
    cout << sizeof(TypeDefine) << endl; // ?
    cout << sizeof(SimpleClass) << endl; // ?
    cout << sizeof(SimpleDerivedClass) << endl; // ?
}

大家可以想一下“?”处的输出是什么。第一个是空类,空类就像是空气一样,仅仅是名字里面包含了“空”字,看起来是“空”的,但其实不是这样子的,空气里面混合了氧、氮、二氮化碳等气体,还有各种微生物,而对于空类,编译器会为之生成四个默认的函数:默认构造函数,默认拷贝构造,默认析构函数,默认赋值运算符。读者就会问了,编译器生成了默认的函数不假,但函数是不占空间的,为什么空类的sizeof算出的值是1?原来类的每一个对象都需要一个独一无二的内存地址,所以编译器会在空类对象中插入一个1字节变量,正是这个1字节的变量,才能够区分空类的不同对象。非空类因为已经有了成员变量,所以编译器可以利用这些成员变量来进行内存地址的区分,从而标识类的不同对象,这个时候是不需要插入一个1字节的变量的。所以第一个问号处输出的是1。

第二个问号输出的是5吗?int四字节再加到空类对象的四字节?理论上是这样,但编译器还会做一种内存对齐的操作,使得类对象的大小会是处理字长的整数倍,一般是4字节的整数倍,所以最后的结果其实是8。

第三个问号呢?前面讲的那么多,好像都与private无关,这个问题终于与它有关了。运行下看看,结果是4。为什么用复合模型时输出的结果是8,但private继承时却是4呢?这其实是编译器做了空白基类优化(EBO),原本是要为空白类对象插入1字节的,但因为子类中已经有了对象了,这样理论上就可以凭借这个对象来进行同一个类不同对象间的识别了,所以这时候编译器就不再插入字节了。

这个结果就是用private继承的好处,是不是很奇葩呢~所以我说,在大部分情况下,都不会考虑private继承,因为它的含义be implemented in terms of 可以用复合来替换。

书上还提到了关于虚函数不想被子类的子类所覆写的问题,这时候不能用private限制虚函数,因为生成的虚指针是一直会被继承下去的,解决方法就是用复合,而且复合的类是一个临时类且复合对象标记为private,这样就只能限制在这个类本身去覆写了。具体的例子可以去看原书。

最后总结一下:

  1. Private继承意味着is implemented in terms of,它通常比复合的级别低(即优先使用复合),但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  2. 与复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

条款四十:明智而审慎地使用多重继承

多重继承是一种比较复杂的继承关系,它意味着如果用户想要使用这个类,那么就要对它的父类也了如指掌,所以在项目中会带来可读性的问题,一般我们都会尽量选择用单继承去替代它。
使用多重继承过程容易碰到的问题就是名字冲突,像下面这样:

class Base1
{
public:
    void fun(){}
};

class Base2
{
private:
    void fun(){}
};

class Derived : public Base1, public Base2
{};

int main()
{
    Derived d;
    d.fun(); // error C2385: 对“fun”的访问不明确
    return 0;
}

因为在两个父类中都有名为fun的函数,所以这时候编译器不知道用户想调用的是哪个函数。但这里细心的读者会发现,这里我们是把Base2的fun的访问权限设为了private的。这个例子同时也说明了,编译器会优先去查找最合适的重载函数,再去考虑它的可访问性。如果真的要去访问重名的函数,可以指定作用域,像这样d.Base1::fun()(但注意d.Base2::fun()不行,因为它的访问性是private的)。

多重继承另一个容易碰到的问题就是虚继承,我记得这还是面试官的一道面试题。试想一下,有一个父类名为A,类B和类C都继承于A,类D又同时继承了B和C(多重继承),那么如果不做任何处理,C++的类继承图里会包含两份A。

但如果在继承的时候加了virtual,像下面这样:
class B: virtual public A{…}
class C: virtual pulibc A{…}
那么D中就只有一份A了。C++标准库里面的流就是采用这样的形式,有一个父流basic_ios,basic_istream和basic_ostream分别虚继承于basic_ios,而basic_iostream又多重继承于basic_istream和basic_ostream。

为了保证不会出现两份父类,只要是public继承理论上都应该有virutal关键字,但virutal也是有代价的,访问virtual base class的成员变量要比访问non-virutal base class的成员变量速度要慢。所以作者的忠告是:

  1. 非必要不使用virtual classes继承,普通情况请使用non-virtual classes继承
  2. 如果必须使用virtual base classes,尽可能避免在其中放置数据。

    后面的篇幅书上就举了一个多重继承的例子,在这里我就不说了,有兴趣的读者可以自己看看,但个人觉得还是能不用多重继承的时候,就尽量不用它,用复合+单继承往往能达到目的。

最后总结一下:

  1. 多重继承比单一继承更复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  3. 多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两两组合。

条款四十一:了解隐式接口和编译期多态

从本条款开始,就进入了全书的第七部分:模板与泛型编程。模板与泛型在C++中是非常重要的部分,还记得本书第一章时,把C++视为一个联邦,它由四个州政府组成,其中一个政府就是模板与泛型了。

本条款是一个介绍性质的条款,内容不难,只需要讲清楚两个概念就行了,即什么是隐式接口,什么是编译期多态。

隐式接口是相对于函数签名所代码的显式接口而言的。当我们看到一个函数签名(即函数声明),比如说:

string GetNameByStudentID(int StudentID);

我们就知道这个函数有一个整型的形参,返回值是string。

但隐式接口是由有效表达式组成的,考虑一个模板函数,像下面这样:

  template <class T>
void TemplateFunction(T& w)
 {
     if(w.size() > 10){…}
 }

T可以是int,可以double,也可以是自定义的类型。光看这个函数声明,我们不能确定T具体是什么,但我们知道,要想通过编译,T必须要支持size()这个函数。也就是说,T中一定要有这样的函数接口声明。

ReturnValue size();

当然返回值ReturnValue不一定是int了,只要它能支持operator > (ReturnValue, 10)这样的运算即可。这种由表达式推判出来的函数接口,称之为隐式接口。

简言之,显式接口由函数签名式构成,隐式接口由有效的表达式组成。

下面讨论编译期多态的问题,我们在讨论继承时就已经多次提到“运行时多态”了,它伴随着virtual关键字,本质是一个虚表和虚指针,在类对象构造时,将虚指针指向了特定的虚表,然后运行时就会根据虚表的内容进行函数调用。

那么“编译期多态”又是什么呢,从字面上来看,它发生在编译阶段,实际上就是template 这个T的替换,它可以被特化为int,或者double,或者用户自定义类型,这一切在编译期就可以决定下来T到底是什么,编译器会自动生成相应的代码(把T换成具体的类型),这就是编译期多态做的事情了,它的本质可以理解成自动生成特化类型版本,T可以被替换成不同的类型,比如同时存在int版本的swap与double版本的swap,形成函数重载。简言之,运行时多态是决定“哪一个virtual函数应该被绑定”,而编译期多态决定“哪一个重载函数应该被调用”。

最后总结一下:

  1. class和template都支持接口与多态;

  2. 对class而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;

  3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/coolwriter/article/details/79745431

智能推荐

西安工业大学计算机考研率高不高,西安工业大学考研难吗?一般要什么水平才可以进入?...-程序员宅基地

文章浏览阅读1k次。首先,申明一点:考研本身就不是一件容易的事情,在考研的过程中,找准自己的定位、学会搜集资料、搜集信息并且辅之于踏实的备考、准确的方法是至关重要的。同时,要学会坚持,不忘初心,很多同学在开始备考时,斗志昂扬,但是在进度接近尾声时,自己的志气也接近了尾声,这是万万不行的。有句话说的好:“骆驼走得慢,但终能到达终点”。我们也是,过程中,要循序渐进的进步,一步一步的坚持备考,终能到达彼岸。接下来,给大家一..._西安工业大学考研率高不高

MVC与MVC2的区别与对比-程序员宅基地

文章浏览阅读696次。mvc模式:view接收用户输入,把命令传到controllercontroller处理命令,更新modelmodel被更新后,会通知view需要updateview更新后向用户显示mvc2模式:由于mvc1中,model可以通知view,然后view就可以被更新,这在windows程序中很常见,像MFC的frame-document-view架构,如果document改变了,他会主动通知view..._mvc-ii-3m

dmesg命令查看java程序突然挂掉的原因_dmesg -t | grep java-程序员宅基地

文章浏览阅读2.6k次。背景:JAVA服务线上毫无征兆的直接crash掉,打开日志查看,日志文件毫无相关挂掉的信息,所以当时直接选择了重启,当时的猜测是:服务内存不足导致程序进程直接挂掉?查找原因后来学习查找到一个命令dmesg命令,这个命令还是非常强大的。dmesg命令行实用程序用于在Linux和其他类似Unix的操作系统中打印和控制内核环形缓冲区。对于检查内核启动消息和调试与硬件相关的问题很有用。可以在服务器执行命令:# 按时间格式显示dmesg -T# 显示跟java 有关的日志dmesg -T |._dmesg -t | grep java

CSS justify-content 属性_css的justify-content-程序员宅基地

文章浏览阅读192次。CSS justify-content 属性在弹性盒对象的 元素中的各项周围留有空白:div{ display: flex; justify-content: space-around;}_css的justify-content

关于AJAX跨域调用ASP.NET MVC或者WebAPI服务的问题及解决方案_调用evms侧webservice失败-程序员宅基地

文章浏览阅读95次。关于AJAX跨域调用ASP.NET MVC或者WebAPI服务的问题及解决方案_调用evms侧webservice失败

linux 删除软链接文件_linux删除软链接会删除源文件么-程序员宅基地

文章浏览阅读2.8k次。软链接即用 ln -s 原始文件或文件夹 目标文件或文件夹当想删除链接文件时 如果不小新 会把原始文件删掉例:ln -s /home/user/bak/ /var/bakrm /var/bak/ 会提示bak是一个文件夹无法删除这时千万不要用 rm -rf /var/bak/ 来删除 否则你删除的并不是链接文件 而是真正的 /home/user/bak文件夹想要_linux删除软链接会删除源文件么

随便推点

计算机组成原理期末复习整理_刷新存储器的容量由共同决定-程序员宅基地

文章浏览阅读2.9k次,点赞7次,收藏66次。计算机组成原理一 计算机的基本组成1.计算机系统指的是电子数字通用计算机系统(使用电子线路,数字式电路,功能多样)2.一个完整的计算机系统包括两大部分:硬件系统和软件系统计算机组成示意图:3.冯诺伊曼体系结构由五大部件组成:运算器,控制器,存储器,输入设备,输出设备(以运算器为中心)地址总线:包括地址总线(AB),数据总线(DB),控制总线(CB)五大部件都是做什么的,简答一下存储器:存放指令和数据运算器:在控制器控制下,进行计算机和逻辑运算控..._刷新存储器的容量由共同决定

02浏览器模拟移动端_浏览器模拟移动端上拉底图-程序员宅基地

文章浏览阅读103次。from selenium import webdriverfrom time import sleepoptions = webdriver.ChromeOptions()mobileEmulation={"deviceName": "iPhone 8"} # 模拟移动端options.add_experimental_option("mobileEmulation", mobileEmulation)driver=webdriver.Chrome(options=options)driv._浏览器模拟移动端上拉底图

论文阅读 (48):A Library of Optimization Algorithms for Organizational Design-程序员宅基地

文章浏览阅读347次。2005Levchuk:用于组织设计的优化算法库 (A library of optimization algorithms for organizational design);背景:介绍用以解决各组织规范设计中广泛出现的优化问题算法库,以满足特定任务的需要。设计过程不同阶段的特定优化算法的使用,引发了任务结构与组织、资源/约束之间的有效匹配问题。该算法库构成了所设计软件环境的核心,用于综合与其任务一致的组织。这允许分析者在多个目标和约束中获得可接受的权衡,且满足计算复杂性与解决方案效率的_a library of optimization algorithms for organizational design

编写代码超过限制时间_编写代码时浪费时间。-程序员宅基地

文章浏览阅读648次。编写代码超过限制时间by Jonathan Solórzano-Hamilton 乔纳森·索洛萨诺·汉密尔顿(JonathanSolórzano-Hamilton) 编写代码时浪费时间。 (When writing code is a waste of time.) 知道不应该构建什么是现代软件开发中最关键的部分。 (Knowing what not to build is the most..._一个代码方法卡了三天合理嘛

稚辉君HDMI-MIPI东芝方案(原创,连载中~)_hdmi 转 mipi-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏8次。HDMI-MIPI Adaptor前言前段时间在哔哩哔哩上看到稚辉君的Pocket LCD项目,感觉很有意思,正好也想要个便携式显示屏,我也来DIY一下~主要的流程如下:方案选择硬件选择软件设计记录1. 方案选择目前不需要充电宝这个功能,干脆就先弄个转换器吧~2. 硬件选择选用UP主的东芝方案,因为龙讯方案不开源,不能愉快地玩耍呀~3. 软件设计ASIC芯片是通过I2C接口进行配置的,先买个STM32F103C8xx来编下程,主要是太久没用STM32了,上次用还是几年前在学校的_hdmi 转 mipi

Spark的Shuffle机制_shuffle key-程序员宅基地

文章浏览阅读441次。MapReduce中的Shuffle转载于: http://www.jianshu.com/p/60bab35bc01e在MapReduce框架中,shuffle是连接Map和Reduce之间的桥梁,Map的输出要用到Reduce中必须经过shuffle这个环节,shuffle的性能高低直接影响了整个程序的性能和吞吐量。Shuffle是MapReduce框架中的一个特定的phase,介于..._shuffle key