• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2024-07-02 16:41 Aet 隐藏边栏 |   抢沙发  3 
文章评分 1 次,平均分 5.0

34 区分接口继承和实现继承

概述

  1. C++的面向对象编程中,继承可以用于两种目的:接口继承和实现继承
  2. 理解并区分这两种继承方式有助于设计更清晰、可维护和灵活的类结构

接口继承

  1. 子类继承基类的接口,但可以重新定义(覆盖)基类的方法
  2. 接口继承通常用于定义类的行为约定,使得不同的子类可以实现相同的接口而具有不同的行为

实现继承

  1. 子类不仅继承基类的接口,还继承基类的方法实现
  2. 实现继承通常用于代码重用,子类直接使用基类的方法实现,减少代码重复

区分

  1. 接口继承:
    1. 用于定义类的行为约定,使得子类可以实现相同的接口而具有不同的行为
    2. 通常使用纯虚函数
  2. 实现继承:
    1. 用于代码重用,使得子类可以直接使用基类的方法实现,减少代码重复
    2. 通常使用非纯虚函数

35 考虑虚函数的替代方案

概述

  1. 虚函数是实现多态的一种常见方式,但它们并不是唯一的选择
  2. 在设计类和接口时,可以考虑一些替代虚函数的方法,这些方法有时可以提供更好的性能、更低的复杂性和更高的灵活性

替代方案一:策略模式(Strategy Pattern)

  1. 策略模式是一种设计模式,它将行为定义为类,并将这些类的对象作为参数传递给需要这些行为的类
  2. 这种模式允许在运行时选择不同的行为,而不需要使用虚函数

替代方案二:模板(Templates)

  1. 模板是一种编译时多态性,可以在编译时决定函数的具体实现,避免了运行时的虚函数开销

替代方案三:函数对象(Function Objects)和Lambda表达式

  1. 函数对象和Lambda表达式是将行为封装成对象或匿名函数,可以在运行时灵活地传递和调用

36 永远不要重新定义继承的非虚函数

概述

  1. C++中,重新定义继承的非虚函数会导致难以理解和错误的行为。
  2. 非虚函数在编译时绑定,因此在子类中重新定义这些函数时,基类和子类对象的行为会不一致
    1. 为了确保代码的可读性和可维护性,应避免重新定义继承的非虚函数

为什么不要重新定义继承的非虚函数

  1. 编译时绑定:
    1. 非虚函数在编译时绑定,这意味着调用函数时取决于对象的静态类型而不是动态类型
    2. 重新定义非虚函数会导致基类和子类对象的行为不一致
  2. 意外行为:
    1. 重新定义非虚函数可能会导致意外的行为,因为调用基类函数时不会调用子类中重新定义的函数
  3. 代码可读性:
    1. 重新定义非虚函数会使代码变得难以理解和维护,因为开发者需要了解基类和子类的所有实现细节

示例代码

  1. 重新定义非虚函数

  1. 正确的做法:使用虚函数

37 永远不要重新定义继承函数的默认参数值

概述

  1. C++中,当子类继承基类的函数时,不应重新定义继承函数的默认参数值
  2. 这是因为默认参数值在编译时绑定,而不是在运行时绑定,这会导致在调用该函数时产生意外行为

为什么不要重新定义继承函数的默认参数值

  1. 编译时绑定:
    1. 默认参数值在编译时绑定,这意味着调用函数时使用的默认参数值取决于调用点的静态类型,而不是对象的动态类型
  2. 意外行为:
    1. 重新定义继承函数的默认参数值可能会导致意外行为,因为在基类指针或引用调用时仍然使用基类的默认参数值
  3. 代码可读性:
    1. 重新定义继承函数的默认参数值会使代码变得难以理解和维护,因为调用点的默认参数值取决于静态类型

示例代码

  1. 重新定义继承函数的默认参数值

  1. 正确的做法:在基类中设置默认参数值

