首页 > 编程语言 >Effective C++ 改善程序与设计的55个具体做法笔记与心得 4

Effective C++ 改善程序与设计的55个具体做法笔记与心得 4

时间:2024-06-22 12:58:12浏览次数:14  
标签:std 函数 Effective 55 成员 C++ 对象 swap type

四. 设计与声明

18. 让接口容易被正确使用,不易被误用

请记住

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任。
  • trl::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。

解释
‌‌‌‌  设计优秀的接口确实要注重以下几个方面:

  1. 易于正确使用:一个良好设计的接口应该使用户能够更容易地使用它。这需要避免在接口设计中的歧义和不一致,并尽量与已有的、用户熟悉的模式保持一致。例如,对一致性和对内置类型行为的兼容都属于这种情况。

  2. 不容易被误用:我们也应确保接口能够防止用户误用。创建新的类型(以区分不同的概念或值),限制类型上的操作,约束对象的值,或者管理客户端的资源,都是有效的防止误用手段。

‌‌‌‌  std::shared_ptr 的删除器就是一个很好的例子。这个特性让我们能够自定义对象被删除时的行为。例如,当 std::shared_ptr 管理的资源是一个在动态链接库(DLL)中分配的对象,或者是一个需要在释放之前执行特定操作(例如解锁)的资源时,删除器就非常有用了。

‌‌‌‌  以下是一个 std::shared_ptr 如何使用自定义删除器的例子:

// 假设 lock 是一个互斥锁对象
std::shared_ptr<std::mutex> lock(new std::mutex, [](std::mutex* m){
    m->unlock(); // 解锁
    delete m;
});

‌‌‌‌  这里,我们创建了一个 std::shared_ptr 来管理一个 std::mutex 对象,并提供了一个自定义的删除器。当 std::shared_ptr 需要释放它管理的对象时,它就会先调用 unlock 方法,然后再删除对象。这样,我们就不需要关心何时解锁或删除互斥锁对象, std::shared_ptr 会帮我们自动完成。

19. 设计class犹如设计type

请记住

  • 新type的对象应该如何被创建和销毁?
  • 对象的初始化和对象的赋值该有什么样的差别?
  • 新type的对象如果被passed by value(以值传递),意味着什么?
  • 什么是新type的“合法值”?
  • 你的新type需要配合某个继承图系吗?
  • 你的新type需要什么样的转换?
  • 什么样的操作符和函数对此新type而言是合理的?
  • 什么样的标准函数应该驳回?
  • 该取用新type的成员?
  • 什么是新type的“未声明接口”?
  • 你的新type有多么一般化?你真的需要一个新type么?

解释

  • 新type的对象应该如何被创建和销毁?:这一问题涉及到类的构造函数和析构函数的设计。构造函数决定如何初始化一个对象,析构函数决定如何清理它。

  • 对象的初始化和对象的赋值该有什么样的差别?: 初始化涉及创建新对象时赋予其初始值,而赋值则是将已存在对象的值改变为新的值。这两者的处理可能会有所不同,因此我们通常需要对这两种操作进行清晰的定义。

  • 新type的对象如果被passed by value(以值传递),意味着什么?:如果类对象被以值传递,那么会创建该对象的一个复制品。为此,我们需要定义复制构造器以指定如何进行复制。

  • 什么是新type的“合法值”?:对于某个特定的类,其对象的“合法值”可能会受到某些约束。“合法值”的概念涉及到类的数据验证和封装。

  • 你的新type需要配合某个继承图系吗?:如果新的类型是某个已有类型的特化,或者需要被其他类型进行扩展,那么你需要考虑使用继承。

  • 你的新type需要什么样的转换?:在某些情况下,你可能需要为类定义转换运算符,例如转换为其他类类型或基本数据类型。

  • 什么样的操作符和函数对此新type而言是合理的?:你需要定义对象可以进行哪些操作,这通常通过重载运算符和定义成员函数来实现。

  • 什么样的标准函数应该驳回?:有些情况下,你可能想禁止某些操作,如禁止复制或者赋值等,可以通过将这些函数设为私有并不提供实现来达到这个目的。

  • 该取用新type的成员?:注意保持类的封装性,尽可能通过公有成员函数获取和设定私有成员变量,而不是将成员变量设置为公有。

  • 什么是新type的“未声明接口”?:这个可能指的是那些未直接列出但通过类的公有接口可以的操作。这种操作需要被考虑在内和进行测试。

  • 你的新type有多么一般化?你真的需要一个新type么?:在你决定创建新的类时,需要考虑它是否过于特定或者过于一般化,以及是否真的需要一个新的类。如果对于问题的解决并没有太大帮助,可能需要重新考虑设计。

20. 宁以pass-by-reference-to-const替换pass-by-value

