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

23 优先选择非成员非友元函数,而不是成员函数

概述

  1. 优先考虑使用非成员非友元函数,而不是成员函数
  2. 这种做法有助于保持类的接口简洁,提高代码的可读性和可维护性

为什么优先选择非成员非友元函数

  1. 增强封装性:
    1. 非成员函数不需要访问类的私有或受保护成员,因此可以减少类的内部实现细节暴露
  2. 提高代码重用性:
    1. 非成员函数可以作用于多个类,增强了代码的通用性和重用性
  3. 简化类定义:
    1. 将不需要访问私有成员的功能从类定义中移出,可以使类的定义更简洁、清晰
  4. 支持对称操作:
    1. 对于某些操作,如对称的二元运算符,使用非成员函数可以使代码更直观

示例代码

  1. 使用成员函数

  1. 使用非成员非友元函数

24 当类型转换应该应用于所有参数时,声明非成员函数

概述

  1. 在设计函数时,如果希望类型转换能够应用于函数的所有参数,应将该函数声明为非成员函数
  2. 这是因为成员函数的第一个参数是隐式的 this 指针,而类型转换不适用于 this 指针

为什么声明非成员函数

  1. 类型转换的应用:
    1. 非成员函数对所有参数都显式声明,因此编译器可以对所有参数执行类型转换
    2. 而对于成员函数,第一个参数是隐式的 this 指针,类型转换不适用
  2. 对称性和一致性:
    1. 对于对称操作(如算术运算符),使用非成员函数可以确保所有参数被一致地处理
  3. 灵活性:
    1. 非成员函数可以定义在类的外部,从而增加了代码的灵活性和可重用性

示例代码

  1. 成员函数的示例

  1. 非成员函数的示例

25 考虑支持不抛出异常的swap操作

概述

  1. 在设计类时,考虑提供一个不抛出异常的swap操作
  2. 这对于提高代码的异常安全性和性能非常重要,特别是在实现复制并交换(copy-and-swap)习惯用法时

为什么需要不抛出异常的swap

  1. 异常安全性:
    1. 如果swap操作不抛出异常,可以在实现异常安全的赋值运算符时使用copy-and-swap习惯用法,从而保证强异常安全性
  2. 性能:
    1. swap操作通常是非常高效的,因为它只涉及指针或引用的交换,而不是实际数据的复制
    2. 这使得它在性能敏感的场景中非常有用

如何实现不抛出异常的swap

  1. 自定义swap函数:
    1. 在类中提供一个自定义的swap函数,并确保它不抛出异常
  2. std::swap特化:
    1. std::swap提供特化版本,以便标准库可以使用这个高效的swap操作

示例代码

  1. 自定义swap函数

  1. 解析:
  2. 在类中定义一个名为swap的成员函数
    1. 这个函数交换两个对象的内部状态(如指针和大小),并确保它不抛出异常(通过在函数声明中使用noexcept关键字)

  1. 使用copy-and-swap习惯用法:
    1. 在赋值运算符中,使用copy-and-swap习惯用法
    2. 通过值传递参数并在函数体内进行交换,可以确保赋值操作的异常安全性

  1. std::swap提供特化版本:
    1. std命名空间中为MyClass提供特化的swap版本,使得标准库在需要交换MyClass对象时可以使用高效的自定义swap函数

26 尽可能延迟变量定义

概述

  1. 在编写代码时,尽量延迟变量的定义,直到你确切知道需要使用它们为止
  2. 这种做法有助于提高代码的效率和可读性,减少错误的可能性,并且确保变量在尽可能小的作用域内有效

为什么要延迟变量定义

  1. 提高效率:
    1. 通过延迟变量定义,避免了不必要的变量初始化和内存分配,从而提高程序的效率
  2. 减少错误:
    1. 延迟变量定义可以减少未初始化变量的使用以及无意中覆盖变量的风险
  3. 增强可读性:
    1. 将变量的定义和使用放在一起,可以使代码更易于阅读和理解
  4. 缩小作用域:
    1. 延迟变量定义可以确保变量在尽可能小的作用域内有效,从而减少变量的生命周期,防止意外的变量覆盖和未定义行为

示例代码

  1. 早期定义变量(不推荐)

  1. 延迟变量定义(推荐)

27 最小化类型转换

概述

  1. 在编写代码时尽量减少类型转换的使用
  2. 类型转换是一种强制改变数据类型的操作,它可能会导致代码变得复杂和容易出错
  3. 通过最小化类型转换,可以提高代码的可读性、可维护性和安全性

为什么要最小化类型转换

  1. 提高代码的可读性:
    1. 类型转换会使代码变得复杂和难以理解,减少类型转换可以使代码更加直观
  2. 减少错误:
    1. 类型转换可能会引入难以发现的错误,特别是在类型转换失败或导致数据损失的情况下
  3. 增强类型安全:
    1. 类型转换会绕过类型检查,增加类型不匹配的风险。通过减少类型转换,可以增强类型安全性
  4. 提高可维护性:
    1. 类型转换会增加代码的复杂性,使得代码更难以维护和修改

