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

12 复制对象的所有部分

概述

  1. C++中,实现拷贝构造函数和赋值运算符时,确保复制对象的所有成员变量是至关重要的
  2. 忽略某些成员变量可能会导致不完整或错误的对象状态,进而引发难以追踪的错误

为什么要复制对象的所有部分

  1. 保持对象的一致性:
    1. 所有成员变量都应该在拷贝时保持一致,以确保新对象的状态与原对象相同
  2. 防止资源泄漏:
    1. 如果对象管理动态资源(如内存、文件句柄等),在拷贝时必须正确处理这些资源以防止泄漏
  3. 确保正确行为:
    1. 忽略某些成员变量可能导致新对象行为异常或不完整

实现

  1. 在实现拷贝构造函数和赋值运算符时,确保所有成员变量都被正确拷贝

示例代码

  1. 一个包含动态内存分配和其他成员变量的类的完整示例,展示如何正确实现拷贝构造函数和赋值运算符:

13 使用对象管理资源

概述

  1. C++编程中,资源管理(如动态内存、文件句柄、网络连接等)是一个重要而复杂的任务
    1. 如果不正确管理资源,容易导致资源泄漏或未定义行为
  2. 使用对象来管理资源,通过RAIIResource Acquisition Is Initialization)原则确保资源在对象的生命周期内被正确管理

RAII(资源获取即初始化)

  1. RAII是一种编程惯例,即资源的获取与对象的初始化绑定在一起,资源的释放与对象的销毁绑定在一起
  2. 通过RAII,可以确保资源在对象的构造函数中获取,在析构函数中释放,从而避免资源泄漏

示例代码

  1. 在构造函数中分配动态内存,并将传入的字符串复制到内部数据成员中

  1. 在拷贝构造函数中分配新的内存,并将原对象的数据复制过来

  1. 在移动构造函数中,将源对象的数据指针移动到当前对象,并将源对象的数据指针置为空

  1. 拷贝赋值运算符中,先检查自我赋值,然后分配新内存并复制数据,最后释放旧内存

  1. 在移动赋值运算符中,先检查自我赋值,然后释放旧内存,将源对象的数据指针移动到当前对象,并将源对象的数据指针置为空

  1. 在析构函数中释放动态分配的内存

14 在资源管理类中仔细考虑复制行为

概述

  1. 在设计资源管理类时,需要特别注意复制行为(拷贝构造和赋值操作)的实现方式
  2. 正确处理这些操作可以避免资源泄漏、双重释放等问题,确保对象的行为符合预期

资源管理类的复制问题

  1. 在资源管理类中,如果不正确处理复制操作,可能会导致以下问题:
  2. 资源泄漏:
    1. 没有正确释放旧资源或处理新资源时可能会导致资源泄漏
  3. 双重释放:
    1. 多个对象共享同一个资源,但在销毁时尝试释放同一个资源,导致未定义行为
  4. 浅拷贝问题:
    1. 复制对象时只是复制了指针,而不是指针指向的资源,这会导致两个对象共享同一个资源

解决方案

  1. 禁用复制操作
    1. 如果不需要对象的复制操作,可以通过删除拷贝构造函数和赋值运算符来禁用它们

  1. 实现深拷贝
    1. 如果需要复制对象,应确保每个对象都有自己的独立资源副本。实现深拷贝可以确保资源的独立性

  1. 使用智能指针
    1. 在现代C++中,使用智能指针(如std::unique_ptrstd::shared_ptr)可以简化资源管理,并自动处理复制和销毁操作

15 在资源管理类中提供对原始资源的访问

概述

  1. 在资源管理类中提供对底层资源(如指针、文件句柄、网络连接等)的访问方法
  2. 虽然RAIIResource Acquisition Is Initialization)可以自动管理资源的生命周期,但有时仍需要直接访问这些底层资源,以便与使用原始资源的API交互

为什么需要提供对原始资源的访问

  1. 与外部API交互:
    1. 某些库或API需要直接使用原始资源
    2. 例如,操作系统API通常需要原始指针或句柄
  2. 提高灵活性:
    1. 在某些情况下,直接访问底层资源可以提高代码的灵活性和效率