请记住

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高级,并可避免切割问题。
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。

解释

  • 尽量以pass-by-reference-to-const替换pass-by-value: 对象在传递过程中,pass-by-value需要对对象进行复制操作,产生新的对象。这通常会发生在函数参数传递和返回值中。但复制大型对象可能会非常耗时,也可能引发性能问题。为了避免这些问题,一种常见的解决方法是使用"传递常量引用",即pass-by-reference-to-const,这样就可以避免复制操作。同时使用const可以避免在函数内部修改原对象。

  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象: 对于内置类型(如int、char等),以及STL的迭代器和函数对象,他们通常在内存占用和复制成本上非常小,因此使用pass-by-value通常会更高效。此外,这些类型通常设计为值语义,使用pass-by-value可以更符合其设计原则。

21. 必须返回对象时,别妄想返回其reference

请记住

‌‌‌‌  绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

解释

  • 绝不要返回pointer或reference指向一个local stack对象:这是因为当函数执行完毕后,它的栈内存会被销毁,那些局部变量也就不复存在了。如果你返回一个指向局部变量的指针或引用,那么该指针或引用就会变成悬挂指针或者悬挂引用,这种无效的引用可能会导致程序错误。

  • 或返回reference指向一个heap-allocated对象:这主要是因为在返回引用到堆上分配的对象时,对象的生命周期控制可能变得复杂。如果在函数中分配了堆内存,但是没有正确返回该内存的指针,那么调用者可能根本不知道应该释放这个内存,这就导致了内存泄漏。

  • 或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象:如果你返回一个指向局部静态变量的指针或引用,而在不同的上下文中需要多个这样的对象,那么这些上下文会共享该对象,这可能会导致出现意料之外的副作用。

总的来说,编程时很重要的一点就是管理好对象的生命周期,不正确的内存管理,如上述的几种情况,可能会引发很多问题。因此在编程时,我们要尽量避免这些错误的用法。

22. 将成员变量声明为private

请记住

  • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  • protected并不比public更具封装性。

解释
‌‌‌‌  在面向对象编程中,封装是非常重要的特性之一。

  • 将成员变量声明为private:这样做能保护类的内部状态,防止客户端代码直接修改它。我们只能通过定义的public方法来访问和修改,这样可以确保数据的安全性和一致性。

  • 可细微划分访问控制:private限定符可以使类的成员只能被该类的方法访问,这样我们可以更精细地控制谁可以访问和修改类的状态。

  • 约束条件获得保证:通过private属性和公开的setter方法,我们可以在修改数据前执行检查,保证数据满足一定的约束条件。

  • 提供class作者以充分的实现弹性:因为客户端代码不能直接访问私有成员,所以我们在未来需要修改类的内部实现时会更加灵活,不需要担心会影响到已有的客户端代码。

  • protected并不比public更具封装性:protected成员可以被自身和任何子类访问,相比private,其访问权限更宽松,所以有时可能不如private符合封装性的理念。

所以,将数据成员设置为private并通过public方法进行访问和修改,是实现良好封装的常用手段。

23. 宁以non-member、non-friend替换member函数

请记住

‌‌‌‌  宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。

解释

  • 增加封装性:在一些情况下,使用non-member non-friend函数(非成员非友元函数)可以增加类的封装性。这是因为non-member non-friend函数无法访问类的私有和受保护成员,所以对类的内部结构知之甚少。这就使得类的实现可以在不破坏这些函数正确性的情况下自由改变。

  • 提高包裹弹性:如果我们知道函数不需要访问对象的私有或受保护成员,那么就没有必要将它作为类的成员函数,这就提供了更多的弹性。我们可以在不改变类定义的情况下添加更多的函数,或者将这些函数放入不同的命名空间中。

  • 提升机能扩充性:non-member non-friend函数可以对多个对象执行操作,即使这些对象来自不同的类。相比之下,成员函数只能对它所属的对象执行操作。所以使用非成员非友元函数更加灵活,能更好地扩展功能。

24. 若所有参数皆需类型转换,请为此采用non-member函数

请记住
‌‌‌‌  如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

解释
‌‌‌‌  一个成员函数的隐式this参数只能用来转换它所属的对象,而不能用来转换传给该函数的其他参数。

‌‌‌‌  但是,非成员函数没有这样的限制,它们可以自由地转换传递给它们的所有参数。因此,如果一个操作需要对所有参数进行类型转换(包括那个由this指针隐含的参数),那么这个操作通常应该由非成员函数来完成。

‌‌‌‌  请注意,根据C++的运算符重载规则,有两个参数的运算符(例如+或-)应该作为非成员函数来实现,以便能处理左操作数进行的类型转换。然而,有些运算符(例如=或+=)则常常作为成员函数,因为它们通常需要改变它们的左操作数,即this对象。这是由于它们通常需要直接访问对象的内部状态,而这正是成员函数所提供的。