示例代码

  1. 不推荐的做法:频繁使用类型转换

  1. 推荐的做法:避免类型转换
    1. 这个例子中,通过使用多态和虚函数,可以避免类型转换

28 避免返回指向对象内部的句柄

概述

  1. 免返回指向对象内部实现的句柄或指针
  2. 这样做的目的是为了保护对象的封装性,防止外部代码直接修改对象的内部状态,从而确保对象的一致性和完整性

为什么要避免返回指向对象内部的句柄

  1. 保护封装性:
    1. 封装是面向对象编程的基本原则之一,通过隐藏对象的内部实现细节,可以防止外部代码直接访问和修改对象的内部状态
  2. 确保数据一致性:
    1. 如果外部代码可以直接修改对象的内部数据,可能会导致数据的不一致和错误
    2. 通过避免返回内部句柄,可以确保所有对数据的修改都通过类的公共接口进行,从而保持数据的一致性
  3. 提高可维护性:
    1. 隐藏内部实现细节可以使类的实现更加灵活
    2. 内部实现可以随时修改,而不影响外部代码,从而提高代码的可维护性
  4. 增强安全性:
    1. 避免返回内部句柄可以防止外部代码进行不安全的操作,从而提高代码的安全性

示例代码

  1. 错误的做法:返回内部指针

  1. 正确的做法:返回常量引用或副本

29 努力编写异常安全的代码

概述

  1. 写异常安全的代码是为了确保程序在发生异常时仍能保持一致性,并且没有资源泄漏
  2. 异常安全性分为三种保证级别:
    1. 基本保证
    2. 强保证
    3. 不抛出异常保证

基本保证

  1. 即使发生异常,程序状态仍然保持一致,没有资源泄漏
    1. 可能会有部分操作失败,但数据结构和资源保持有效
    2. 例如,容器在插入元素时可能会抛出异常,但容器仍然是有效的,已经存在的元素不会丢失

强保证

  1. 即使发生异常,程序状态仍然保持一致,没有资源泄漏,并且所有操作要么完全成功,要么完全失败(即程序状态恢复到操作前的状态)
    1. 这种保证通常通过使用事务性操作来实现
    2. 例如,插入元素时,如果操作失败,容器会恢复到插入前的状态

不抛出异常保证

  1. 操作承诺不会抛出任何异常
    1. 这种保证通常用于析构函数和swap操作等
    2. 例如,std::vector::swap 保证不抛出异常

如何实现异常安全的代码

  1. 使用RAII(资源获取即初始化)
    1. 使用RAII管理资源,通过构造函数获取资源,通过析构函数释放资源
      1. 这样可以确保资源在异常发生时也能正确释放
      2. 例如,使用智能指针(如std::unique_ptrstd::shared_ptr)来管理动态内存
  2. 使用智能指针
    1. 智能指针自动管理动态内存的生命周期,确保在异常发生时内存不会泄漏
    2. 例如,使用std::unique_ptr管理动态分配的对象
  3. 使用标准库容器
    1. 使用标准库容器(如std::vectorstd::map等)来管理动态数组和集合,避免手动管理内存和资源
  4. 使用try-catch
    1. 在适当的位置使用try-catch块捕获异常,并进行适当的处理或清理工作
    2. 确保在捕获异常后程序能够恢复到一致状态
  5. 提供异常安全的接口
    1. 设计类和函数时,提供异常安全的接口,确保在发生异常时对象状态保持一致

示例代码

  1. 基本保证

  1. 强保证

  1. 不抛出异常保证

30 理解内联函数的方方面面

概述

  1. 内联函数(inline functions)是一种在函数调用时通过在调用点插入函数代码来替代函数调用,从而避免函数调用开销的方法
  2. C++中,内联函数可以通过在函数定义前加上 inline 关键字来实现
  3. 内联函数具有许多优点,但也需要谨慎使用,以避免代码膨胀和其他潜在问题

为什么使用内联函数

  1. 减少函数调用开销:
    1. 内联函数通过在调用点直接插入函数代码,消除了函数调用的开销,如参数传递和返回地址保存
  2. 提高性能:
    1. 在某些情况下,内联函数可以提高性能,特别是对于短小、频繁调用的函数
  3. 增加编译器优化机会:
    1. 内联函数使得编译器有更多的机会进行优化,如常量传播和循环展开

优点

  1. 消除函数调用开销:
    1. 内联函数避免了函数调用的额外开销,特别适用于短小且频繁调用的函数
  2. 编译器优化:
    1. 内联函数提供了更多的优化机会,如常量传播、代码移动和循环展开

