18.
让接口容易被正确使用,不易被误用
1 2 3 4 class Date {public : Date (int month, int day, int year); };
函数接口 可能以错误的次序传递参数,或者传递一个无效的月份或天数
导入简单的外覆类型来区别天数,月份等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct Day {explicit Day (int d) :val(d){ }int val; }struct Month {explicit Month (int m) :val(m){ }int val; }struct Year {explicit Year (int y) :val(y){ }int val; }
令Day,Month,Year成为成熟且经充分锻炼的类并封装内部数据,比简单使用structs好
限制正确的类型的取值,比较安全的方法时预定义所有有效的Months,以函数替换对象,表现某个特定月份,因为non-local
static对象的初始化次序可能出现问题
预防客户错误的另一个办法是,限制类型内什么是可做,什么是不能做。常见的限制是加上const。条款3:以const修饰operator*的返回类型可阻止客户因用户自定义类型而犯错
除非有好理由,否则应该尽量令你的types的行为于内置types一致
避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。STL容器的接口十分一致,这使得它们非常容易被调用
较佳接口的设计原则是令函数返回一个智能指针,事实上,返回tr1::shared_ptr让接口设计者阻止资源泄露的错误
1 std::tr1::shared_ptr<Investment> createInvestment () ;
假设类设计者期许"从createInvestment取得Investment*指针"的客户将该指针传递给一个名为getRidOfInvestment的函数,而不是直接使用delete。这样一个接口又开启通往另一个客户错误的大门,该错误是"企图使用错误的资源析构机制",设计者可以设计一个返回将getRidOfInvestment绑定为删除器"的tr1::shared_ptr
tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时将被调用的删除器。这启发我们创建一个null
tr1::shared_ptr并以getRidOfInvestment作为删除器
1 std::tr1::shared_ptr<Investment>pInv (static_cast <Investment*>(0 ),getRidOfInvestment);
tr1::shared_ptr有一个特别好的性质是,它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“cross-DLL
problem"。这个问题发生于对象在动态链接程序库DLL中被new创建,却在另一个DLL内被delete销毁,在许多平台上,这一类跨DLL之new/delete成对使用会导致运行期错误。
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
trl::shared_ptr支持定制型删除器(custom
deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)等等。
19. 设计class犹如设计type
新type对象应该如何被创建和销毁?(第八章)
对象的初始化和对象的赋值该有什么样的差别?(条款四)
新type的对象如果被passed by value(以值传递),意味着什么?
copy构造函数用来定义一个type的pass-by-value如何实现
什么是新type的”合法值“
你的新type需要配合某个继承图系吗?
如果继承自某些既有的类,就会受到那些类的设计的束缚,特别是他们的函数时virtual或non-virtual的影响(条款34和36)
如果允许其他类继承你的class,那会影响你所声明的函数,尤其是析构函数是否为virtual(条款7)
你的新type需要什么杨的转换
如果允许T1隐式转换为T2,就必须在class
T1内写一个类型转换函数(operator T2)或者在class
T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数,如果只允许explicit狗在函数存在,就得写专门负责转换的函数,且不得为类型转换操作符(type
conversion
operators)或者non-explicit-one-argument构造函数。(条款15)
什么样的操作符和函数对此新type而言时合理的?
决定于将为class声明哪些函数,其中某些该是member函数,某些则否(见条款23,24,46)
什么样的标准函数应该驳回?
那些正是你必须声明为private者(条款6)
谁该取用新type的成员
决定哪个成员为public,哪个为protected,哪个为private
决定哪一个类或者函数应该是friends,以及将它们嵌套于另一个之内是否合理
什么是新type的”未声明接口“
对效率、异常安全性(条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证,
你的新type有多么一般化
定义一个新的class template,定义一整个types家族
你真的需要一个新type吗
如果只是定义新的派生类以便为既有的class添加机能,说不定单纯定义一个或者多个non-member函数或者templates,更能达到目标
20.
宁以pass-by-reference-to-const替换pass-by-value
缺省情况下C++以by value方式传递对象至函数
除非另外指定,否则函数参数都是以实际参数的副本为初值,而调用端所获得的亦是函数返回值的一个副本,这些副本是对象的copy构造函数产出,使得pass-by-value成为费时的操作。
如何回避所有的构造和析构动作?pass by reference-to-const
1 bool validateStudent (const Student& s) ;
这种传递方式效率高,没有任何构造函数和析构函数被调用,因为没有任何新对象被创建。const是重要的,
以by
reference方式传递参数可以避免slicing(对象切割)问题。当一个派生类对象以by
value方式传递并被视为一个基类对象,基类的copy构造函数会被调用,而造成此对象的行为像个派生类对象的那些特化性质全被切割掉了,仅仅留下一个基类对象,但这几乎不是想要的
references往往以指针实现出来,因此pass by
reference意味真正传递的是指针。如果对象属于内置类型(int等),pass by
value往往比pass by
reference效率高。对于内置类型而言,有机会采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非无道理。
同时也适用于STL迭代器和函数对象,因为它们习惯上被设计为passed by
value
内置类型都相当小,对象小并不意味着其copy构造函数不昂贵。许多对象,包括大多数STL容器,内含的东西只比指针多一些,但复制这种对象却需要承担复制那些指针所指的每一样东西,那将非常昂贵
小型的用户自定义类型不必然成为pass by value优良候选人
尽量以pass-by-reference-to-const替换pass by
value,前者通常高效,并可避免切割问题
以上规则并不适用内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。
21.必须返回对象时,别妄想返回其reference
可能会传递一些references指向其实并不存在的对象
函数创建新对象的途径:在stack和heap空间创建
不能返回reference指向的local 对象,
若在heap内构造一个对象,返回指向的reference,同样有问题,delete该如何调用?
一个必须返回新对象的函数的正确写法是,让那个函数返回一个新对象
1 2 3 4 inline const Rational operator * (const Rational &lhs, const Rational& rhs) { return Rational (lhs.n * rhs.n, lhs.d * rhs.d); }
需要承受operator *返回值的构造和析构成本
当你必须在“返回一个reference和返回一个object”之间抉择时,你的工作时挑出行为正确的那个
绝不要返回pointer或reference指向一个local
stack对象,或者返回reference指向一个heap-allocated对象或返回pointer或reference指向一个local
static对象而有可能同时需要多个这样的对象。条款4已经为“单线程环境中合理返回reference指向一个local
static对象”提供一个设计实例。
22. 将成员变量声明为private
如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要打算访问class成员时迷惑的试着记住是否该使用小括号
如果通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class内部实现已经起了变换
将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如这可以使得成员变量被读或被写时轻松通知其对象、可以验证class的验证条件以及函数的前提和时候状态、可以在多线程环境中执行同步控制等
封装的重要性。对客户隐藏成员变量,可以确保class的约束条件总是会获得维护
protected成员变量的论点类似。成员变量的封装性与“成员变量的内容改变时所破坏的代码数量”成反比。
假设有一个protected成员变量,而我们最终取消了它,所有使用它的派生类都会被破坏,同样缺少封装习惯。
从封装角度来说,只有两种访问权限:private(提供封装)和其他(不提供封装)
将成员变量声明为private。这可赋予客户访问数据的一致性,可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
protected并不比public更具封装性。
23.
宁以non-member、non-friend替换member函数
1 2 3 4 class WebBrowser {public : void clearCache () ; void clearHistory () ;
void clearEverythinng();
};
//non-member函数 void clearBrowser(WebBrowser& wb) {
wb.clearCache(); wb.clearHistory(); }
1 2 3 4 5 6 7 8 9 10 11 12 1. 面向对象守则要求,数据以及操作数据的那些函数应该捆绑在一起,member函数时较好的选择。这是对基于对面向对象真实意义的一个误解。面向对象守则要求数据应该尽可能被封装。2. 如果member函数的封装性比non-member低。3. 愈多东西被封装,我们改变那些东西的能力就越大,这就是推崇封装的原因,它能使改变事物而只影响有限客户4. 能够访问private 成员变量的函数只有class 的member函数加上friend 函数而已5. 如果在一个member函数和一个non-member,non-friend 函数之间选择,而且两者提供相同机能,那么导致封装性较大的时non-member non-friend 函数,因为它并不增加“能够访问class 内之private 成分”的函数变量。6. 让clearBrowser成为一个non-member函数并位于WebBrowser所在的同一个namespace 内 ```cppnamespace WebBrowserStuff{ class WebBrowser {}; void clearBrowser (WebBrowser& wb) ; }
namespace和classes不同,前者可以跨越多个源码文件而后者不能。
将所有便利函数放在多个头文件内但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。要做的是添加更多non-member
non-friend函数到此命名空间
宁可拿non-member
non-friend函数替换member函数,这样做可以增加封装性、包裹弹性和机能扩充性
24.
若所有参数皆需类型转换,请为此采用non-member函数
令classes支持隐式转换是个糟糕的主意,最常见的例外就是在建立数值类别时。假设设计一个class用来表现有理数,允许整数“隐式转换”合理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Rational {public : Rational (int num1 = 0 , int num2 = 1 ); int num1 () const ; int num2 () const ; };const Rational operator *(const Rational& lhs, const Rational& rhs) { return Rational (lhs.num1 () * rhs.num1 (), lhs.num2 () * rhs.num2 ()); } Rational result; result = R1 * 2 ; result = 2 * R1;
member函数的反面时non-member函数,而不是friend函数
如果需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member
25.
考虑写出一个不抛异常的swap函数
swap函数原来只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我复制的可能性的一个常见机制。还有非凡的复杂度。
1 2 3 4 5 6 7 8 9 namespace std{ template <typename T> void swap (T& a, T& b) { T temp (a) ; a = b; b = temp; } }
主要类型T支持copying,缺省的swap实现代码会帮你置换类型T的对象
声明一个non-member函数调用member函数,
有些类型,swap的缺省行为会降低速度,如以指针指向一个对象,内含真正数据的类型,这种类型的常见表现形式是所谓的“pimpl手法”。
只要置换其Pimpl指针,将std::swap针对Widget特化
1 2 3 4 5 6 7 namespace std{ template <> void swap <Widget>(Widget& a, Widget& b) { swap (a.pImpl, b.pImpl); } }
template<>表示它是std::swap的一个全特化版本,函数名称之后的表示这一特化版本系针对“T是Widget”而设计的。一般性的swap
template施行于Widgets身上便会启用这个版本。通常不能够改变std命名空间内的任何东西,但可以为标准templates制造特化版本,使它专属于我们自己的classes
令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Widget {public : void swap (Widget& other) { using std::swap; swap (pImpl, other.pImpl); } };namespace std{ template <> void swap <Widget>(Widget& a, Widget& b) { a.swap (b); } }
这种做法不只能够通过编译,还与STL容器有一致性,因为所有STL容器都是有提供public
swap成员函数和std::swap特化版本(用以调用前者)
C++只允许对class template偏特化(partially specialize),在function
template偏特化行不通
当打算偏特化一个function
template时,惯常做法是简单为它添加一个重载版本,声明一个non-member
swap让它调用member swap,但不再将那个non-member
swap声明为std::swap的特化版本或者重载版本。
1 2 3 4 5 6 7 8 9 10 namesapce WidgetStuff{ template <typename T> class Widget {}; template <typename T> void swap (Widget<T>& a, Widget<T>& b) { a.swap (b); } }
C++的名称查找法则确保将找到global作用域T所在之命名空间内的任何T专属的swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”找出WidgetStuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using表达式让std::swap在函数内曝光。即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,如果已针对T将std::swap特化,特化版会被编译器挑中。
首先,如果swap的缺省实现码对你的class或class
template提供可接受的效率,你不需要额外做任何事。任何尝试置换那种对象的人都会取得缺省版本,而那将有良好的运作。
其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
提供一个public
swap成员函数,让他高效的置换你的类型的两个对象值,这个函数绝不该抛出异常
在你的class或template所在的命名空间内提供一个non-member
swap,并令它调用上述swap成员函数
如果你正在编写一个class(而非class
template),为你的class特化std::swap。并令它调用你的swap成员函数
最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member
swap用来调用前者。对于classes,(而非templates),也请特化std::swap
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
为“用户定义类型”进行std
templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西