读书笔记中涉及到的所有代码实例可通过https://github.com/LuanZheng/EffectiveCPlusPlus.git进行下载获得。
Item22 将成员变量声明为private
如果public接口内的每样东西都是函数,客户就不需要在打算访问class成员时迷惑地试着记住是否该使用小括号。
细微的划分访问控制破有必要,因为许多成员变量应该被隐藏起来。
如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。
封装的重要性比你最初见到它时更重要。如果一个public成员变量被替换,或删除,可能会导致很多代码被破坏。
Item23 宁以non-member, non-friend替换member函数
面向对象守则要求尽可能的对数据进行封装。与直观相反的,non-member, non-friend函数更利于对象的封装。
这个论述只适用于non-member non-friend函数。只因在意封装性而让函数“成为class的non-member”并不意味它“不可以是另一个class的member”。
封装可以使我们改变事务而只影响有限客户。
我们计算能够访问该数据的函数数量,作为一种粗糙的度量。越多函数可访问它,数据的封装性就越低。
namespace和class不同。前者可以跨越多个源码文件而后者不能。
将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。
例子见Item23
Item24 若所有参数皆需类型转换,请为此采用non-member函数
下面例子中,若使用成员函数,则在使用如下两句(乘法交换律)时,却会发生不同的结果。
//Rational r5 = r2 * 2; //正确,r2有operator*成员函数,2(作为参数)可以通过构造函数进行隐式转换,
//从int转换成Rational
//Rational r6 = 2 * r2; //错误,2(作为调用者)并没有operator*成员函数,也无法自己对自己进行转换
#ifndef _RATIONAL_H_
#define _RATIONAL_H_
class Rational
{
//friend const Rational operator*(const Rational& multiplicator1, const Rational& multiplicator2);
public:
Rational(const int numerator, const int denominator = 1);
~Rational();
const Rational operator*(const Rational& rhs) const;
private:
int m_Numerator;
int m_Denominator;
};
#endif // !_RATIONAL_H_
只有当参数被列为参数列内,这个参数才是隐式类型转换的合格参与者。this对象绝不是隐式类型转换的合格参与者。
如果需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数一定是个non-member.
补充:对于非成员函数,比如友元函数,不能对函数使用const限定符。
例子见Item21
Item25 考虑写出一个不抛异常的swap函数
缺省情况下swap动作可由标准程序提供swap算法完成。
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
但在这个swap中,涉及到三次复制,a复制到temp,b复制到a,temp复制到b. 有时候,这些复制并不必要。例如在使用了“以指针指向一个对象,内含真正数据”那种类型中。
可以看到,在如下程序中,std::swap(w1,w2)导致了一次copy构造函数调用,2次copy运算符调用。
#include "Widget.h"
int main()
{
Widget w1;
Widget w2;
std::swap(w1, w2);
return 0;
}
对代码进行修改,将std::swap提供一个全特化版本,然后提供一个成员函数用来进行Widget内部的数据交换。则可以看到,前面三次copy构造函数和copy assignment的调用就消除了。
void Widget::swap(Widget& other)
{
std::swap(this->pWImpl, other.pWImpl);
}
#include "Widget.h"
namespace std
{
template<>
void swap(Widget& a, Widget& b)
{
a.swap(b);
}
}
int main()
{
Widget w1;
Widget w2;
std::swap(w1, w2);
return 0;
}
注:函数模板的特化: 函数模板特化是在一个统一的函数模板不能在所有类型实例下正常工作时,需要定义类型参数在实例化为特定类型时函数模板的特定实现版本。
假设Widget和WidgetImpl都是class templates而非classes,
template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};
尝试偏特化函数模板是不符合语法的。比如:
namespace std
{
template<typename T> //企图偏特化函数模板,那不合法
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
解决方法是简单的提供一个重载版本
namespace std
{
template<typename T>
//重载swap,但很不幸,不允许添加新的templates(或function,或class)在std命名空间中
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
如果希望软件有预期的行为,请不要添加任何新东西到std里头(但可以全特化std中的模板函数)。
解决方法是把上面的swap重载版本放入到另一个命名空间中即可。
namespace WidgetStuff
{
template<typename T>
//重载swap,但很不幸,不允许添加新的templates(或function,或class)在std命名空间中
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
C++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属swap.
查找顺序:
1)所在命名空间的swap.
2)std空间中特化版本的swap.
3)std空间的普通swap.
最后,请记住,成员版的swap绝不抛异常。注意,这一准则只适用于成员版。
例子见Item25