【Effective C++】第四章 设计与声明(上)

2024-08-22 13:08

本文主要是介绍【Effective C++】第四章 设计与声明(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第四章 设计与声明

条款18:让接口容易被正确使用,不易被误用

  • 考虑客户可能做出什么样的错误。

  • 除非有好理由,否则应该尽量令你的types的行为与内置types一致。

  • tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“cross-DLL problem”。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。

请记住:

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。

  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

  • tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥量等等。

条款19:设计class犹如设计type

如何设计高效的classes呢?首先你必须了解面对的问题。几乎每一个class都要求面对以下提问,而你的回答往往导致你的设计规范:

  • 新type的对象应该如何被创建和销毁?构造函数和析构函数以及内存分配函数和释放函数的设计。

  • 对象的初始化和对象的赋值该有什么样的差别? 决定于构造函数和赋值操作符的行为,以及其间的差异。

  • 新type的对象如果被passed by value(以值传递),意味着什么? copy构造函数用来定义一个type的passed by value该如何实现。

  • 什么是新type 的“合法值”? 对class的变量而言,通常只有某些数值集是有效的。那些数值集决定了你的class必须维护的约束条件,也就决定了你的成员函数必须进行错误检查工作。它也影响函数抛出异常、以及函数异常明细列。

  • 你的新type需要配合某个继承图系(inheritancegraph)吗? 如果你的class是从别的基类继承下来的,那么会受到基类的束缚。反之,如果你允许其他classes继承你的class,会影响你声明的函数尤其是析构函数是否是virtual。

  • 你的新type需要声明样的转换?显示or隐式。

  • 什么样的操作符和函数对此新type而言是合理的? 这个问题答案决定你将为你的class声明哪些函数。其中某些该是member函数,某些则否。

  • 什么样的标准函数应该驳回? 声明为private者。

  • 谁该取用新type的成员?

  • 什么是新type的“未声明接口”(undeclared interface)

  • 你的新type有多么一般化? 并非定义一个新type,而是定义一个types家族,应该定义为class template。

  • 你真的需要一个新type吗? 如果只是定义新的derived class以便为既有的class添加机能,那么说不定单纯定义一个或多个non-member函数或templates,更能够达到目标。

请记住:

  • Class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。

条款20:宁以pass-by-reference-to-const替代pass-by-value

1、缺省情况下C++以by value方式传递对象至函数。除非你另外指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是返回值的一个副本。这些副本由对象的拷贝构造函数产生,这可能使得pass by value成为昂贵的操作。

  #include<iostream>  #include<string>  using namespace std;class Person {public:    Person() { cout << "Person()" << endl; }    Person(const Person& p) :name(p.name), address(p.address) {        cout << "Person copy construct" << endl;    }    ~Person() { cout << "~Person()" << endl; }private:    string name;    string address;};class Student :public Person {public:    Student() { cout << "Student()" << endl; }    Student(const Student& s):Person(s), schoolName(s.schoolName), schoolAddress(s.schoolAddress) {        cout << "Student copy construct" << endl;    }    ~Student() { cout << "~Student()" << endl; }private:    string schoolName;    string schoolAddress;};//pass-by-valuebool validateStudent(Student s) {    return true;}int main() {    Student s;    cout << "------------------" << endl;    bool  b = validateStudent(s);    return 0;}

运行结果:

  Person()Student()------------------Person copy constructStudent copy construct~Student()~Person()~Student()~Person()

说明:从运行结果我们可以对validateStudent函数的参数的传递成本是“一次Student拷贝构造函数调用和一次Student析构函数调用”。其实远不止这些,因为上述两个类都含有两个string,他们也相应的调用拷贝构造函数,就是说四次string拷贝构造动作,还有四次析构函数。所以最终结果是以by value方式传递一个Student对象会导致一次Student拷贝构造函数、一次Person拷贝构造函数、四次string拷贝构造函数。当函数内的那个Student副本被销毁时,都要调用对应的析构函数。因此,以by value方式传递一个Student对象总体成本是“六次构造函数和六次析构函数”。

避免构造和析构的方法,就是pass by reference-to-const。

  bool validateStudent(const Student& s){      return true;  } 

运行结果:

Person()
Student()
------------------
~Student()
~Person()

说明:这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。将它声明为const是必要的,防止传入的Student对象被改变。

2、以by reference方式传递参数可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。

以by reference方式传递参数可以避免切割问题。如果窥视c++编译器的底层,references往往以指针实现出来,因此pass by reference通常意味着这真正传递的是指针。对内置类型(例如int)而言,pass by value或pass by reference-to-const时,选择pass by value并非没有道理。这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为pass by value。

slicing(对象切割)例子:

  #include<iostream>  #include<string>  using namespace std;class Window {public:    virtual void display()const {        cout << "window display()" << endl;    }};class WindowWithScrollBars :public Window {public:    virtual void WindowWithScrollBars::display() const {        cout << "WindowWithScrollBars display()" << endl;    }};void pintNaandWind(Window w) {    w.display();}int main() {    WindowWithScrollBars wwsb;    pintNaandWind(wwsb);    return 0;}

输出结果:

  window display()

说明:从运行结果可以看出,我们得到的是Window的display(),而我们本想得到WindowWithScrollBars的display()。它调用的是window对象的display(),而不是子类的,因为pintNaandWind中的参数传递是by value,WindowWithScrollBars的所有特化信息都会被切除。解决办法是 pass by reference-to-const。

  void pintNaandWind(Window& w){      w.display();  }  

运行结果:

  WindowWithScrollBars display()

请记住:

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicingproblem)。

  • 以上规则并不适合与内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适合。

