设计与声明
条款18:让接口容易被正确使用,不易被误用
假设为一个用来表现日期的class设计构造函数:
class Date {
public:
Date(int month,int day,int year);
...
};
Date d(30,3,1995); //错误,应该是"3,30"而不是"30,3"
Date d(2,30,1995); //错误,2月没有30天
以上两种初始化虽然语法上没错误,但是并不符合客观事实。
以上错误可以通过导入新类型而得到预防:
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); //错误,不正确的参数类型
Date d(Day(30),Month(3),Year(1995)); //错误,不正确的参数类型
Datr d(Month(3),Day(30),Year(1995)); //正确,类型正确
一旦正确的类型就定位,有时候需要限制其值。例如一年只有12个月份,所以Month应该反应这个事实。
比较安全的方法就是预先定义所有有效的Months:
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));
在条款13中导入了一个函数,它返回一个指针指向Investment继承体系内的一个动态分配对象:
Investment* createInvestment();
为避免资源泄漏,createInvestment返回的指针必须被删除。
较佳接口的设计原则是先发制人,令函数返回一个智能指针:
std::tr1::shared_ptr<Investment> createInvestment();
假设设计者期许这个返回的智能指针被传递给一个名为getRidOfInvestment的函数.
则可以:返回一个将getRidOfInvestment绑定为删除器的tr1::shared_ptr
std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);
//通过cast转型建立一个null shared_ptr,并以getRidOfInvestment为删除器
当然,如果被pInv管理的原始指针在建立pInv之前先确定下来,那么“将原始指针传给pInv构造函数”会比“先将pInv初始化为null再对它做一次赋值操作” 要好。
请记住:
-好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这些性质。
-“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
-“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
-tr1::shared_ptr支持定制型删除器.这可防范DLL问题,可被用来自动解除互斥锁。
条款19:设计class犹如设计type
如何设计高效的classes呢?要面对以下的问题:
-新type的对象应该如何被创建和销毁? 这会影响到构造函数和析构函数以及内存分配函数和释放函数的设计。
-对象的初始化和对象的赋值该有什么样的差别? 这决定构造函数和赋值操作符的行为以及其间的差异。
-新type的对象如果被passed by value(以值传递),意味着什么?
-什么是新type的合法值? 对class的成员变量而言,通常只有某些值是有效的。
-你的新type需要配合某个继承图系吗? 若继承自某些base classes,就会受到那些classes的设计的束缚。virtual和non-virtual
-你的新type需要什么样的转换?
-什么样的操作符和函数对此新type而言是合理的? 这决定你将为class声明哪些函数。
-什么样的标准函数应该驳回? 这正是必须声明为private者
-谁该取用新type的成员? 这帮助你决定哪个成员是public,哪个是protected,哪个是private.
-什么是新type的"未声明接口"?
-你的新type有多么一般化?
-你真的需要一个新type吗? 如果只是定义新的drived class以便为既有的class添加机能,那么单纯定义一个或多个non-member函数或template更能达到目标。
条款20:宁以pass-by-reference-to-const替换pass-by-value
pass-by-value是昂贵的操作:
现在考虑以下代码,调用函数validateStudent, 需要一个Student实参(by value)并返回它是否有效
bool validateStudent(Student s); //函数以by value方式接受学生
Student plato;
bool platoIsOK = validateStudent(plato);
对此函数而言,参数的传递成本是”一次Student copy构造函数调用,加上一次Student析构函数调用。
pass-by-reference-to-const:
bool validateStudent(const Student& s);
这种传递方式效率高得多:没有任何构造函数和析构函数被调用,因为没有任何新对象被创建。
以by reference方式传递参数可以避免对象切割问题。
假设一组classes用来实现一个图形窗口系统:
class Window {
public:
...
std::string name() const; //返回窗口名称
virtual void display() const; //显示窗口和其类型
};
class WindowWithScrollBars: public Window {
public:
...
virtual display() const;
};
display()是个virtual函数,意味和基类和继承类中的display()不同
假设希望写个函数打印窗口名称,然后显示该窗口:
void printNameAndDisplay(Window w) //参数可能被切割
{
std::cout << w.name();
w.display();
}
当调用上述函数并传递给它一个WindowWithScrollBars对象
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
因为函数是pass by value,所以参数都会被构造为一个WIndow对象,不论传递的对象原来是什么类型,这就产生了切割问题。
因此在printNameAndDisplay内调用display调用的总是Window::display.
解决切割问题的办法,就是以by reference-to-const的方式传递w:
void printNameAndDisplay(const Window& w) //参数不会被切割
{
std::cout << w.name();
w.display();
}
现在,传进来的窗口是什么类型,w就表现出那种类型。
reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。
若对象是内置类型,pass by value往往比pass by reference的效率高些。
一般而言,pass by value的对象就是内置类型和STL的迭代器和函数对象,其他任何东西尽量以pass-by-reference-to-const替代pass by value.
请记住:
-尽量以pass-by-reference-to-const替换pass-by-value.前者往往比较搞笑,并可避免切割问题。
-以上规则不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value比较适当。
条款21:必须返回对象时,别妄想返回其reference
-绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象“提供了一份设计实例。
条款22:将成员变量声明为private
为什么将成员变量声明为private:
-语法一致性:客户唯一能够访问对象的办法就是通过成员函数。
-使用函数可以让你对成员变量的处理有更精确的控制。
-封装!
将成员变量隐藏在函数接口的背后,可以为”所有可能的实现“提供弹性。
例如这可使得成员变量被读或被写时轻松通知其他对象、可以验证class的约束条件以及函数的前提和事后状态,可以在多线程环境中执行同步控制...等等。
假设取消了一个public成员变量,所有使用它的客户码都会被破坏,那是一个不可知的大量。假设去下了一个protected成员变量,所有使用它的drived class都会被破坏,那也是不可知大量。 protected 和 public一样缺乏封装性。
从封装的角度讲,只有两种访问权限:private(提供封装),和其它(不提供封装)
请记住:
-切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的弹性实现。
-protected并不比public更具封装性。
条款23:宁以non-member、non-friend替换member函数
有个class表示网页浏览器,其含有众多函数,有清除下载告诉缓冲区,清除访问过的URL的历史记录,以及移除系统中的所有cookies:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
//使用一个函数执行所有动作
class WebBrowser {
public:
...
void clearEverything(); //调用三个函数
...
};
// 这一机能也可由一个non-member函数调用适当的member函数而提供出来:
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory;
wb.removeCookies();
}
non-member函数和non-friend函数封装性比member函数好!
clearBrowser导致WebBrowser class有较大的封装性。
一个像WebBrowser的class可能拥有大量便利函数,某些与书签相关,某些与打印相关,还有一些和cookie的管理有关...
分离它们最直接的做法就是将书签相关的便利函数声明与一个头文件中,将cookie相关的便利函数声明于另一个头文件中,将与打印相关便利函数声明与第三个头文件。依次类推:
//头文件webbrowser.h-这个头文件针对class WebBrowser本身及WebBrowser的核心机能
namespace WebBrowserStuff {
class WebBrowser { ... };
... //核心机能,例如几乎所有客户都需要的non-member函数
}
//头文件webbrowserbookmarks.h
namespace WebBrowserStuff {
... //与书签相关的便利函数
}
//头文件webbrowsercookies.h
namespace WebBrowserStuff {
... //与cookie相关的便利函数
}
...
将所有便利函数放在多个头文件但隶属同一个命名空间,意味可以轻松扩展这一组便利函数。需要做的就是添加更多non-member non-friend函数到此命名空间。
请记住:
-宁可拿non-member non-friend函数替换member函数,这样做可以增加封装性,包裹弹性和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
请记住:
-如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
条款25:考虑写出一个不抛出异常的swap函数
所谓swap(置换)两对象值,就是将两对象的值彼此赋予对方。典型实现:
namespace std {
template<typename T> //std::swap的典型实现
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
其中最主要的就是”以指针指向一个对象,内含真正数据“那种类型。设计Widget class:
class WidgetImpl {
public:
...
private:
int a,b,c; //可能有很多数据
std::vector<double> v;
}
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //复制Widget时,令它复制其WidgetImpl对象
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl* pImpl; //指针,所指对象内含Widget数据
};
一旦要置换两个Widget对象值,唯一需要做的就是置换其pImpl指针,但缺省的swap算法不知道这一点,它不仅复制三个Widget,还复制WidgetImpl对象。非常缺乏效率。
将std::swap针对Widget特化。下面是基本构想:
namespace std {
template<> //这是std::swap针对T是Widget的特化版本,目前还不能通过编译。
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); //置换Widget&时只要置换它们的pImpl指针就好
}
}
函数开始的template<>表示它是std::swap的全特化版本,函数名称之后的<Widget>表示这一特化版本针对T是Widget设计的。
这个函数无法通过编译,因为它企图访问a和b的pImpl指针,那却是private。
我们令Widget声明一个名为swap的public成员函数做真正的置换工作,然后std::swap特化,令它调用该成员函数:
class Widget {
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl,other.pImpl); //若要置换Widget&就置换其pImpl指针
}
...
};
namespace std {
template<>
void swap<Widget> (Widget& a, Widget& b)
{
a.swap(b); //若要置换Widget,调用其swap成员函数
}
}
这种做法不只能够通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swao特化版本。
请记住:
-当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
-如果你提供一个member swap,也该提供一个non-member swap用来调用前者,对于classes,也请特化std::swap
-调用swap时应针对std::swap使用using声明式,然后调用swao并且不带任何命名空间资格修饰。
-为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加上某些对std而言全新的东西。