n 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质 。
n 促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。
n 阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
n tr1::shared_ptr支持定制型删除器。这可防范dll问题,可被用来自动解除互斥锁。
当你定义一个新class,你也就定义了一个新type。几乎每一个class都要求回答如下提问:
如果你继承自某些已有的classes,你就受到那些classes的束缚,特别是它们的函数是virtual还是non-virtual。
如果你允许其他class继承你的class,会影响你所声明的函数——是否为virtual。
允许类型T1隐式转换为T2,就必须在T1内写一个类型转换函数(operator T2)或在T2中写一个non-explicit-one-argument的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数。
n 尽量以pass-by-ref-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
class Person{ public: Person(); virtual ~Person(); … }; |
class Student: public Person{ public: Student(); ~Student(); … } |
bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);
//Student的copy构造函数会被调用,以plato为蓝本将s初始化。
替代方案:bool validateStudent(const Student& s);
以引用为传递方式效率高。修订后的参数中的const是重要的。原先的是by value传递Student,因此调用者知道他们受到保护,不会对传入的plato作任何改变,只能对它的复件s作改变。因此const是必要的。
by ref传递的另一个好处是可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些物化性质全被切割掉了,仅留下一个base class对象。
n 以上规则并不适用内置内型,以及 STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
n 绝不要返回pointer或ref指向一个local stack对象,或返回 ref反射一个heap-allocated对象,或返回pointer或ref指向一个local static对象。
一个有理数的class内含一个函数用来计算有理数的乘积:
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
…
private:
int n,d;
friend Rational operator* (const Rational& lhs, const Rational& rhs); //以by-value返回结果
};
why要以by value返回结果,by ref不可以吗?
ref只是一个名称,代表一个既有对象。当看到一个ref时,定要想到它的另一个名称是什么?
在调用operator *之前,内含乘积的Rational对象不可能存在。例如:
Rational a(1, 2), b(3, 5);
Rational c = a*b; //原本存在一个3/10的对象是不合理的
创建新对象在stack空间或heap空间上。
const Rational& operator *(const Rational& lhs, const Rational& rhs)
{
Rational result ( lhs.n * rhs.n, lhs.d * rhs.d);
return result; //函数调用一结束,result就会被释放,ref指向一个已释放的对象,Danger!
}
const Rational& operator *(const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational ( lhs.n * rhs.n, lhs.d * rhs.d);
return *result; //如何释放result??会造成资源泄漏
}
例如:Rational w, x, y, z;
w =x*y*z;
const Rational& operator *(const Rational& lhs, const Rational& rhs){
static Rational result;
…
return result;
}
情况更差:Rational a, b, c, d;
…
if( a*b == c*d)//总为true;
{…}
n 将成员变量声明为private。 这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。
n protected并不比public更具封装性。
假设我们有一个protected成员变量,而我们最终取消了它?有多少代码被破坏?所有使用它的derived class都会被破坏,那往往是个不可知的大量。一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。
举例来说:
class WebBrowser{
public:
…
void clearCache();
void clearHistory();
void removeCookies();
…
};
class WebBrowser{ public: void clearEverything();//调用上面的三个函数 }; |
void ClearBrowser(WebBrower& wb) {… //通过wb调用上面的三个函数。 } |
哪一个较好?member函数?还是non-member函数?
面向对象守则要求,数据以及操作数据的函数应该捆绑在一起,这意味着member函数较好。
但clearEverything实际上只是一个便利函数,一个像WebBrowser这样的 class有大量的便利函数,某些与书签有关,某些与打印有关,还有与cookie有关,通常用户只对其中的部分感兴趣。没道理只对cookie便利感兴趣的客户也要与书签发生关系。
//头文件webbrowser.h namespace WebBrowserStuff{ class WebBrowser{ … }; … //核心机能,所有客户都需要 } |
//头文件browserbookmarks.h namespace WebBrowserStuff{ … //与书签相关的便利函数 } |
//头文件browsercookie.h namespace WebBrowserStuff{ … //与cookie相关的便利函数 } |
这正是C++标准程序库组织的方式。标准程序库并不是拥有单一头文件并在其内含std命令空间内的每一个东西,而是有数十个头文件<vector> <algorithm>等,每个头文件声明为std的某些机能。
将所有便利函数放在多个头文件中但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
//构造函数不为explicit,允许int-to-Rational隐式转换
int numerator( ) const;
int denominator( ) const;
private:
…
};
乘法:
class Rational{
public: …
const Rational operator* (const Rational& rhs)const;
};
Rational oneEighth(1, 8), oneHalf(1, 2);
Rational result = oneHalf * oneEihth; //OK
result = oneEighth * oneHalf; //OK
但result = oneHalf * 2; //OK, oneHalf.operator*(2)
result = 2 * oneHalf; //Error, 2.operator*(oneHalf)
编译器也会尝试寻找operator*(2, oneHalf),但没有定义这样的函数为什么2*oneHalf没有发生隐式转换?只有参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。
解决办法:
const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.deominator() * rhs.deominator());
}
result = oneHalf * 2; //OK
result = 2 * oneHalf; //OK
无论何时如果你可以避免friend函数就该避免,因为就像真实世界一样,朋友带来的麻烦往往多过其价值。当然有时候friend有其正当性,但这个事实依然存在:不能够只因函数不该成为member,就自动让它成为friend。
STL标准的swap实现如下:
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
n 如果你提供一个member swap,也请提供一个non-member swap用来调用前者。对于 classes非templates,请特化std::swap。
class WidgetImpl{ public:… private: int a, b, c; std::vector<double> v; }; |
class Widget{ public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) { … *pImpl = *(rhs.pImpl); } private: WidgetImpl* pImpl; }; |
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);}
}
对于class templates而非class,如下:
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);}
//Error,std是特殊的命令空间,其管理规则比较特殊。客户可以全物化 std内的 template,但不可以添加新的template
}
解决方案:
namespace Widget Stuff{
template<typename T>class Widget{ }
template<typename T>
void swap (Widget<T> & a, Widget<T>& b){a.swap(b);} //OK
}