条款21:必须返回对象时,别妄想返回其reference

在坚定追求pass-by-value(传值)的纯度中,一定会犯下一个致命的错误:开始传递一些references执行其实并不存在的对象。这可不是什么好事。任何时候看到一个reference声明式,都必须立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。

函数创建新对象的途径有二:在栈空间或在堆空间创建。

在stack空间创建对象例子:

  class Rational{  public:      Rational(int numerator, int denominator);      ...  private:      int n, d;      friend const Rational operator*(const Rational& lhs, const Rational& rhs);  };  const Rational& operator*(const Rational& lhs, const Rational& rhs){      Rational result(lhs.n * rhs.n, lhs.d * rhs.d);      return result;  }  

你可以拒绝这种做法,因为你的目标是避免调用构造函数,而result却必须像任何对象一样地由构造函数构造起来。更严重的是:这个函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前就被销毁了。

在heap空间创建对象例子:

  const Rational& operator*(const Rational& lhs, const Rational& rhs){      Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);      return result;  }

在堆上构造一个对象,并返回reference指向它。会出现问题:谁该对着被你new出来的对象实施delete呢?还有一个问题:如果有计算:Rational result = xyz,其中x、y、z都是Rational类型的对象,此时就会产生内存泄露的问题。因为同一语句内调用了两次operator*,因而使用两次new,也就需要两次delete,但却没有合理的办法让` operator * 使用者进行那些delete调用,因为没有办法取得operator*返回的references背后隐藏的那个指针。

一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。

请记住:

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

这篇关于【Effective C++】第四章 设计与声明(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1096358

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Python中的可视化设计与UI界面实现

《Python中的可视化设计与UI界面实现》本文介绍了如何使用Python创建用户界面(UI),包括使用Tkinter、PyQt、Kivy等库进行基本窗口、动态图表和动画效果的实现,通过示例代码,展示... 目录从像素到界面:python带你玩转UI设计示例:使用Tkinter创建一个简单的窗口绘图魔法:用

Golan中 new() 、 make() 和简短声明符的区别和使用

《Golan中new()、make()和简短声明符的区别和使用》Go语言中的new()、make()和简短声明符的区别和使用,new()用于分配内存并返回指针,make()用于初始化切片、映射... 详细介绍golang的new() 、 make() 和简短声明符的区别和使用。文章目录 `new()`

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