文章目录
- 一、让接口容易被正确使用,不易被误用
- 1、引入新类型预防“接口被误用”
- 2、限制类型内什么事可做,什么事不可做
- 3、请记住
- 二、设计class犹如设计type
- 1、设计class的一些问题
- 2、请记住
- 三、宁以pass-by-reference-to-const替换pass-by-value
- 1、pass-by-value存在的问题
- 2、pass-by-reference-to-const的好处
- 3、请记住
- 四、必须返回对象时,别妄想返回其reference
- 1、定义local 变量,在stack空间创建对象
- 2、使用new在heap-based上创建对象
- 3、“必须返回新对象”的正确做法
- 4、请记住
- 五、将成员变量申明为private
- 1、将成员变量声明为private的理由
- 2、请记住
- 六、宁以non-member,non-friend替换member
- 1、non-member函数还是member函数
- 2、请记住
- 七、若所有参数都需要类型转换,请为此采用non-member函数
- 1、采用non-member函数支持类型转换
- 2、请记住
一、让接口容易被正确使用,不易被误用
接口是客户和你的代码交换的唯一手段。如果客户正确使用你开发的接口,那自然很好;但是如果你的接口被误用,你也要负一部分责任。理想上,如果客户使用了某个接口却没有获得他想要的行为,那么不应该编译通过;如果编译通过了,那么客户就应该得到他想要的行为。
1、引入新类型预防“接口被误用”
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;
};
class Date
{
public:
Date(const Month& m, const Day& d, const Year& y);
};
Date d(30, 3, 1995); // 错误,类型(需要Day,Month,Year类型)不正确
Date d(Day(30), Month(3), Year(1995)); // 错误,类型(顺序)不正确
Date d(Month(3), Day(30), Year(1995)); // ok,顺序和声明式一致,类型正确
2、限制类型内什么事可做,什么事不可做
常见的限制是加上const。这样,下面语句便不会导致错误:
if (a * b) = c // 原意是要做一次比较动作。
3、请记住
- 好的接口很容器被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解锁互斥锁。
二、设计class犹如设计type
1、设计class的一些问题
- 新type的对象应该如何被创建和销毁:这会影响到class的构造函数和析构函数以及内存的分配和释放。
- 对象初始化和对象赋值该有什么样的区别:这个是你构造函数和赋值操作符的行为以及它们的差异。不要混淆初始化和赋值,它们对应不同的函数。
- 新type的对象如果被pass by value,意味着什么:这决定于你的copy构造函数。
- 什么是新type的合法值:对于class的成员变量而言,通常只有某些数据集是有效的。这些数据集决定了你的class要维护的约束条件,也决定了你的某些成员函数进行的错误检查的工作,它也影响函数抛出异常的明细。
- 新type需要配合某个继承图系(inheritance graph)吗:如果你继承自某些既有的classes,特别是收到那些classes的virtual或non-virtual的影响(条款34和条款36)。如果你允许其他classes继承你的class,那会影响你所声明的函数,尤其是析构函数(是否为virtual)。
- 新type需要什么样的转换:你的type生存在其他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函数,一些不是。
- 什么样的函数应该被驳回:即哪些函数应该声明为private。
- 谁该取用新type的成员:这个问题帮助你决定哪个成员是public,哪个是private。也帮你解决哪一个class或fuction应该是friends,以及将它们嵌套于另一个是否合理。
- 什么是新type的“未声明接口”(undeclared interface):它对效率、异常安全性(条款29)以及资源运用(例如多任务和动态内存)提供何种保证?你在这些方面提供的保证将为你的class实现代码上加上响应约束条件。
- 你的新type有多么一般化:也许你并非定义一个新type,而是定义一整个types家族。如果是这样就不应该定义一个新class,而是应该定义一个新的class template。
- 你真的需要一个新type吗:如果只是定义新的derived class以便为既有的class添加机能,那么说不定单纯定义一个或多个non-member function或templates,更能达到目标。
2、请记住
- Class的设计就是type的设计。在定义一个新type之前,请谨慎考虑本条款覆盖的所有讨论主题。详见原著。
三、宁以pass-by-reference-to-const替换pass-by-value
1、pass-by-value存在的问题
缺省情况下C++以by value方式(一个继承自C的方式)传递对象至(或来自)函数。除非你另外指定,否则函数参数都是以实际实参的复件为初值,而调用端所获得的亦是函数返回值的一个复件。这些复件系由对象的copy构造函数产出,这可能使得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); // pass by value
Student plato;
bool platIsOK = validateStudent(plato);
2、pass-by-reference-to-const的好处
用pass-by-reference-to-const 方式,则不会有任何构造函数或析构函数被调用,因为没有任何新对象被创建。const非常重要,原先以by value方式接受一个对象参数,因此调用者知道他们受到保护,函数内绝对不会对传入的对象作任何改变,函数只能够对其复件做修改。若对象以by reference方式传递,将它声明为const是必要的,因为不这样做的话调用者会忧虑函数会不会改变他们传入的那个对象。
以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质(derived class专属的成员变量)全被切割掉了,仅仅留下一个base class对象。
3、请记住
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免对象切割问题。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
四、必须返回对象时,别妄想返回其reference
1、定义local 变量,在stack空间创建对象
const Rational& operator* ( const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 糟糕的代码
return result;
}
上述代码,其一,使用构造函数构造新对象;其二,函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前就被销毁了。任何函数如果返回一个reference指向某个local对象,都将一败涂地。
2、使用new在heap-based上创建对象
const Rational& operator* ( const Rational& lhs, const Rational& rhs)
{
// 更糟糕的代码
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
上述代码更糟糕,因为你现在还需要面对另一个问题:谁该对着被你new出来的对象实施delete?没有合理的办法让operator* 使用者进行那些delete调用,因为没有合理的办法让他们取得operator*返回的reference背后隐藏的那个指针。
3、“必须返回新对象”的正确做法
inline const Rational operator* ( const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
一个“必须返回新对象”的函数的正确写法:让那个函数返回一个新对象。即便需要承担operator*返回值的构造成本和析构成本也是值得的。
4、请记住
- 不要返回pointer或者reference指向一个on stack对象(被析构)。
- 不要返回pointer或者reference指向一个on heap对象(需要用户delete,我觉得必要的时候也不是不可以)。
- 不要返回pointer或者reference指向local static对象,却需要多个这样的对象(static只能有一份)。
五、将成员变量申明为private
1、将成员变量声明为private的理由
- 语法一致性。使成员变量唯一的访问办法是通过成员函数(public接口)。
- 使用函数,可以让你对成员变量的处理有更精确的控制。可以实现出“不准访问”,“只读”,“只写”,“可读可写”的访问限制。
- 最重要的,实现类的封装性。如果通过函数访问成员变量,日后对成员变量的更改便不会影响到客户。
2、请记住
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class 作者以充分的实现弹性。
- protected并不比public更具封装性。
六、宁以non-member,non-friend替换member
1、non-member函数还是member函数
class WebBrowser
{
public:
void clearCash();
void clearHistory();
void removeCookies();
public:
// 成员函数
void clearEverything()
{
clearCash();
clearHistory();
removeCookies();
}
};
// 非成员函数
void clearBrowser(WebBrowser& wb)
{
wb.clearCash();
wb.clearHistory();
wb.removeCookies();
};
// 更好的做法
namespace WebBrowserStuff
{
class WebBrowser{……};
void clearBrowser(WebBrowser& we);
}
上面两种做法,哪种比较好呢?答案是non-member函数比较好。面向对象思想要求,数据尽可能被封装,member函数clearEverything带来的封装性比non-member函数clearBrowser低。提供non-member函数,对class相关机能有较大包裹弹性(packaging flexibility),因此带来了较低的编译相依度,增加了class的可延展性。
封装意味着不可见。愈多东西被封装,欲少人可以看到它,我们就有愈大的弹性去改变它。愈少代码可以看到数据(访问数据),愈多数据可被封装,我们就更有自由来改变对象数据。愈多函数可以访问它,数据的封装性就愈低。
前面有讲到,成员变量应该是private,否则就有无限多函数可以访问它,毫无封装可言。能访问private成员变量的函数只有class的member函数、friend函数而已。在一个member函数和一个non-member、non-friend函数之间做抉择,如果两者提供相同的机能,显然后者提供了更大的封装,这个就是上面选择clearBrowser函数的原因。
在封装这点上,需要注意两点。1、这个论述只适用于non-member、non-friend函数。2、因为封装,让函数成为class的non-member函数,但这并不意味着它不可以是另一个class的member函数。
2、请记住
- 用non-member、non-friend函数替换member函数,这样可以增加封装性、包裹弹性和机能扩充性。
七、若所有参数都需要类型转换,请为此采用non-member函数
1、采用non-member函数支持类型转换
通常情况,class不应该支持隐式类型转换,因为这样可能导致我们想不到的问题。这个规则也有例外,最常见的例外是建立数值类型时。例如编写一个分数管理类,允许隐式类型转换。
class Rational
{
public:
// 非explicit,允许隐式转换
Rational(int numerator = 0, int denominator = 1);
const Rational operator*(const Rational& rhs);
};
Rational onEight(1, 8);
Rational oneHalf(1, 2);
Rational result = onEight * oneHalf;
result = result * onEight;
result = oneHalf * 2; // 正确,相当于oneHalf.operator*(2);
result = 2 * oneHalf; // 错误,相当于2.operator*(oneHalf);
不能满足交换律。因为2不是Rational类型,不能作为左操作数。oneHalf*2会把2隐式转换为Rational类型。
上面两种做法,第一种可以发生隐式转换,第二种却不可以,这是因为只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。第二种做法,还没到到”参数被列于参数列内“,2不是Rational类型,不会调用operator*。
如果要支持混合运算,可以让operator*成为一个non-member函数,这样编译器可以在实参身上执行隐式类型转换。
class Rational
{
public:
};
//non-member函数
const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // ok
result = 2 * oneFourth; // ok
这里还有一个重要结论:member函数的反面是non-member函数,不是friend函数。如果可以避免成为friend函数,那么最好避免,因为friend的封装低于非friend。
2、请记住
- 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member(因为其显式操作所有参数)。