【C++】《Effective C++》第四章
第四章 设计与声明
条款18:让接口容易被正确使用,不易被误用
请记住
- 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这些性质。
- "促进正确使用"的办法包括接口的一致性,以及与内置类型的行为兼容。
- "阻止误用"的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr
支持定制型删除器(custom deleter
)。这可防范DLL
问题,可被用来自动解除互斥锁(mutexes
)等等。
条款19:设计class
犹如type
C++中,当定义一个新的class,也就是定义了一个新type。
那么,如果设计高效的classes呢?以下是需要面对的问题:
- 新
type
的对象应该如何被创建和销毁?- 这会影响到你的
class
的构造函数和析构函数以及内存分配函数的释放函数(operator new
,operator new []
,operator delete
,operator [] delete
)等的设计,不过前提是如果你自定义它们。
- 这会影响到你的
- 对象的初始化和对象的赋值该有什么样的差别?
- 这会决定你的构造函数和赋值操作符的行为以及其间的差异。很重要的是别混淆了"初始化"和"赋值",因为它们对应于不同的函数调用。
- 新
type
的对象如果被passed-by-value(以值传递)
,意味这什么?- 这会让你思考
copy构造函数
用来定义一个type
的pass-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
。也会决定你哪一个classes
或functions
应该是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
对象,或返回pointer
或reference
指向一个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-member
、non-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
并且不带任何"命名空间资格修饰"。 - 为"用户定义类型"进行s
td templates
全特化是好的,但不要尝试在std
内加入某些对std
而言全新的东西。
【C++】《Effective C++》第四章的更多相关文章
- <<C++ Primer>> 第四章 表达式
术语表 第 4 章 表达式 算术转换(arithmetic conversion): 从一种算术类型转换成另一种算术类型.在二元运算符的上下文中,为了保留精度,算术转换通常把较小的类型转换成较大的类型 ...
- C++Primer 第四章
//1.当我们对运算符进行重载的时候,其包括运算对象的类型和返回值的类型都是由该运算符定义的,但是运算对象的个数和优先级,结合律都是不能改变的 //2.当一个对象被用作右值的时候,用的是对象的值(内容 ...
- c++primer 第四章编程练习答案
4.13.1 #include<iostream> struct students { ]; ]; char grade; int age; }; int main() { using n ...
- C Primer Plus_第四章_字符串和格式化输入输出_编程练习
Practice 1.输入名字和姓氏,以"名字,姓氏"的格式输出打印. #include int main(void) { char name[20]; char family[2 ...
- 《C++Primer》第五版习题解答--第四章【学习笔记】
[C++Primer]第五版习题解答--第四章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/11 第四章:表达式 练习4. ...
- C++ Primer Plus学习:第十四章
第十四章 C++中的代码重用 包含对象成员的类 将类的对象作为新类的成员.称为has-a关系.使用公有继承的时候,类可以继承接口,可能还有实现(纯虚函数不提供实现,只提供接口).使用包含时,可以获得实 ...
- C++ Primer Plus学习:第四章
C++入门第四章:复合类型 1 数组 数组(array)是一种数据格式,能够存储多个同类型的值. 使用数组前,首先要声明.声明包括三个方面: 存储每个元素中值的类型 数组名 数组中的元素个数 声明的通 ...
- 【C++】《C++ Primer 》第十四章
第十四章 重载运算与类型转换 一.基本概念 重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成.也包含返回类型.参数列表以及函数体. 当一个重载的运算符是成员函数时 ...
- 【C++】《C++ Primer 》第四章
第四章 表达式 一.基础 重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义. 左值和右值: C中:左值可以在表达式左边,右值不能. C++中:当一个对象被用作右值的时候,用的是对象 ...
- C++ Primer Plus 第四章 复合类型 学习笔记
第四章 复合类型 1. 数组概述 1.1 数组的定义 数组(array)是一种数据格式,能够存储多个同类型的值.每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素. 数组声明的三 ...
随机推荐
- 【题解】Generator(UVA1358)
感觉我字符串和期望都不好-- 题目链接 题意 有 \(n\) 种字符,给定一个模式串 \(S\) ,一开始字符串为空,现在每次随机生成一个 1~n 的字符添加到字符串末尾,直到出现 \(S\) 停止, ...
- JavaScript:使用递归构建树型菜单
使用递归函数将扁平数据转为树型结构,并渲染到页面 效果图: 代码: <!DOCTYPE html> <html lang="en"> <head> ...
- 精尽Spring MVC源码分析 - 一个请求的旅行过程
该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...
- SecureCRT无法退格删除
SecureCRT无法退格删除 securecrt无法退格删除问题解决: 如果想要全部会话都可以实现退格删除的功能,需要在全局选项设置. 最后选择全局应用即可.
- css进阶 04-如何让一个元素水平垂直居中?
04-如何让一个元素水平垂直居中? #前言 老板的手机收到一个红包,为什么红包没居中? 如何让一个子元素在父容器里水平垂直居中?这个问题必考,在实战开发中,也应用得非常多. 你也许能顺手写出好几种实现 ...
- 数字crawlergo动态爬虫结合长亭XRAY被动扫描
群里师傅分享了个挖洞的视频,搜了一下,大概就是基于这篇文章录的 https://xz.aliyun.com/t/7047 (小声哔哔一下,不得不说,阿里云先知社区和360酒仙桥六号部队公众号这两个地方 ...
- Python机器学习课程:线性回归算法
本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 最基本的机器学习算法必须是具有单个变量的线性回归算法.如今,可用的高级机器学习算法,库和技术如此之多 ...
- iOS label 添加删除线(删划线)遇到的坑
1.添加删划线方法遇到的问题 -(void)lastLabelDeal:(NSString *)str1 string:(NSString *)str2 label:(UILabel *)label{ ...
- NET 5 使用HttpClient和HttpWebRequest
HttpWebRequest 这是.NET创建者最初开发用于使用HTTP请求的标准类.HttpWebRequest是老版本.net下常用的,较为底层且复杂,访问速度及并发也不甚理想,但是使用HttpW ...
- 获取Web项目中的控制器类以及类中Action方法
前言 在使用时需要修改命名空间.需要过滤控制器.需要过滤Action方法.结果生成表的插入语句. 代码 public ActionResult ReloadData() { #region 获取所有的 ...