item18 让接口容易被正确使用,不易被误用
一个例子
class Date {
public:
Date(int month, int day, int year);
...
};
- 输入无效的数据(超过范围)
输入顺序错误 :::tips 解决方式:
预定义所有有效值
- 限制类型内什么可以做什么不可以
- 除非有好的理由,否则应该尽量让自己定义的types行为与内置的types一致
- 消除客户对于管理资源的责任
:::
另一个例子
比如之前的factory
函数的例子,当调用factory
函数是,返回一个指针
这种方式强迫客户将返回值存储在std::tr1::shared_ptr<Investment> createInvestment();
shared_ptr
中,消除了忘记删除底部对象的可能;不过这又有新的错误可能 :::info 例如,我们希望给智能指针的删除方法是自定义的getRidOfInvestment
,但是可能客户会忘记; ::: 我们的结局思路是,在createInvestment
中先构建一个绑定好的空shared_ptr
,然后重新赋值 :::infotr1::shared_ptr
提供某个构造函数接受两个实参
- 需要管理的指针
- 删除器
:::
:::tips shared_ptr的另一个好性质是可以自动使用每个指针专属的删除器;std::tr1::shared_ptr<Investment> createInvestment(){
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment* >(0),
getRidOfInvestment);
retVal=...真正值
return retVal;
}
这可以消除cross-DLL problem:
- 如果对象在一个DLL中被new创建,在另一个DLL内被delete,跨DLL的new/delete可能导致崩溃
但是shared_ptr会缺省的删除器是来自shared_ptr诞生的那个DLL。 :::
item19 设计class犹如设计type
:::info 应当带着“语言设计者当初设计语言内置类型时”一样的谨慎来研讨 class 的设计。 ::: 几乎每一个 class 都要求你面对以下提问:
新 type 的对象应该如何被创建和销毁?
这会影响 class 的构造函数和析构函数,以及内存分配函数和释放函数(new,new[],delete,delete[])
- 对象的初始化和对象的赋值该有什么样的差别?
这会影响你的构造函数和赋值操作符的行为
- 新 type 的对象如果被 passed by value(以值传递),意味着什么?
这会影响 copy 构造函数
- 什么是新 type 的“合法值”?
所有成员函数需要进行错误检查工作,进行必要的约束,处理异常
- 你的新 type 需要配合某个继承图系吗?
如果继承自既有 class,你就会收到这些 classes 设计的束缚,特别是收到它们的函数是 virtual 或 non-virtual 的影响。如果你允许其他 classes 继承你的 class,会影响你所申明的函数,尤其是析构函数是否为 virtual
- 什么样的操作符和函数对此新 type 而言是合理的?
这会影响你为你的 class 申明哪些函数
- 你的新type需要什么样的转换?
如果允许隐式转换,那么要考虑写转换函数以及是否允许explicit的构造函数
- 什么样的标准函数应该驳回?
如果不需要编译器默认生成的版本,需要自行申明为 private 拒绝
- 谁该取用新 type 的成员?
这会影响你 class 所有成员的可访问级别,以及哪些是 friends
- 什么是新 type 的“未申明接口”?
它对效率、异常安全性以及资源运用提供何种保证?
- 你的新 type 有多么一般化?
或许你并非定义一个新 type,而是定义一整个 types 家族,此时应该定义一个新的 class template 而非 class
- 你真的需要一个新 type 么?
如果只是添加一些功能,也许淡出添加些 non-member 函数或 templates 就能达到目标
item20 宁以pass-by-reference替换pass-by-value
:::tips
- 尽量以pass-by-reference替换pass-by-value。前者通常高效,并且可以避免切割问题
以上规则并不适用于内置类型、STL的迭代器以及函数对象,对他们而言pass-by-value更适当。 ::: 缺省情况下C++以by value方式传递对象给函数,此外函数返回值也是一个复制值。这对于函数而言有时会造成比较大的开销:一次copy构造函数的调用,一次析构函数调用。 :::info 对象切割是什么问题?
当一个派生类对象被用pass-by-value的方式传递给一个基类对象时,基类对象的拷贝构造函数只能有权限复制基类的部分。但是如果使用pass-by-reference的话,会起到多态的效果。 ::: :::success 实际上在C++编译器的底层,reference往往是以指针实现出来的,pass-by-reference通常意味着传递的是指针。 :::item21 必须返回对象时,别妄想返回其reference
:::info 绝不要返回
pointer
或reference
指向一个local stack
对象,或返回reference
指向一个head-allocated
对象,或返回pointer
或reference
指向一个local static
对象设置有可能需要多个这样的对象。 ::: 如果从节约资源的角度考虑,那么函数返回一个reference肯定比返回一个值要节约一次拷贝构造函数。函数创建新对象:在stack或者heap空间上创建一个对象
- local变量——在stack上创建,如果返回其引用,那么函数结束生命周期时,该局部变量会销毁,引用非法。
- new一个对象——在heap上创建,谁来销毁是个问题
在类内部定义一个static对象;
- 多线程安全问题
- 比较相等时会出错。
item22 将成员变量声明为private
如果public
接口内所有东西都是函数,那么用户不需要在打算访问class
成员时迷惑地试图记住是否该使用小括号;此外,使用函数可以对成员变量更精准的控制,如:可以实现只读、可写等等约束;
:::info 绝大多数变量是不需要从外部访问的,因此可以将所有变量变成class accessLevels{
public:
//...
int getReadOnly() const{return readOnly;}
void setWriteOnly(int val){ writeOnly=val;}
private:
int readOnly;
int writeOnly;
};
private
,只需要给必须的变量留接口。 ::: 封装的好处: :::tips
可以确保
class
的约束始终被维护- 可以避免与客户的代码过度耦合
成员变量的封装性与“当其内容改变时可能造成的代码破坏量”成反比;
- 例如:如果一个
public
成员变量,我们取消了它,那么所有涉及该变量的代码都需要重写 - 如果一个
protected
成员变量,我们取消了它,那么所有使用它的派生类都会被破坏。 ::: :::danger 实际上只有两种封装:private
和其他 :::
- 例如:如果一个
切记将成员变量声明为
private
。这赋予客户访问数据的一致性、可细微划分的范围控制、允诺约束条件获得保证,并提供class
作者以充分的实现弹性。protected
不必public
更具有封装性。item23 宁以non-member、non-friend替换member函数
一个例子:
例如有一个class
表示网页浏览器;
如果有一个操作想要实现清除以上所有信息,有两种实现:class WebBrowser{
public:
void clearCache();
void clearHistory();
void removeCookies();
};
member function
ornon-member function
:::tips 问题来了,哪个版本更好? ::: 先说答案,从控制封装的角度看用non-member function更好!class WebBrowser{
public:
void clearEverything();
}
//--------------------------------------------------
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
如何衡量成员的封装性?
:::info 简单的说,我们计算能够访问该成员变量的函数数量,作为一种粗糙的测量。愈多函数可访问它,数据的封装性就越低。 ::: 因此对于一个函数而言,non-member version如果和member version机能相同,那么从封装的角度考虑,non-member version可能会更合适,当然不止需要从封装角度考虑,还需要从别的角度(如隐式类型转换)
成为class 的non-member并不意味着不可以是另一个class的member;比如可以令函数是某个工具类的成员。 :::tips 在C++中,比较自然的方法是定义一个non-member function,并且位于和类同一个命名空间内。 :::
namespace WebBrowserSutff{
class WebBrowser{};
void clearBrowser(WebBrowser& wb);
//namespace可以跨越多个源码文件。
}
如何分离便利函数
大多数情况下,用户可能只会对某个便利函数感兴趣,如果全部编译需要时间较多,如何分离编译相依的关系? :::tips 分离声明所在的头文件 :::
//webbrowser.h
namespace WebBrowserSutff{
class WebBrowser{};
}
//webbrowserbookmarks.h
namespace WebBrowserSutff{
void clearbookmark(WebBrowser& wb);
//与书签相关的便利函数
}
//webbrowsercookies.h
namespace WebBrowserSutff{
void clearcookie(WebBrowser& wb);
//与cookie相关的便利函数
}
:::info C++STL的组织方式也是这样的!将多个便利函数放在不同头文件但是隶属于同一个命名空间。 :::
item24 若所需参数皆需要类型转换,请采用non-member函数
:::info 如果需要为某个函数的所有参数(包括this指针指向的隐含参数)进行类型转换,此函数必须是non-member ::: 考虑一个有理数类:
class Rational{
public:
Rational(int numerator=0,int denominator=1);
int numerator() const;
int denominator() const;
private:
..
};
例如我们要实现乘法operator*
采用member 还是non-member?
member function带来的问题
const Rational Rational::(const Rational& rhs) const;
这种情况下定义的乘法可以支持两个对象相乘,但是对于混合运算可能出问题。
Rational oneHalf(1,2);
Rational result;
result = oneHalf*2;
//result = oneHalf.operator*(2);
//执行了隐式的类型转换
//如果Rational的构造是explicit的,那么也不可以!
result = 2*oneHalf;//wrong!
//result = 2.operator*(oneHalf);
:::success
解决方案:让operator*
成为一个non-member function
- 只有当参数被列于参数列内,此参数才是隐式类型转换的合格参与者
- 如果用成员函数来表示的话,必然会隐含this指针,不在参数列表内
:::
const Rational operator*(const Rational& lhs,const Rational& rhs);
item25 考虑写出一个不抛异常的
swap置换两个对象的值,在缺省时会使用标准库提供的swap算法swap
函数
:::tips 缺陷是会复制三次!对于某些类而言没有必要。——哪些类?pimpl:(pointer to implementation) ::: ```cpp class WidgetImpl { public: WidgetImpl(); ~WidgetImpl();namespace std{
template<typename T>
void swap(T& a,T& b){
T temp(a);
a=b;
b=temp;
}
}
private:
int a, b, c;
vector
private: WidgetImpl* pImpl; };
:::info
pimpl:用指针指向一个对象,内含真正的数据
- 如果想要交换两个Widget的值,实际上只需要交换pImpl指针;
- 但是缺省的`swap`算法不仅复制3个`Widget`,还会复制三个`WidgetImpl`对象(why?**看**`**operator=**`**的操作可以看出来**!)
:::
<a name="sYdsi"></a>
## 修改方案1
直接在namespace std中定义特化的swap可以解决,但是无法访问私有对象;
:::tips
通常我们不允许改变std命名空间中的任何东西,但是可以被允许为标准template制造特化版本。
:::
```cpp
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);
}
}
:::info 解决方案如上:
- 在std域中定义全特化版本的swap,其实现是通过调用类内的swap
- 类内的swap采用std中普通的swap来交换指针
这样做维持了拷贝构造函数的深拷贝,并且节约了交换时的开销; :::
对于template类的特化
:::info C++只允许对class templates偏特化,在function templates上偏特化不被允许。 :::
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>t& b){
a.swap(b);
}
}
因此考虑采用重载; :::info 但是std命名空间里不允许添加新的templates! :::
namespace std {
template<typename T>
//重载的swap,swap后没有<>
void swap(Widget<T>& a, Widget<T>t& b){
a.swap(b);
}
}
//std空间不允许修改!达咩!
解决方案:
定义一个命名空间;
- 采用non-member版本的swap,通过调用member版本swap实现
将swap和class都放入此空间;
namespace WidgetStuff {
template<typename T>
class Widget{
public:
void swap(const Widget& rhs){
using std::swap;
swap(pImpl,other.pImpl);
}
};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
C++编译器查找规则
考虑如下代码
template<typename T>
void doSomething(T& obj1,T& obj2){
using std::swap;
swap(obj1,obj2);
}
:::info
名称查找法则(name lookup rules):寻找global作用域或者T所在的命名空间内任何T专属的swap
- 实参取决之查找规则:在T的作用域内找的时候安装此规则
鉴于使用了std::swap,可以找std中的swap。 :::
summary
当
std::swap
对某类型效率不高时,提供一个swap
成员函数,并确定此函数不抛出异常;- 如果提供了一个member
swap
,也应该给出一个non-memberswap
来调用他。对于class而言,需要特化std::swap - 调用
swap
时,应该针对std::swap
使用using
,然后调用swap
并且不带任何命名空间资格修饰 - 可以为用户定义的类型进行std全特化,但是不要在std中加入新的东西。