缺点

  1. 代码膨胀:
    1. 内联函数会增加生成代码的大小,特别是当函数在多个调用点被内联时
    2. 这可能导致代码膨胀,影响程序的内存占用和缓存性能
  2. 调试困难:
    1. 内联函数使得调试更加复杂,因为内联代码不会显示在栈回溯中,难以跟踪函数调用
  3. 编译时间增加:
    1. 内联函数可能增加编译时间,因为编译器需要处理更多的代码展开

使用

  1. 定义内联函数

  1. 类中的内联函数

示例代码

31 最小化文件之间的编译依赖

概述

  1. 建议尽量减少头文件之间的编译依赖关系
  2. 减少编译依赖可以显著加快编译速度,减少代码耦合,提高代码的可维护性和可重用性

为什么要最小化编译依赖

  1. 加快编译速度:
    1. 减少文件间的依赖关系,可以避免不必要的重新编译,提高整体编译效率
  2. 减少代码耦合:
    1. 减少依赖关系,可以降低代码模块之间的耦合,使得代码更易于维护和重用
  3. 提高代码的可维护性:
    1. 当修改一个头文件时,减少依赖关系可以减少受影响的文件数量,降低维护成本
  4. 避免循环依赖:
    1. 过多的依赖关系容易导致循环依赖,增加调试和解决问题的难度

如何最小化编译依赖

  1. 前向声明(Forward Declaration):
    1. 在头文件中使用前向声明,而不是包含其他头文件
    2. 前向声明是在使用类之前声明类名,而不是包含类的完整定义
    3. 这可以减少头文件之间的依赖

  1. 使用指针或引用:
    1. 在类成员中使用指针或引用,而不是包含其他类的实例
    2. 这样可以避免在头文件中包含其他类的定义

  1. 使用抽象类或接口:

    1. 通过抽象类或接口定义依赖关系,而不是具体实现
  2. 将实现细节放在源文件中:

    1. 尽量将类的实现细节放在源文件中,而不是头文件中
    2. 这样可以减少头文件的修改频率,减少编译依赖

示例代码

  1. 错误的做法:直接包含头文件

  1. 正确的做法:使用前向声明和指针
    1. 在类成员中使用指针或引用,而不是包含其他类的实例
    2. 这样可以避免在头文件中包含其他类的定义

32 确保公共继承符合“is-a”关系

概述

  1. 在使用公共继承时,必须确保子类是一个真正的基类的特例,即子类和基类之间存在“is-a”(是一个)关系
  2. 公共继承表示子类不仅继承了基类的接口,还继承了基类的行为,因此子类应该能够完全替代基类的对象

为什么要确保公共继承符合“is-a”关系

  1. Liskov替换原则(Liskov Substitution Principle):
    1. 子类对象应该能够替代基类对象而不改变程序的正确性
    2. 违反这一原则可能导致程序的不稳定和错误
  2. 代码可读性和可维护性:
    1. 确保公共继承符合“is-a”关系可以使代码更易于理解和维护,避免不必要的复杂性
  3. 多态性:
    1. 在面向对象编程中,多态性的前提是子类能够替代基类对象进行操作
    2. 如果不符合“is-a”关系,多态性就不能正确实现

如何确保公共继承符合“is-a”关系

  1. 设计时考虑“is-a”关系:
    1. 在设计类继承层次时,始终考虑子类是否真正是基类的一种特例,是否满足“is-a”关系
  2. 使用接口和抽象基类:
    1. 将公共行为抽象为接口或抽象基类,使得子类必须实现这些行为,确保接口的一致性
  3. 测试替换性:
    1. 通过编写测试代码,确保子类对象能够在所有基类对象可以使用的地方正确工作

示例代码

  1. 正确的公共继承示例

  1. 错误的公共继承示例

33 避免隐藏继承的名称

概述

  1. C++中,派生类(子类)可以继承基类的成员函数和数据成员
  2. 但是,如果子类定义了与基类同名的成员函数或数据成员,基类的同名成员将会被隐藏
    1. 隐藏继承的名称可能会导致代码的可读性和可维护性问题,以及意想不到的行为
    2. 因此,避免隐藏继承的名称是编写健壮和可维护代码的重要原则

为什么要避免隐藏继承的名称

  1. 代码可读性:
    1. 隐藏继承的名称会使代码变得难以理解,尤其是当同名函数在子类和基类中有不同的行为时
  2. 意外行为:
    1. 隐藏继承的名称可能会导致意外行为,特别是当开发者期望调用基类的函数而实际上调用了子类的函数时
  3. 维护困难:
    1. 在大型项目中,隐藏继承的名称会增加代码的复杂性和维护难度

示例代码

  1. 隐藏继承名称的问题

避免隐藏继承名称的方法

  1. 使用using声明
    1. 在子类中使用using声明来引入基类的同名成员,从而避免隐藏

  1. 选择不同的名称
    1. 为子类中的新函数选择不同的名称,以避免与基类中的函数同名

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

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2024-07-02
Everything will be better.

发表评论

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