25. 考虑写一个不抛异常的swap函数

请记住

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

解释

  • 提供一个swap成员函数:这样可以确保swap操作针对你的类型最为高效。确保这个函数不会抛出异常,这样可以使之在异常敏感的上下文环境中更安全。

  • 提供一个non-member swap:非成员swap函数往往更易用,因为它们可以被引入到不需要访问类内部数据的函数或者范畴中。这个非成员函数应该简单地调用上面定义的成员swap函数。

  • 特化std::swap:如果你的类型不适用于标准库提供的std::swap,你可以为你的类型提供一个std::swap的全特化版本,这样可以使标准算法和容器能够利用你的高效swap实现。

  • 不带任何“命名空间资格修饰”调用swap:这样可以确保在swap操作符重载的上下文中你总是调用了正确的swap版本。

  • 不要在std内添加新东西:这是一个关于C++编程习惯的通常建议。尽管为std::swap提供全特化版本是可以接受的,但在std命名空间内添加全新的内容是不被允许的,因为这可能引发未定义的行为。

标签:std,函数,Effective,55,成员,C++,对象,swap,type
From: https://blog.csdn.net/qq_43504141/article/details/139844397

相关文章

  • C/C++ 堆栈stack算法详解及源码
    堆栈(stack)是一种常见的数据结构,具有"先进后出"(LastInFirstOut,LIFO)的特性。堆栈算法允许在堆栈顶部进行元素的插入和删除操作。堆栈的操作包括:入栈(Push):将元素添加到堆栈的顶部。出栈(Pop):从堆栈的顶部移除元素。取栈顶元素(Top):获取堆栈顶部的元素,但不对其进行删除操作。......
  • C/C++ stack实现深度优先搜索DFS算法详解及源码
    深度优先搜索(DepthFirstSearch,DFS)是一种图遍历算法,它从一个节点开始,通过访问其相邻节点的方式,依次深入到图中的更深层次。Stack(栈)是一种先进后出(LastInFirstOut,LIFO)的数据结构,它非常适合实现DFS算法。首先,我们来解释一下Stack实现DFS算法的原理。DFS算法的核心思想是......
  • 【C++ | 重载运算符】一文弄懂C++运算符重载,怎样声明、定义运算符,重载为友元函数
    ......
  • C++PrimerPlus:第十三章类和继承:抽象基类
    :第十三章类和继承:抽象基类提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加例如::第十三章类和继承:抽象基类提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录:第十三章类和继承:抽象基类前言一、抽象基类总结前言提示:这......
  • [题解]AT_abc255_d [ABC255D] ±1 Operation 2
    思路因为\(1\leqn,q\leq2\times10^5\),所以对于每一次查询的时间复杂度一定要达到\(\Theta(\logn)\),甚至于\(\Theta(1)\)。一个最简单的想法,我们先统计出整个序列\(a\)的和\(sum\),然后答案是\(|sum-x\timesn|\)。很显然,这个想法是错误的,因为对于\(a\)中只有......
  • 从0开始C++(五):友元函数&运算符重载
    友元函数介绍C++中的友元函数是一种特殊的函数,它可以访问和操作类的私有成员和保护成员。友元函数可以在类的内部或外部声明和定义,但在其声明和定义中需要使用关键字friend来标识。友元函数可以是全局函数,也可以是其他类的成员函数。下面是友元函数的一些重要特点和用法:......
  • c++ 多重包含/定义 || 链接性 || 生命周期
     作用域&&生命周期C++中的作用域(scope)指的是变量、函数或其他标识符的可见和可访问的范围。生命周期(Lifetime)指的是变量或对象存在的时间段。它开始于变量或对象的创建(定义)时刻,结束于其被销毁的时刻。作用域:通过其声明的位置来确定。全局作用域:定义在(类/函数)外部......
  • c++ virtual || virtual =0
    虚函数&&纯虚抽象类:包含纯虚函数的类称为抽象类,继承层次结构的较上层。作用:将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。继承:子类继承基类的成员及成员函数,不可以删除,可以(修改)通过虚函数重写......
  • 【C++】list的使用方法和模拟实现
    ❤️欢迎来到我的博客❤️前言list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素list与forward_list非常相似:最......
  • 2024年华为OD机试真题-分披萨-(C++/Java/python)-OD统一考试(C卷D卷)
    题目描述"吃货"和"馋嘴"两人到披萨店点了一份铁盘(圆形)披萨,并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。但是粗心的服务员将披萨切成了每块大小都完全不同奇数块,且肉眼能分辨出大小。由于两人都想吃到最多的披萨,他们商量了一个他们认为公平的分法:从"吃货"开始,轮流......