38 通过组合来建模“has-a”或“is-implemented-in-terms-of”

概述

  1. 在面向对象设计中,有两种主要的类关系:继承和组合
  2. 继承用于表示“is-a”关系,而组合用于表示“has-a”或“is-implemented-in-terms-of”关系
  3. 组合通过在一个类中包含另一个类的对象来实现,优先考虑组合可以使代码更灵活、可重用性更高,并且减少类之间的耦合

为什么使用组合

  1. 灵活性:
    1. 组合比继承更灵活,可以在运行时改变包含的对象,从而改变类的行为
  2. 可重用性:
    1. 组合使得类可以重用已有类的功能,而不需要继承它们的接口或实现
  3. 降低耦合:
    1. 组合减少了类之间的耦合,因为它们通过接口而不是实现来交互
  4. 单一职责原则:
    1. 组合有助于遵循单一职责原则,每个类只负责一个特定的功能

示例代码

  1. 建模“has-a”关系
    1. 这个例子中,Car类通过组合包含了一个Engine对象,并在start方法中使用了Engine对象的功能

  1. 建模“is-implemented-in-terms-of”关系
    1. 这个例子中,List类通过组合包含了一个Array对象,并在adddisplay方法中使用了Array对象的功能

39 明智的使用私有继承

概述

  1. 私有继承是一种类继承方式,其中基类的公有和保护成员在子类中变成私有成员
  2. 与公有继承不同,私有继承强调实现细节而不是接口的一致性
  3. 尽管如此,私有继承也有其独特的用途,但应谨慎使用

私有继承的特性和用途

  1. 实现复用:
    1. 私有继承用于代码复用,而不是表示“is-a”关系
    2. 它强调实现的一部分是由基类提供的,但不希望将基类的接口暴露给外部
  2. 隐藏基类接口:
    1. 通过私有继承,可以隐藏基类的接口,仅在子类中使用基类的实现细节
  3. 访问保护和私有成员:
    1. 私有继承允许子类访问基类的保护成员和私有成员(如果使用友元类)

私有继承 vs 组合

  1. 尽管私有继承可以用于实现复用,组合通常是更好的选择,因为它提供了更高的灵活性和更低的耦合性
  2. 组合使用一个类的实例作为另一个类的成员,而不是继承其实现

何时使用私有继承

  1. 当子类需要复用基类的实现,但不希望暴露基类的接口时
  2. 当子类需要访问基类的保护成员时

何时使用组合

  1. 当类之间是“has-a”关系而不是“is-a”关系时
  2. 当需要更高的灵活性和更低的耦合性时
  3. 当希望公开或封装成员类的功能时

示例代码

  1. 私有继承

  1. 组合

40 明智地使用多重继承

概述

  1. C++中,多重继承允许一个类同时继承多个基类,这提供了很大的灵活性,但也带来了复杂性和潜在的问题

为什么需要谨慎使用多重继承

  1. 复杂性:
    1. 多重继承增加了类之间关系的复杂性,使代码更难以理解和维护
  2. 菱形继承问题:
    1. 当一个类通过多个路径继承自同一个基类时,可能会引发菱形继承问题,导致基类的成员被多次继承
  3. 名称冲突:
    1. 如果多个基类中有同名成员,可能会导致名称冲突,增加调试和维护的难度
  4. 构造和析构顺序:
    1. 多重继承会使构造函数和析构函数的调用顺序变得复杂,可能会导致资源管理问题

多重继承的正确使用

  1. 使用接口继承(纯虚基类):
    1. 将多个基类设计为纯虚基类(接口),从而避免菱形继承和资源管理问题
  2. 使用虚继承:
    1. 当需要解决菱形继承问题时,可以使用虚继承来确保基类只被继承一次

