第四章 设计与声明

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

请记住

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

条款19:设计class犹如type

C++中,当定义一个新的class,也就是定义了一个新type。

那么,如果设计高效的classes呢?以下是需要面对的问题:

  • type的对象应该如何被创建和销毁?

    • 这会影响到你的class的构造函数和析构函数以及内存分配函数的释放函数(operator newoperator new []operator deleteoperator [] delete)等的设计,不过前提是如果你自定义它们。
  • 对象的初始化和对象的赋值该有什么样的差别?
    • 这会决定你的构造函数和赋值操作符的行为以及其间的差异。很重要的是别混淆了"初始化"和"赋值",因为它们对应于不同的函数调用。
  • type的对象如果被passed-by-value(以值传递),意味这什么?
    • 这会让你思考copy构造函数用来定义一个typepass-by-value该如何实现。
  • 什么是新type的"合法值"?
    • class的成员变量而言,通常只有某些数值集是有效的。那些数值集决定了你的class必须维护的约束条件,也就决定了你的成员函数必须进行的错误检查工作,它也影响函数抛出的异常、以及函数异常明细。
  • 你的新type需要配合某个继承体系吗?
    • 如果你的类继承自某些既有的类,那么你就受到那些classes的设计的束缚,特别是受到"它们的函数是virtual还是non-virtual"的影响。如果你的类允许其他classes继承,那会影响你所声明的函数,尤其是析构函数是否为virtual
  • 你的新type需要什么样的转换?
    • 如果你允许类型T1被隐式转换为T2,那么就必须在class T1内写一个类型转换函数(operator T2)或者在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果你允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不能为类型转换操作符或on-explicit-one-argument构造函数
  • 什么样的操作符和函数对此新type而言是合理的?
    • 这会决定你将为你的class声明哪些函数,其中某些该是member函数,某些则不是。
  • 什么样的标准函数应该被驳回?
    • 这会决定你必须声明为private的函数。
  • 谁该取用新type的成员?
    • 这会决定你哪个成员为public,哪个为protected,哪个为private。也会决定你哪一个classesfunctions应该是friends,以及将它们嵌套于另一个之内是否合理。
  • 什么是新type的"未声明接口"?
    • 这会决定你对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?你在这些方面提供的保证将为你的class实现代码加上相应的约束条件。
  • 你的新type有多么一般化?
    • 或许你其实并非定义一个新type,而是定义一整个types家族。果真如此你就不该定义一个新class,而是应该定义一个新的class template
  • 你真的需要一个新type吗?
    • 如果只是定义新的派生类以便为既有的添加机能,那么说不定单纯定义一个或多个non-member函数或者templates更能达到目标。

请记住

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

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

缺省情况下C++by value方式传递对象至函数,传递过程中副本由对象的copy构造函数产出,这可能使得pass-by-value成为昂贵的操作。

class Person {
public:
Person();
virtual ~Person();
private:
std::string name;
std::string address;
}; class Student: public Person {
public:
Student();
~Student();
private:
std::string schoolName;
std::string schoolAddress;
}; // 使用
bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);

分析上述代码,以by value方式传递一个Student对象会导致调用一次Student copy构造函数、一次Person copy构造函数、四次string copy构造函数,并且当函数内的那个Student副本被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作,这就是昂贵的操作了。并且当参数接受一个基类对象但是传入一个子类对象时,传入的子类对象会被切割,只保有基类对象的部分,从而无法表现出多态。

这两个问题,通常可以通过pass-by-reference-to-const解决。因为reference往往以指针实现出来,因此它通常意味真正传递的是指针。

// 使用
bool validateStudent(const Student &s);

请记住

  • 尽量以pss-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
  • 以上规则并不适用与内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较恰当。但是不是所有小型对象都是pass-by-value的合格候选者。

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

必须返回对象的最常见是运算符函数:

const Rational operator*(const Rational&lhs, const Rational&rhs);

在必须返回对象时,不要企图放回reference,不然就会造成下面的情况:

  • 使用stack构造一个局部对象,返回局部函数的reference
const Rational& operator*(const Rational&lhs, const Rational&rhs) {
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}

这样做的问题:使用reference的本意是避免构造新对象,但是一个新的对象result还是经由构造函数构造。更严重的是,这个局部对象在函数调用完成后就被销毁了,reference将指向一个被销毁的对象。

  • 使用heap构造一个局部对象,返回局部函数的reference
const Rational& operator*(const Rational&lhs, const Rational&rhs) {
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
} Rational w, x, y, z;
w = x * y * z;

这样做的问题:虽然不再引用一个被销毁的对象,但是多了动态内存分配的开销。而且,谁该为delete负责也成为问题。并且当多次动态分配内存时只返回最后一个的指针,这就造成了资源泄漏,比如上面的连乘操作。

  • 构造一个static局部对象,每次计算结果保存在这个对象中,返回其reference
