条款18 让接口容易被正常使用,不易被误用
总结:
1、好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
2、促进正确使用的方法包括接口的一致性,以及与内置类型的行为兼容。
3、预防错误的方法包括创建新的类型,限定类型的操作const,约束对象的值,以及消除客户的资源管理职责。
4、tr1::shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体(mutex)等。
引言:
class Date {
public:
Date(int month, int day, int year);
}
引起两个错误:
(1)传递参数次序犯错,如:Date d(30, 3, 1995);
(2)传递无效的月份或天数,如 Date d(2, 30, 1995);
解决办法
一、输入区分参数类型
区分三种输入的类型,以不同类的对象区分,这样会因为输入参数类型错误而报错。
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, 1998); //错误,不正确类型
Date d(Day(30), Month(3), Year(1998));//错误,不正确类型
Date d(Month(3), Day(30), Year(1998));//正确,正确类型
explicit可以防止内置类型隐式转换,所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.
问题:类的构造先后问题,解决办法:local static 对象,首次初始化,之后会一直留下来。
限制值,例如月仅有12个合法值,所以 Month 类型应该反映这一点。方法之一是用一个枚举来表现月,但是枚举不具备类型安全性。例如枚举能被作为整数使用。一个安全的解决方案是预先确定合法的 Month 的集合:
class Month {
public:
static Month Jan() { return Month(1); } // 函数而非对象,返回有效月份
static Month Feb() { return Month(2); }
...
static Month Dec() { return Month(12); }
... // 其它成员函数
private:
explicit Month(int m); // 阻止生成新的月份,这是月份专属数据
...
};
Date d(Month::Mar(), Day(30), Year(1995));
static静态对象,首次初始化,之后无须。
二、限定类型的操作const,用户就明白不能更改
预防客户错误的另一个办法是:限制类型内什么事可做,什么事不能做。常见的限制是加上const。
三、动态内存用tr1::shared_ptr,消除用户的资源管理责任
Investment* createInvestment();//用户忘记使用智能指针就坏了
tr1::shared_ptr<Investment> createInvestment();//先发制人,强制返回shared_ptr
四、返回一个将getRidOfInvestment绑定为删除器的tr1::shared_ptr
tr1::shared_ptr<Investment> createInvestment() {
tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVla = ...; //令retVal指向正确的对象。
return retVal;
}
static_cast强制将int 类型的0转型为Investment*类型。
五、tr1::shared_ptr自动使用它的“每个指针专属的删除器”
之前潜在问题:cross-Dll problem:对象在一个dll中被new,却在另一个dll内被delete。在许多平台上,这一类“跨dll的new/delete成对运用”会导致运行期错误。
tr1::shared_ptr是一个消除某些客户错误的简单方法,值得我们核计其使用成本。最通用的 tr1::shared_ptr 实现来自于 Boost,其shared_ptr的大小是原始指针的两倍,以动态分配内存用于簿记用途和deleter专属数据,当调用它的deleter时使用一个virtual函数来调用,并在多线程程序修改引用次数时蒙受线程同步化的额外开销(你可以通过定义一个预处理符号来使多线程支持失效。)。在缺点方面,它比一个原始指针大且慢,而且要使用辅助动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。
条款20 宁以pass by reference-to-const 替换 pass by value
总结:
1、pass by reference-to-const 替换 pass by value。前者使用效率高,且可避免切割问题。
2、以上规则不适用于内置类型、以及STL的迭代器和函数对象。对他们而言,pass-by value 效率更高。
正确方式:
bool validate(const Student& s);//pass by reference-to-const
介绍两种pass by reference 的优点:
一、减少调用构造函数、析构函数
//基类
class Person
{
public:
Person();
virtual ~Person();
}
//派生类
class Student:public Person
{
public:
Student();
~Student();
}
bool validate(Student s);// pass by value
bool validate(const Student& s);//pass by reference-to-const
第一种:需要一次Student copy构造函数、一次Person copy构造函数;以及对应的析构函数。
第二种:没有任何构造函数或析构函数被调用。效率最高。
bool validate(const Student& s);//pass by reference-to-const
二、避免Slicing(对象切割)问题:只留下基类对象
即:pass by value 输入的是一个派生类对象,被视为基类对象。派生类的特征信息会被切除。
class Window{};
class Window1:public Window{};
void printName(Window w){};//成员函数里是基类对象
Window1 wwsb;
printName(wwsb);//输入派生类对象,被识别为基类,切除派生类的特化信息。
虽然wwsb是派生类对象,但是在printName()函数中,wwsb被识别为基类。
解决办法:pass by reference-to-const
void printName(const Window& w){};
根据输入参数类型,都能够识别出来。
三、内置类型、以及STL的迭代器和函数对象,pass-by value 效率更高
1、pass-by-reference 往往是以指针实现出来,真正传递的是指针?
2、内置类型、以及STL的迭代器和函数对象,pass-by value 效率更高?
条款21 必须返回对象时,别妄想返回其reference
总结:
该返回对象时,就返回对象,不要必须返回reference , 下面是三种想要返回reference的不好行为。
1、绝不要返回一个local 栈对象的指针或引用;
2、绝不要返回一个被new 分配的堆对象的引用;
3、绝不要返回一个局部对象有可能同时需要多个这样的对象的指针或引用。
下面用operator*来举例子说名这个问题。因为operator返回的一定是对象而非引用。正确的做法:
inline const Rational operator*(const Rationa& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
一、一定要避免传递一些references去指向其实并不存在的对象。
考虑一个用以表现有理数的类,包含一个函数计算两个有理数的乘积:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);//构造函数
private:
int n, d; // 分子与分母
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};
这里返回对象,需要付出构造函数和析构函数的成本。如果你能用返回一个引用来代替,就不需付出代价。
这里先终点强调一句话:所谓的引用只是一个名称,代表某个已经存在的对象,任何时候看到一个reference声明式,你都应该立刻问自己,他的另外一个名称是什么?因为他一定是某物的另外一个名称。
加入operator返回了一个引用
friend const Rational& operator*(const Rational& s1, const Rational& s2);
Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c should be 3/10
这就意味着一定存在一个对象保存的是两个对象的计算结果,在operator*调用之前提前存在几乎是不可能的事情,所以就需要我们自己在函数中创造一个这样的对象了。
二、在stack栈中创建对象(定义local变量):
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //更糟的代码!
return result;
}
错误!!!
而local对象在函数退出前被销毁了。
三、在heap堆中分配对象:
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?
四、函数内部的局部static 对象
friend const Rational& operator*(const Rational& s1, const Rational& s2)
{
static Rational result;
result = s1*s2;
return result;
}
当用户写下如下代码时:
bool operator=(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
if ((a*b) == (c*d)){
...
}else{
...
}
表达式if ((a*b) == (c*d))总是被核算为true, 不论a,b, c,d是什么。因为虽然两次调用operator*都改变了static Rational对象的值, 但是返回的reference, 调用端看到的永远是static Rational的现值。
正确的做法:
五、一个“必须返回新对象”的函数的正确写法:就让那个函数返回一个新对象
inline const Rational operator*(const Rationa& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
该函数返回的仍然是一个对象,在函数定义阶段定义一个新的对象且直接return。避免像二中函数定义内先创建局部对象result,再return result(局部对象在函数推出前已销毁了)
结论:operator*函数只能返回一个对象,不能返回引用,
条款22 将成员变量声明为private
总结:
1、切记声明数据成员为private。它为客户提供了访问数据的一致,细微划分的访问控制,允许约束条件获得保证,而且为类的作者提供了实现上的弹性。
2、 protec并不比public更具有封装性。
不该讲成员变量声明为public的三个理由:
一、语法一致性
成员变量是private,客户只能通过成员函数访问该变量。会一直需要小括号()。frame.getCurrentR();
二、精准控制成员变量
如果我们令成员变量为public,那么每个人都可以读写它!
但如果以函数取得成员变量,就可以实现出不准访问”、“只读访问”以及“读写访问”,我们甚至可以实现“惟写访问”。
class AccessLevels {
public:
//成员noAccess无任何访问动作,实现不准访问!
int getReadOnlay() const {return readOnly;} //有返回Int,为只读访问!
void setReadWrite(int value){readWrite = value;} //无返回,可写!
int getReadWrite()const {return readWrite;} //有返回int,可读!
void setWriteOnly(int value){writeOnly = value;} //无返回,只写
private:
int noAccess;
int readOnly;
int readWrite;
int writeOnly;
};
三、封装
1、通过函数访问成员变量,日后替换这个成员变量,使用你类的人不会察觉。将成员变量隐藏(封装)在函数接口后面,给日后“所有可能的更改”提供弹性。
2、类class的约束条件总能被维持,只有成员函数能够影响它们。
3、保留了日后变更实现的权利,否则public 取消,所有使用它的客户代码将被破坏。
条款23 宁以non-member、non-friend替换member函数
非成员函数增加封装性、包裹弹性。(成员函数除了访问class的private数据,还可以采用enums,typedefs等,访问的东西多,封装性低)
正确方式:
// 同一namespace
namespace WebBrowserStuff
{
class WebBrowserP{};// 类
void clearBrowser(WebBrowswer& wb);// 类之外的非成员函数
}
一、比较member函数与non-member函数
//定义类
class WebBrowser{
public:
void clear1();
void clear2();
void clear3();
void cleareverything();//第一种member函数
}
void clearbrowser(WebBroeser& wb)//第二种non-member函数
{
wb.clear1();
wb.clear2();
wb.clear3();
}
面向对象守则要求数据尽可能封装。选择第二种non-member函数。
封装:越多东西被封装,我们改变那些东西的能力越大,因为改变仅仅直接影响看到改变的实物。
自然地做法:clearbrowser成为non-member函数且位于WebBrowser所在的同一namespaxe明面空间内
namespace WebBrowserStuff
{
class WebBrowserP{};
void clearBrowser(WebBrowswer& wb);
}
条款24 若所有参数都需类型转换,请采用non-member函数
想要实现下面两种都可以
Rational one(1,8);
Rational result=one*2;
Rational result=2*one;
采用非成员函数operator*,允许编译器在每一个实参身上执行隐式类型转化。
// 类
class Rational{
...};
// 类之外的非成员函数
const Rational operator*(const Rational& lhs,const Rational& rhs){
return Rational(lhs.number()*rhs.number());
}
一、表象
class Rational{
public:
Rational(int numerator=0, int denominator=1);//copy构造函数
int numerator() const;//成员变量访问函数
const Rational operator* (const Rational& rhs) const;//operator*成员函数
private:
...//成员变量
}
当operator*是member成员函数时,两个有理数相乘可以。
Rational one(1,8);
Rational two(1,2);
Rational result=one*two;//成功
但是混合式运算,是失败的
result=one*2;//成功
result=one.operator*(2);//one是一个内含operator*函数的class的对象
result=2*one;//失败
result=2.operator*(one)//2不能调用operator*函数
二、分析(隐式类型转换)
混合式运算的result=one.operator*(2)发生了隐式类型转换,输入的int,隐式转化为了Rational& rhs类型。
只有参数列为参数列内,这个参数才是隐式类型转化和合格者。
三、正确做法
让operator*成为一个non-member函数,允许编译器在每一个实参身上执行隐式类型转化。
class Rational{
...};
const Rational operator*(const Rational& lhs,const Rational& rhs){
return Rational(lhs.number()*rhs.number());
}