原理

  1. 虚函数表
    1. 在编译期,编译器为每个包含虚函数的类生成一个虚函数表
      总结:只要一个类有自己的虚函数,编译器就会为它生成一个虚函数表
    2. 虚函数表包含该类的虚函数的地址
    3. 虚函数表(vtable)是编译器生成的一个用于实现多态性的数据结构
    4. 在运行时存储在程序的内存中,具体地说,虚函数表通常存储在程序的数据段或只读数据段中
  2. 虚函数指针(vptr):
    1. 每个包含虚函数的对象实例在其内存布局中包含一个指向虚函数表的指针,称为虚函数指针(vptr
  3. 多继承
    1. 继承多个基类,如果基类都有虚函数,那么派生类实例化后就有多张虚函数表
      派生类的表里存储的是对应的重载版本的函数的地址
    2. C++通过在对象的内存布局中包含多个虚函数指针(vptr)来管理这些虚函数表,以确保虚函数调用能够正确分派
    3. 总结:一个类包含了几张虚函数表,那它的内存布局里就有几个vptr指针
    4. 另外,多继承中,通过基类指针调用虚函数时,只有当基类声明了该虚函数时,才能通过基类指针调用派生类重载的该虚函数

多重继承情况

  1. 继承了2个有虚函数的基类,但并没有自己的虚函数
    1. 派生类有2张虚函数表

  1. 继承了2个有虚函数的基类,派生类也有自己的虚函数表
    1. 派生类就有3张虚函数表

  1. 棱形继承
    1. 如果只有虚基类定义了虚函数,就有1虚函数表
    2. 如果基类1和基类2不仅重载了虚基类的虚函数,还都定义了自己虚函数,就有3张虚函数表
    3. 如果孙子类也定义了自己的虚函数,那总归就会有4张虚函数表

示例代码

  1. 使用接口继承

  1. 使用虚继承解决菱形继承问题

41 理解隐式接口和编译期多态

概述

  1. C++中,隐式接口和编译期多态是实现高效和灵活代码的重要手段
  2. 它们主要通过模板和泛型编程来实现

隐式接口

  1. 隐式接口是指通过模板实现的接口
  2. 在模板编程中,编译器会在编译期检查模板参数是否满足接口要求
  3. 隐式接口没有显式声明,而是通过使用特定的成员函数或操作符来定义
  4. 示例:
    1. 这个例子中,模板函数 print 期望模板参数 T 有一个 print 成员函数
    2. 编译器在实例化模板时检查这个要求

编译期多态

  1. 编译期多态(也称为静态多态)通过模板和泛型编程来实现
  2. 在编译期多态中,函数和类模板通过模板参数实现多态性,而不是通过继承和虚函数
  3. 示例:
    1. 这个例子中,Wrapper 类模板通过模板参数 T 实现多态性,并且在编译期确定了调用的 print 函数

编译期多态优缺点

  1. 优点
    1. 性能:由于在编译期确定调用,消除了运行时开销
    2. 类型安全:编译器在编译期进行类型检查,确保模板参数满足接口要求
    3. 灵活性:可以使用任意类型作为模板参数,只要满足接口要求
  2. 缺点
    1. 代码膨胀:模板实例化可能导致代码膨胀(生成大量代码)
    2. 调试困难:模板错误信息可能复杂,调试困难
    3. 编译时间:大量使用模板可能增加编译时间

编译期多态 vs 运行期多态

  1. 编译期多态
    1. 通过模板实现
    2. 在编译期确定函数调用,提高了运行时效率
    3. 没有运行时开销
    4. 代码可读性和可维护性可能受到模板复杂性的影响
  2. 运行期多态
    1. 通过继承和虚函数实现
    2. 在运行期通过虚函数表确定函数调用
    3. 具有运行时开销(虚函数表查找)
    4. 提供更灵活的接口,易于扩展

42 理解typename的两种含义

概述

  1. C++模板编程中,typename 关键字有两种主要用途:
    1. 声明类型别名
    2. 指示依赖类型

用途一:声明类型别名

  1. typename 关键字可以用来声明类型别名,这是与 class 关键字在模板参数列表中同义的用法
  2. 在模板参数列表中,typenameclass 都可以用于声明模板类型参数
  3. 示例:
    1. 这个例子中,typenameclass 都用于声明模板类型参数 TU

用途二:指示依赖类型

  1. 当一个模板类使用另一个模板参数定义的类型时,编译器需要明确知道该类型是一个类型名而不是一个变量或其他实体
  2. typename 关键字在这种情况下用于指示依赖类型
  3. 示例:
    1. 这个例子中,T::InnerType 是一个依赖类型,因为它依赖于模板参数 T
    2. 在这种情况下,必须使用 typename 关键字来指示 InnerType 是一个类型名

依赖类型

  1. 依赖类型是在模板中依赖于模板参数的类型
  2. C++编译器在第一次看到模板定义时,不知道模板参数是什么,因此需要显式地告诉编译器,某个名称是一个类型而不是其他东西
  3. typename 关键字在这种情况下非常重要

非限定名

  1. 在非模板代码中,编译器可以很容易地识别类型名和其他名字
  2. 但是在模板代码中,编译器在实例化模板之前无法确定某些名字的确切含义
    1. 例如:如果没有 typename 关键字,编译器会认为 T::InnerType 是一个变量或静态成员,而不是一个类型

依赖类型的使用

43 学会访问模板化基类中的名字

概述

  1. C++模板编程中,访问模板化基类中的名字可能会遇到一些复杂性,特别是在多继承和依赖名称的情况下

问题描述

  1. 当一个模板类继承自另一个模板类时,基类中的名字(如类型名或成员函数)在派生类中可能无法直接访问
  2. 这是因为这些名字依赖于模板参数,而编译器在实例化模板之前无法确定这些名字的具体含义

解决方案

  1. 使用 this 指针
    1. 如果你需要在派生类中访问基类的成员,可以通过 this 指针来显式地引用这些成员

  1. 使用 using 声明

  1. 显式作用域解析

示例代码

  1. 问模板化基类中的类型
    1. 这个例子中,Derived 类通过 typename Base<T>::value_type 来访问基类中的类型名

44 将与参数无关的代码移出模板

概述

  1. C++模板编程中,将与模板参数无关的代码移出模板,可以减少代码重复,提高编译速度,并简化代码维护

问题描述

  1. 模板的实例化会在每个不同的模板参数集上生成不同的代码
    1. 这可能导致代码膨胀,增加编译时间和可执行文件的大小
  2. 如果模板中有大量与模板参数无关的代码,这些代码会被重复实例化,浪费资源

解决方案

  1. 为了避免这种情况,可以将与模板参数无关的代码移出模板,使其成为独立的函数或类
  2. 这可以减少重复代码,提高编译效率,并使代码更易于维护

具体策略

  1. 提取独立的类或函数:
    1. 将与模板参数无关的代码提取到独立的类或函数中
  2. 使用继承或组合:
    1. 模板类可以继承或包含这些独立的类,从而复用代码
  3. 在模板中调用独立的代码:
    1. 在模板类中调用这些独立的类或函数,从而减少重复代码

优点

  1. 减少代码重复:
    1. 通过提取与模板参数无关的代码,可以减少重复代码,提高代码的复用性
  2. 提高编译速度:
    1. 减少模板实例化的数量,可以提高编译速度,减少编译时间
  3. 简化维护:
    1. 减少重复代码,使代码更简洁,更易于理解和维护

示例代码

  1. 不优化的模板
    1. 这个例子中,print 函数包含了一部分与模板参数无关的代码(“Common code”)
    2. 如果我们实例化多个不同类型的 MyClass,这些与模板参数无关的代码会被重复生成

  1. 优化后的模板
    1. 这个优化后的例子中,我们将与模板参数无关的代码提取到一个独立的 CommonCode 类中,并在模板类中继承它
    2. 这样,CommonCode 类的代码只会生成一次,而不会在每个模板实例化中重复生成

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享