const Rational& operator*(const Rational&lhs, const Rational&rhs) {
static Rational result;
result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
} Rational w, x, y, z;
if((w * x) == (y * z)) {
// ...
}

这样做的问题:显而易见的问题是这个函数在多线程情况是不安全的,多个线程会修改相同的static对象。并且,在上面的判断语句中,不管传入的w, x, y, z是什么,由于operator*返回的reference都指向同一个static对象,因此上面的判断永远为真。

请记住

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

条款22:将成员变量声明为private

为什么不能是public

  • 语法一致性:如果成员函数和成员变量一样,都是public,那么调用时会困惑于该不该使用括号,比如想获取大小时使用size,但是这到底是一个成员变量还是一个成员函数呢?
  • 更精确的访问控制:通过将成员变量声明为private,通过成员函数提供访问,可以实现更精确的访问控制。
  • 封装特性(主要):如果通过public暴露,在需要改成员变量的大量实现代码中,会直接使用当这个成员变量被修改或删除时,这样所有直接访问该成员的代码可能将会变得不可用。

为什么不能是protected

  • 理由同上面的三个。

请记住

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

条款23:宁以non-member、non-friend替换member函数

假设有个浏览器类,包含一些功能用来清除下载元素高速缓存区、清除访问过的历史记录、以及移除系统中所有的cookies

class WebBrowser {
public:
// ...
void clearCache();
void clearCookies();
void clearHistory();
// ...
};

此时,如果想整个执行所有这些动作,那么有两个选择,一种实现成member函数,一种实现成non-member函数。

class WebBrowser {
public:
// ...
void clearCache();
void clearCookies();
void clearHistory(); // 实现成member函数,可以访问private成员
void clearEverything() {
clearCache();
clearCookies();
clearHistory();
}
// ...
}; // 实现成non-member函数,不可以访问private成员
void clearEverything(WebBrowser& wb) {
wb.clearCache();
wb.clearCookies();
wb.clearHistory();
}

关于这两种选择的抉择在于封装性

对于对象内的代码,越少代码可以看到数据(也就是访问它),越多的数据可被封装,也就越能自由地改变对象数据。作为一种粗糙的测量,越多的函数也可访问它,数据的封装性就越低。

请记住

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

条款24:如所有参数皆需类型转换,请为此采用non-member函数

为类支持隐式类型转换不是个好主意,但是在数值类型之间颇为合理。考虑有理数和内置类型之间的相乘运算。

具有如下有理数:

class Rational {
public:
Rational(int n = 0, int d = 0); // 构造函数可以不为explicit,提供了int-to-Rational的隐式转换 int numerator() const; // 分子的访问函数
int denominator() const; // 分母的访问函数
private:
// ...
};

现在提供了隐式转换方式,那么operator*应该实现成member还是non-member呢?

class Rational {
public:
// member
const Rational operator*(const Rational&rhs) const;
} // non-member
const Rational operator*(const Rational&lhs, const Rational&rhs);

区别在于混合运算上,如果是member,那么下面的混合运算只有一半行得通:

result = oneHalf * 2;   // 等价于oneHalf.operator*(2)  Success
result = 2 * result; // 等价于2.operator*(oneHalf) Error

因为内置类型没有相应的class,也就没有operator*成员函数,所以会错误。但是当实现为non-member时,具有两个参数,都能通过int-to-Rational,所以会正常。

请记住

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

条款25:考虑写出一个不抛异常的swap函数

swap原先只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自赋值的常见机制,可以参考条款12

  • STL的swap实现
namespace std{
template<typename T>
void swap(T& a,T& b){
T temp(a);
a=b;
b=temp;
}
}

只要类型T支持copying操作,上述实现就没问题,但是对于某些类型而言,其copying行为是不必要的,降低了程序运行的性能。

  • 最好的方案
class Widget{
public:
void swap(Widget& other){
using std::swap; // 必须声明
swap(pImpl, other.pImpl);
} private:
Widget *pImpl;
}; namespace std{
template<>
void swap<Widget>(Widget& a,Widget& b){
a.swap(b);
}
}

这种实现不仅高效还能与STL容器兼容,因为STL容器也提供了public swap成员函数与std::swap特例化版本。

  • template下的swap实现
namespace WidgetStuff{
template<typename T>
class Widget { }
template<typename T>
void swap(Widget<T>& a,Widget<T> &b){
a.swap(b);
}
}

请记住

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