如何提供对原始资源的访问?

  1. 常量访问:
    1. 提供只读访问,不允许修改资源
  2. 非常量访问:
    1. 提供可修改访问,允许对资源进行修改

16 在对应的new和delete使用中使用相同的形式

概述

  1. 如果使用new分配单个对象,就应该使用delete释放单个对象
  2. 如果使用new[]分配数组,就应该使用delete[]释放数组
  3. 否则会导致未定义行为

为什么要保持一致性

  1. 内存管理一致性:
    1. C++的内存分配和释放机制要求使用匹配的形式
    2. 如果不匹配,会导致内存泄漏或程序崩溃
  2. 避免未定义行为:
    1. 不匹配的newdelete形式会导致未定义行为,可能会破坏程序的稳定性和安全性

示例代码

17 将通过new分配的对象存储在智能指针中,并且要使用独立语句

概述

  1. 在使用new操作符分配对象时,应将其存储在智能指针中,以避免内存泄漏
  2. 同时,建议使用独立的语句来进行分配和存储,以确保异常安全性

为什么使用智能指针

  1. 智能指针(如std::unique_ptrstd::shared_ptr)自动管理动态分配的对象的生命周期
  2. 确保在不再使用对象时自动释放内存,防止内存泄漏

为什么使用独立语句

  1. 在复杂的表达式中同时进行对象分配和智能指针初始化,可能会导致异常安全性问题
  2. 如果在分配对象之后但在智能指针初始化之前抛出异常,可能会导致内存泄漏
    1. 因此,建议使用独立语句来分配对象并将其存储在智能指针中

示例代码

注意

  1. 上面的写法中,最推荐的是:
    1. auto ptr2 = std::make_unique<MyClass>();
  2. 第二推荐的是:
    1. std::unique_ptr<MyClass> ptr1(new MyClass());
  3. 不推荐的写法是:
    1. 在分配对象之后但在智能指针初始化之前,如果发生异常(例如,在其他代码中抛出异常),会导致内存泄漏
      会发生异常的原因,是无法保证这两行中间会不会加可能抛出异常的代码
    2. 分开写会使代码变得冗长,不如使用 std::make_unique 简洁和安全

  1. 不推荐上面写法的原因:
    1. 单线程环境中,如果可以保证这两行是挨着的,中间不会有其他代码,那么不会有上述的异常导致的内存泄漏
    2. 多线程环境中,任然有一种可能性极地的情况:
      刚构造完对象,两行代码之间发生了线程切换,并且在其他线程在切换期间引发了影响全局状态的异常,就可能出现问题
  2. 额外的解析:
    1. 如果这里的异常发生后,throw这一行下面的代码是不会有机会执行的,控制流立即跳转到对应的异常处理程序(catch 块),并跳过 throw 语句之后的所有代码
    2. 如果异常发生后,整个程序还在运行,那么 new 分配的 ptr 这一块空间就泄漏了
    3. 多线程环境中,每个线程都有自己的异常处理上下文,因此线程之间的异常处理是独立的

18 使接口易于正确使用,难于误用

概述

  1. 良好的接口设计可以减少用户犯错误的机会,提高代码的可读性和可维护性

原则和策略

  1. 合理使用类型系统
    1. 使用合适的类型来表达函数参数的含义
    2. 例如,使用std::string表示字符串而不是char*,使用std::vector表示动态数组而不是C风格的数组
  2. 使用明确的命名
    1. 为函数和变量选择有意义且自解释的名称,避免使用模糊的缩写或不相关的名称
  3. 提供合理的默认值
    1. 为函数参数提供合理的默认值,避免用户必须提供不必要的参数
  4. 限制接口的复杂性
    1. 尽量保持接口的简洁性,避免用户必须记住太多的细节
  5. 防止错误的使用
    1. 使用类型系统和编译时检查来防止常见的错误
    2. 例如,使用enum来表示一组相关的常量,而不是使用int
  6. 使用RAII和智能指针
    1. 通过使用RAIIResource Acquisition Is Initialization)和智能指针来管理资源,减少用户手动管理资源的负担,避免资源泄漏

示例代码

19 将类设计视为类型设计

概述

  1. 设计类时应该将其视为创建一个新类型的过程

