《Effective C++》第三版-4. 设计与声明(Design and Declarations)
- 条款17:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly)
- 条款19:设计class犹如设计type(Treat class design as type design)
- 条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-cons to pass-by-value)
- 条款21:必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)
- 条款22:成员变量声明为private(Declare data members private)
- 条款23:宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)
- 条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)
- 条款25:考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)
条款17:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly)
限制类型和值
class Date {
public:
Date(int month, int day, int year); //可能月日年顺序错,可能传递无效的月份或日期
...
};
可使用类型系统(type system)规避以上错误,即引入外覆类型(wrapper type)区别年月日:
struct Day {
explicite Day(int d)
: val(d) { }
int val;
}
struct Month {
explicite Month(int m)
: val(m) { }
int val;
}
struct Year{
explicite Year(int y)
: val(y) { }
int val;
}
class Date {
public:
Date(const Month& m, const Day& d, const Year& y); //可能月日年顺序错,可能传递无效的月份或日期
...
};
Date d(Month(3), Day(30), Year(1995)); //可有效防止接口误用
保证了类型正确之后,需要保证输入的值有效:
class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
...
static Month Dec() { return Month(12); }
...
private:
explicit Month(int m);
...
};
Date d(Month::Mar(), Day(30), Year(1995));
规定能做和不能做的事
if ( a * b = c) ... //以const修饰操作符*,使其不能被赋值
提供行为一致的接口
为了避免忘记删除或者重复删除指针,可令工厂函数直接返回智能指针:
Investment* createInvestment(); //用户可能忘记删除或者重复删除指针
std::tr1::shared_ptr<Investment> createInvestment();
若期望用自定义的getRidOfInvestment,则需要避免误用delete,可考虑将getRidOfInvestment绑定为删除器(deleter):
删除器在引用次数为0时调用,故可创建一个null shared_ptr
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment); //创建一个null shared_ptr
retVal = ... ; //令retVal指向目标对象
return retVal;
}
若pInv管理的原始指针能在pInv创立之前确定下来,则将原始指针直接传递给pInv的构造函数更好
tr1::shared_ptr会自动使用每个指针专属的删除器,从而无须担心cross-DLL problem:
cross-DLL problem:对象在动态连接程序库(DLL)中被new创建,但在另一个DLL内被delete销毁
//返回的tr1::shared_ptr可能被传递给任何其他DLL
//其会追踪记录从而在引用次数为0时调用那个DLL的delete
std::tr1:;shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
Boost的tr1::shared_ptr特点:
- 是原始指针的两倍大
- 以动态分配内存作为簿记用途和删除器的专属数据
- 以virtual形式调用删除器
- 在多线程程序修改引用次数时有线程同步化(thread synchronization)的额外开销
Tips:
- 好的接口不易被误用
- 促进正确使用的方法包括接口一致性和与内置类型的行为兼容
- 阻止误用的办法包括建立新类型、限制类型上的操作、束缚对象值、消除客户的资源管理责任
- tr1::shared_ptr支持定制型删除器(custom deleter),这可以防范DLL问题,可被用来自动解除互斥锁(mutexes)等
条款19:设计class犹如设计type(Treat class design as type design)
定义一个新class时也就定义了一个新type。设计高效的类需要考虑以下问题:
- 新type的对象应如何创建和销毁(第8章))
- 影响构造函数和析构函数、内存分配函数和释放函数(operator new,operator new [],operator delete,operator delete [])
- 对象的初始化和赋值应有什么差别(条款4)
- 决定构造函数和赋值操作符的行为
- 新type的对象如果被pass-by-value意味着什么
- 由copy构造函数定义pass-by-value如何实现
- 什么是新type的合法值
- 有效的数值集决定了类必须维护的约束条件(invariants),
- 进而决定了成员函数(特别是构造函数、析构函数、setter函数)的错误检查
- 还影响函数抛出的异常和极少使用的函数异常明细列(exception specifications)
- 有效的数值集决定了类必须维护的约束条件(invariants),
- 新type需要配合某个继承图系(inheritance graph)吗
- 继承既有的类,则受那些类束缚,尤其要考虑那些类的函数是否为虚函数
- 被其他类继承,则影响析构函数等是否为virtual
- 新type需要什么样的转换
- 若允许类型T1隐式转换为类型T2,可可考虑:
- 在T1类内写类型转换函数(operator T2)
- 在T2类内些non-explicit-one-argument(可被单一实参调用)的构造函数
- 若只允许explicit构造函数存在,就得写专门执行转换的函数,且没有类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数
- 若允许类型T1隐式转换为类型T2,可可考虑:
- 什么样的操作符和函数对于此新type合理
- 决定需要声明哪些函数,其中哪些是成员函数
- 什么样的标准函数应驳回
- 这些必须声明为private
- 谁改取用新type的成员
- 影响public、private、protected的选择
- 影响友元类、友元函数、及其嵌套的设计
- 什么是新type的未声明接口(undeclared interface)
- 要考虑其对效率、异常安全性、资源运用的保证
- 新type有多么一般化
- 若要定义整个type家族,则应该定义新的class template
- 是否真的需要新type
- 若定义新的派生类就足够,则可能定义non-member函数或templates更好
Tips:
- Class设计就是type设计,需要考虑以上所有问题
条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-cons to 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); //会调用六次构造函数和六次析构函数
bool validateStudent(const Student& s); //效率提升很多
上述代码validateStudent函数中pass-by-value会调用六次构造函数和六次析构函数:
- Student构造+Person构造+Student的2个string+Person的2个string
- 析构同理
使用pass-by-reference可避免频繁构造和析构
避免对象切割
对象切割(slicing):派生类以值传递并被视为基类对象时,回调用基类的构造函数,而派生类的成分全无
class Window {
public:
...
std::string name() const; //返回窗口名称
virtual void display() const; //显示窗口和其内容
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};
void printNameAndDisply(Window w)
{
std::cout << w,name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisply(wwsb); //对象会被切割,因为参数w时Window对象,故调用的Window::display
void printNameAndDisply(const Window& w) //不会被切割
{
std::cout << w,name();
w.display();
}
例外
- 内置类型和STL的迭代器与函数对象采用pass by value往往效率更高,
- 小型type不一定适合pass by value
- 一旦需要复制指针的所指物,则copy构造函数可能成本很高
- 即使小型对象的copy构造函数不昂贵,其效率也存在争议
- 某些编译器对内置类型和自定义类型的态度截然不同,即使二者底层表示(underlying representation)相同
- 如可能会把一个double放入缓存器,而只包含一个double的对象则不会
- by reference则肯定把指针放入缓存器
- 用户自定义类型的大小容易变化,因其内部实现可能改变,故不一定适合pass by value
- 某些标准程序库实现版本中的string类型比其他版本大七倍
Tips:
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常高效且能避免切割问题
- 以上规则并不适用内置类型和STL的迭代和与函数对象,它们更适合pass-by-value
条款21:必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)
考虑有理数乘积:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
...
private:
int n, d; //分子和分母
friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};
上述代码中操作符以by value的方式返回值,如果要返回reference则操作符必须自己创建新Rational对象,其途径有二:在stack或heap空间创建(反例)
//返回local对象的引用,但是local对象在离开函数时就销毁了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
//难以对new创建的对象delete,尤其以下连乘的例子
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; //operator*(operator*(x, y), z)
若使用static Rational避免调用构造函数,则会有如下问题:
//返回local对象的引用,但是local对象在离开函数时就销毁了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = ... ;
return result;
}
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a * b) == (c * d)) { ... } //==总是为true
else { ... } //,因两侧是同一个同一个stetic Rational对象的引用
必须返回新对象的函数的正确写法为:
inline const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
Tips:
- 绝不要返回指针或引用指向local stack对象
- 绝对不要返回引用指向heap-allocated对象
- 绝对不要在有可能同时需要多个这样的对象时返回指针或引用指向local static对象
条款22:成员变量声明为private(Declare data members private)
这一条似乎没啥内容_
把成员变量声明为private的原因如下:
- 接口的一致性:非public的成员函数只能通过函数访问,并且可以方便的设置读写权限
- 封装
- 把成员变量隐藏在函数接口背后,可以方便地更改实现方式
- public成员变量修改后所有使用它们的客户码都会被破坏
- protected成员变量修改后所有使用它们的派生类都会被破坏,其并不比public更具有封装性
Tips:
- 切记把成员变量声明为private,这个赋予访问数据的一致性、可细微划分访问控制、保证约束条件、提供充分的实现弹性
- protected并不比public更具封装性
条款23:宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)
考虑有个类表示网页浏览器:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything(); //执行所有清除动作
...
};
执行所有清除动作由两个方案:
- WebBrowser 提供函数
- 由non-member函数调用相应的member函数
- 封装性更高,且包裹弹性(packaging flexibility)较大,编译相依度较低,是更好的方案
//WebBrowser 提供函数
class WebBrowser {
public:
...
void clearEverything(); //执行所有清除动作
...
};
//由non-member函数调用相应的member函数
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
两点注意事项:
- 准确地说,封装性良好的是non-member non-friend函数,而非non-member函数
- 一个类的non-member non-friend函数可以是可以是另一个类的member
- 有些语言的函数必须定义在类内(如Eiffel,Java,C#),可以令clearBrowser成为某个工具类(utility class)的一个static member函数,而非WebBrowser的一部分或friend
- 在C++中可让clearWebBrowser成为non-member函数且和WebBrowser位于同一命名空间
namespace WebBrowserStuff {
Class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
命名空间能跨越多个源码文件而类不能,故可将同一命名空间下不同功能类型的函数放在不同的头文件:
标准程序库也不是一个庞大的单一头文件,而是有若干个头文件,每个头文件声明std的某些功能,这样可以使得用户只依赖所使用的一小部分系统
//头文件webbrowser.h,包含WebBrowser自身和核心功能
namespace WebBrowserStuff {
class WebBrowser { ... };
... //核心功能,如广泛使用的non-member函数
}
//头文件webbrowserbookmarks.h,
namespace WebBrowserStuff {
... ////与书签相关的函数
}
//头文件webbrowsercookies.h,
namespace WebBrowserStuff {
... //与cookie相关的函数
}
Tips:
- 宁可拿non-member non-friend函数替换member函数,以增肌封装性、包裹弹性、功能扩展性
条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)
考虑有理数类:
class Rational {
public:
Rational(int numerator = 0, //构造函数刻意不为explicit
int denominator = 1); //允许int到Rational的隐式转换
int numerator() const; //分子和分母的访问函数
int denominator() const;
private:
...
};
若操作符*为Rational的成员函数:
class Rational {
public:
...
const Rational operator* (const Rational& rhs) const;
};
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; //正确,发生了隐式类型转换,根据int创建了Rational
result = oneHalf.operator*(2); //但如果是explicit构造函数则错误
result = 2 * oneHalf; //错误!
result = 2.operator*(oneHalf); //错误!重写上式,错误一目了然
result = operator*(2, oneHalf); //错误!本例不存在接受int和Rational的操作符*
只有参数位于参数列内,这个参数才能隐式类型转换
要支持混合运算,则可让操作符*成为non-member函数:
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
member函数的反面是non-member函数,而非friend函数
从Objected-Oriented C++转换到Template C++且Rational是一个class template时,本条款需要考虑新的问题
Tips:
- 若需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那这个函数必须是non-member
条款25:考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)
缺省的swap
缺省情况下swap动作可由标准程序库提供的swap算法完成:
namespace std {
template<typename T> //只要T支持copying即可实现swap
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
特化的swap
缺省的swap涉及三个对象的复制,而pimpl手法(pointer to implementation)可避免这些复制:
置换两个Widget对象值只需要置换其pImpl指针;而缺省的swap会复制三个Widget,并且复制三个WidgetImpl对象
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //复制Widget时,就复制WidgetImpl对象
{
...
*pImpl = *(ths.pImpl);
...
}
...
private:
WidgetImpl* pImpl; //所指对象内涵Widget数据
};
将std::swap针对Widget特化可解决上述问题:
令Widget声明public swap成员函数做真正的置换工作(采用成员函数是为了取用private pImpl,non-member函数则不行),再把std::swap特化
class Widget {
public:
...
void swap(Widget& other)
{
using std::swap; //这个声明有必要,稍后解释
swap(pImpl, other.pImpl); //真正做置换工作,
}
...
};
namespace std {
template<> //表示其是std::swap的全特化(total template specialization)版本
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //要置换WIdget就调用其swap成员函数
}
}
上述代码与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::特化版本(以调用成员函数)
若Widget和WidgetImpl都是class template,可考虑把WidgetImpl内的数据类型参数化:
template<typename T>
class WidgetImpl { ... };
template<typename T>
calss Widget { ... };
此时特化std::swap会遇到问题:
//以下代码企图偏特化(partially specialize)一个function template(std::swap)
//但C++只允许对class template偏特化
//故无法通过编译(虽然少数编译器错误地通过编译)
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{ a.swap(b);}
}
//偏特化function template时,通常会添加重载版本
//但以下代码也不合法,因为std不能添加新的templates,这由C++彼岸准委员会决定
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //注意swap之后没有<...>
{ a.swap(b); }
}
解决方案:声明一个non-memebr swap以调用member swap,但不再将non-member swap声明为std::swap的特化版本或重载版本
任何代码如果要置换两个Widget对象而调用swap,则C++的名称查找法则(name lookup rules;更具体地说是argument-dependent lookup或Koeing lookup法则)会找到WidgetStuff内的Widget专属版本
namespace WidgetStuff { //为简化,把Widget相关功能都放入WidgetStuff命名空间内
...
template<typename T>
class Widget { ... };
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //non-member swap函数,不属于std命名空间
{
a.swap(b);
}
}
若想要class专属版的swap在尽可能多的语境下被调用,则需呀在该class所在的命名空间内写一个non-member版本和一个std::特化版本,故应该为该class特化std::swap
若希望调用T专属版本,并且在该版本不存在的情况下调用std内的一般化版本,可实现如下:
C++的名称查找法则确保会找到global作用域或T所在的命名空间内的任何T专属的swap;若没有专属swap则using声明使得能够调用std::swap
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; //令std::swap在此函数内可用
...
swap)obj1, obj2); //为T调用最佳swap版本
...
}
std::swap(obj1, obj2); //错误的方式!强迫编译器调用std::swap
使用swap的总结
swap的使用总结如下:
- 若缺省的swap的效率可接受,则无需做额外的事
- 若缺省的swap效率不足,则可考虑:
- 提供public swap成员函数,使其置换相应类型的两个对象值,且绝不抛出异常
- 在class或template所在的命名空间内提供一个non-member swap,并调用上述swap成员函数
- 若正在编写class(而非class template),则特化std::swap并使其调用swap成员函数
- 若调用swap,则需要包含using声明式,使std::swap在函数内可见,之后不加namespace直接调用swap
成员版swap绝不可抛出异常,其最好的应用是帮助class或class template提供强烈的异常安全性(exception-safety)保障
Tips:
- 当std::效率不高时,提供一个swap成员函数,并确保其不抛出异常
- 如果提供一个member swap,则要提供一个non-member swap调用前者。对于class(而非template),也最好特化std::swap
- 调用swap时应声明 using std:;swap,之后不带命名空间修饰地调用swap
- 为自定义类型进行std template全特化可以,但是不要再std内加入新东西`
《Effective C++》第三版-4. 设计与声明(Design and Declarations)的更多相关文章
- 《Effective Java 第三版》新条目介绍
版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...
- 《Effective C++》第4章 设计与声明(2)-读书笔记
章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti ...
- 《Effective C++》第4章 设计与声明(1)-读书笔记
章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti ...
- C++学习书籍推荐《Effective C++ 第三版》下载
百度云及其他网盘下载地址:点我 编辑推荐 <Effective C++:改善程序与设计的55个具体做法(第3版)(中文版)(双色)>前两个版本抓住了全世界无数程序员的目光.原因十分明显:S ...
- 《Effective Java 第三版》目录汇总
经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...
- Effective Java第三版(一) ——用静态工厂代替构造器
此文做为<Effective Java>系列的第一篇,所以有必要大概说下此书的特点,当然很多人可能都看过,毕竟是有着Java四大名著之一的大名在外,不过总会有萌新不了解,例如我!<E ...
- effective java(第三版)---读书笔记
第一章 引言 < Effective Java>这本书并不厚,而且并不适合初学者,适合有一定的工作经验的java攻城狮.这本书不是百科全书式的JAVA 手册,而是试图在讲述如何正确.高效地 ...
- 《Effective C++(第三版)》-笔记
1. 让自己习惯C++ 条款01: 视C++为一个语言联邦 1.1 C++ 是一个多重泛型编程语言(multiparadigm programming),支持:过程形式(procedural),面向对 ...
- Effective Java 第三版——77. 不要忽略异常
Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...
- Effective Java 第三版——19. 如果使用继承则设计,并文档说明,否则不该使用
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
随机推荐
- 【环境配置】vscode配置C C++开发和调试环境
按照本教程配置好之后,不再需要写任何tasks.json和launch.json文件,减轻记忆负担,即使是调试程序,也不用再写这些文件了,跟着做就能得到一个很好的集成开发环境 目录 msys2的安装与 ...
- vivo 消息中间件测试环境项目多版本实践
作者:vivo 互联网中间件团队 - Liu Tao 在开源 RocketMQ 基础之上,关于[测试环境项目多版本隔离]业务诉求的落地与实践. 一.背景 在2022年8月份 vivo 互联网中间件团队 ...
- rsync 运维利器,同步工具
NAME rsync - faster, flexible replacement for rcp SYNOPSIS rsync [OPTION]... SRC [SRC]... DEST rsync ...
- 论文阅读:A new approach solve the multi-product multi-period inventory lot sizing with supplier selection problem
论文:A new approach solve the multi-product multi-period inventory lot sizing with supplier selection ...
- 使用OHOS SDK构建zlib
参照OHOS IDE和SDK的安装方法配置好开发环境. 从gitee下载源码,当前最新的提交记录ID为04f42ceca40f73e2978b50e93806c2a18c1281fc. 执行如下命令: ...
- 鸿蒙手表定位功能Demo体验,适用儿童、老年和外出旅游安全市场
针对儿童和老人,可穿戴的智能手表用处很大.市场也有许多类似的产品,支持接打电话.支付扫码.定位等功能,属于新兴的商业机会.依托华为品牌,鸿蒙手表也致力为用户打造精品的.产品质量佳.可穿戴的智能体验.对 ...
- django admin后台自定义数据保存方式
故事背景是这样的: 为了方便工作中数据的整理,需要开发一个 管理系统,用于记录一些事情. 该系统不需要精美的前端的页面,只需要使用django的admin后台管理就可以了. 我需要在添加数据的时候,把 ...
- HDC2021技术分论坛:酷炫3D效果在瘦设备上也能实现?
作者:zhuhuanhuan,图形技术专家 随着3D技术的应用普及,越来越多的场景都能看到3D的身影,比如充电动效.3D壁纸.游戏等等,给用户带来了更有趣.更丰富的体验.要满足用户的3D体验需求,离不 ...
- DevEco Studio 3.1差异化构建打包,提升多版本应用开发效率
原文:https://mp.weixin.qq.com/s/8XtgZ-k0mGXCjKHfSXFoOg,点击链接查看更多技术内容. HUAWEI DevEco Studio是开发Harmo ...
- 4天带你上手HarmonyOS ArkUI开发
本次HarmonyOS ArkUI入门训练营课程--健康生活实战篇,手把手教大家如何制作一个合理膳食的APP前端Demo! 课程实战样例通过ArkUI声明式UI开发框架实现,只需用几行简单直观的声明式 ...