【C++】《Effective C++》第四章的更多相关文章

  1. <<C++ Primer>> 第四章 表达式

    术语表 第 4 章 表达式 算术转换(arithmetic conversion): 从一种算术类型转换成另一种算术类型.在二元运算符的上下文中,为了保留精度,算术转换通常把较小的类型转换成较大的类型 ...

  2. C++Primer 第四章

    //1.当我们对运算符进行重载的时候,其包括运算对象的类型和返回值的类型都是由该运算符定义的,但是运算对象的个数和优先级,结合律都是不能改变的 //2.当一个对象被用作右值的时候,用的是对象的值(内容 ...

  3. c++primer 第四章编程练习答案

    4.13.1 #include<iostream> struct students { ]; ]; char grade; int age; }; int main() { using n ...

  4. C Primer Plus_第四章_字符串和格式化输入输出_编程练习

    Practice 1.输入名字和姓氏,以"名字,姓氏"的格式输出打印. #include int main(void) { char name[20]; char family[2 ...

  5. 《C++Primer》第五版习题解答--第四章【学习笔记】

    [C++Primer]第五版习题解答--第四章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/11 第四章:表达式 练习4. ...

  6. C++ Primer Plus学习:第十四章

    第十四章 C++中的代码重用 包含对象成员的类 将类的对象作为新类的成员.称为has-a关系.使用公有继承的时候,类可以继承接口,可能还有实现(纯虚函数不提供实现,只提供接口).使用包含时,可以获得实 ...

  7. C++ Primer Plus学习:第四章

    C++入门第四章:复合类型 1 数组 数组(array)是一种数据格式,能够存储多个同类型的值. 使用数组前,首先要声明.声明包括三个方面: 存储每个元素中值的类型 数组名 数组中的元素个数 声明的通 ...

  8. 【C++】《C++ Primer 》第十四章

    第十四章 重载运算与类型转换 一.基本概念 重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成.也包含返回类型.参数列表以及函数体. 当一个重载的运算符是成员函数时 ...

  9. 【C++】《C++ Primer 》第四章

    第四章 表达式 一.基础 重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义. 左值和右值: C中:左值可以在表达式左边,右值不能. C++中:当一个对象被用作右值的时候,用的是对象 ...

  10. C++ Primer Plus 第四章 复合类型 学习笔记

    第四章 复合类型 1. 数组概述 1.1 数组的定义 数组(array)是一种数据格式,能够存储多个同类型的值.每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素. 数组声明的三 ...

随机推荐

  1. 【题解】Generator(UVA1358)

    感觉我字符串和期望都不好-- 题目链接 题意 有 \(n\) 种字符,给定一个模式串 \(S\) ,一开始字符串为空,现在每次随机生成一个 1~n 的字符添加到字符串末尾,直到出现 \(S\) 停止, ...

  2. JavaScript:使用递归构建树型菜单

    使用递归函数将扁平数据转为树型结构,并渲染到页面 效果图: 代码: <!DOCTYPE html> <html lang="en"> <head> ...

  3. 精尽Spring MVC源码分析 - 一个请求的旅行过程

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  4. SecureCRT无法退格删除

    SecureCRT无法退格删除 securecrt无法退格删除问题解决: 如果想要全部会话都可以实现退格删除的功能,需要在全局选项设置. 最后选择全局应用即可.

  5. css进阶 04-如何让一个元素水平垂直居中?

    04-如何让一个元素水平垂直居中? #前言 老板的手机收到一个红包,为什么红包没居中? 如何让一个子元素在父容器里水平垂直居中?这个问题必考,在实战开发中,也应用得非常多. 你也许能顺手写出好几种实现 ...

  6. 数字crawlergo动态爬虫结合长亭XRAY被动扫描

    群里师傅分享了个挖洞的视频,搜了一下,大概就是基于这篇文章录的 https://xz.aliyun.com/t/7047 (小声哔哔一下,不得不说,阿里云先知社区和360酒仙桥六号部队公众号这两个地方 ...

  7. Python机器学习课程:线性回归算法

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 最基本的机器学习算法必须是具有单个变量的线性回归算法.如今,可用的高级机器学习算法,库和技术如此之多 ...

  8. iOS label 添加删除线(删划线)遇到的坑

    1.添加删划线方法遇到的问题 -(void)lastLabelDeal:(NSString *)str1 string:(NSString *)str2 label:(UILabel *)label{ ...

  9. NET 5 使用HttpClient和HttpWebRequest

    HttpWebRequest 这是.NET创建者最初开发用于使用HTTP请求的标准类.HttpWebRequest是老版本.net下常用的,较为底层且复杂,访问速度及并发也不甚理想,但是使用HttpW ...

  10. 获取Web项目中的控制器类以及类中Action方法

    前言 在使用时需要修改命名空间.需要过滤控制器.需要过滤Action方法.结果生成表的插入语句. 代码 public ActionResult ReloadData() { #region 获取所有的 ...