原则和策略

  1. 明确类的职责
    1. 确保每个类有明确的职责,遵循单一职责原则(Single Responsibility Principle
    2. 类应该只做一件事,并把它做好

  1. 封装实现细节

    1. 封装类的实现细节,通过公共接口与外部进行交互

    2. 避免暴露类的内部实现,使其可以在不影响外部代码的情况下进行修改

  1. 使用一致的接口
    1. 保持类的接口一致和直观
    2. 使用命名规范和一致的参数类型,使类的使用更加方便和易于理解

  1. 支持类型安全
    1. 确保类的操作是类型安全的,防止错误的使用
    2. 使用适当的类型和编译时检查来确保类型安全性

  1. 提供必要的操作
    1. 提供类的基本操作,如构造函数、析构函数、拷贝构造函数、赋值运算符等,使类的行为符合预期

  1. 考虑类的扩展性
    1. 设计类时要考虑未来的扩展性,确保类可以方便地添加新功能或修改现有功能,而不破坏现有代码

20 优先选择传递常量引用而不是传值

概述

  1. 在函数参数传递时,优先选择通过常量引用传递对象,而不是通过值传递
    1. 这样做可以提高程序的效率,特别是当传递大对象或复杂对象时

为什么优先选择传递常量引用

  1. 性能
    1. 传递大对象或复杂对象时,传值会导致对象的拷贝构造函数被调用,从而增加开销
    2. 而通过常量引用传递则避免了这种不必要的拷贝
  2. 避免拷贝
    1. 某些对象可能不允许复制(例如,禁止拷贝的对象),通过常量引用传递可以避免这种问题
  3. 一致性
    1. C++中,许多标准库函数都采用传递常量引用的方式,这是一种常见且推荐的做法

适用场景

  1. 大对象或复杂对象:
    1. 当对象的拷贝开销较大时,通过常量引用传递可以显著提高效率
  2. 不可复制对象:
    1. 当对象不允许复制时(例如,禁用拷贝构造函数),通过常量引用传递是唯一可行的方式
  3. 保持对象的状态:
    1. 通过常量引用传递可以确保函数内部不修改传递的对象,从而保持对象的状态一致性

示例代码

  1. 传值方式

  1. 通过常量引用传递

21 在必须返回对象时,不要试图返回引用

概述

  1. 在设计函数返回类型时,如果需要返回一个新创建的对象,应该返回对象本身而不是引用
  2. 返回引用可能会导致未定义行为和难以调试的错误

为什么不要返回引用

  1. 生命周期问题:
    1. 如果返回一个局部对象的引用,当函数返回后,该局部对象会被销毁,导致引用指向无效内存
  2. 未定义行为:
    1. 返回局部对象的引用会导致未定义行为,使用这样的引用可能会导致程序崩溃或意外行为
  3. 易于调试:
    1. 返回对象而不是引用可以避免难以调试的错误,因为对象的生命周期和所有权更加明确

适用场景

  1. 返回局部对象:
    1. 如果函数需要返回一个在函数内部创建的局部对象,应该返回对象本身而不是引用
  2. 返回临时对象:
    1. 如果函数返回一个临时对象,应该返回对象本身以确保对象的生命周期在函数返回后仍然有效

示例代码

  1. 错误做法

  1. 正确做法

22 将数据成员声明为私有

概述

  1. 在设计类时,应将数据成员声明为私有
  2. 这样可以保护数据,控制对数据的访问,并保持类的封装性和数据完整性

为什么要将数据成员声明为私有

  1. 封装性:
    1. 通过将数据成员声明为私有,类可以控制外部代码对数据的访问和修改
    2. 这是面向对象编程的基本原则之一,称为封装
  2. 数据完整性:
    1. 私有数据成员可以防止外部代码直接修改数据,确保数据的一致性和完整性
    2. 所有对数据的修改都必须通过类的公共接口进行
  3. 灵活性:
    1. 私有数据成员允许类的实现细节随时改变而不影响外部代码
    2. 只要公共接口保持不变,类的内部实现可以随意调整
  4. 可维护性:
    1. 将数据成员声明为私有可以提高代码的可维护性和可读性
    2. 其他开发人员在使用类时,不需要了解类的内部实现,只需了解类的公共接口

示例代码

  1. 错误做法

  1. 正确做法

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

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

发表评论

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