4 设计与声明
条款 18 :让接口容易被正确使用,不易被误用
// 设计一个日期的 class 构造函数
class Date
{
public:
Date(int month, int day, int year);
~Date();
...
};
// 错误使用示例1
Date d(30, 3, 1995); // 应该是 3,30 而不是 30,3
// 错误使用示例2
Date d(2, 30, 1995); // 应该是 3,30 而不是 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();
...
};
Date d(30, 3, 1995); // 错误!不正确的类型
Date d(Day(30), Month(3), Year(1995)); // 错误!不正确的类型
Date d(Month(3), Day(30), Year(1995)); // ok,类型正确
// 令 Day, Month 和 Year 成为成熟且经充分锻炼的 classes 并封装其内部数据,比简单使用上述的structs 好
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));
请记住:
- 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::shared_ptr 支持定制型删除器。这可防范DLL(动态链接库程序)问题,可被用来自动解除互斥锁等等。?
?条款 19 :设计 class 犹如设计 type
如何设计高效的classes呢?首先你必须了解你面对的问题。几乎每一个class都要求你面对以下提问,而你的回答往往导致你的设计规范:
- 新 type 的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新 type 的对象如果被 passed by value (以值传递),意味着什么?
- 什么是新 type 的“合法值”?
- 你的新 type 需要配合某个继承图系(inheritance graph)吗?
- 你的新 type 需要什么样的转换?
- 什么样的操作符和函数对此新 type 而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新 type 的成员?
- 什么是新 type 的“未声明接口”?
- 你的新 type 有多么一般化?
- 你真的需要一个新 type 吗?
请记住:
- class 的设计就是 type 的设计。在定义一个新的 type 之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
条款 20 :宁以 pass-by-reference-to-const 替换 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;
};
// 以 by value 方式定义形参
bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);
// 以 pass by reference to const 方式
bool validateStudent(const Student& s)
// 好处:1. 回避多余的构造和析构动作;
// 2. 避免slicing(对象切割)问题
// 示例2:一组图形窗口系统类
class Window
{
public:
...
std::string name() const;
virtual void display() const;
};
class WindowWithScrollBars: public Window
{
public:
...
virtual void display() const;
};
// 错误示范
void printNameAndDisplay(Window W) // 不正确!参数可能被切割
{
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb); // 会造成派生类的特化信息被切除
// 正确范例
void printNameAndDisplay(const Window& w)
{
std::cout << w.name();
w.display();
}
请记住:
- 尽量以 pass by reference to const 替换 pass by value. 前者通常比较高效,并可避免切割问题。
- 以上规则并不适用与内置类型,以及 STL 的迭代器和函数对象。对它们而言,pass by value 往往比较适当。
条款 21 :必须返回对象时,别妄想返回其 reference?
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1);
...
private:
int n, d; // n (numerator):分子,m(denominator):分母
friend const Rational operator*(
const Rational& lhs, const Rational& rhs);
};
// 不正确示范1
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 警告!糟糕的代码!
return result;
}
// 不正确示范2
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 警告!更遭的写法
return *result;
}
// 不正确示范3
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = ...;
return result;
}
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a * b) == (c * d)) {
...
} else {
...
}
// 不可!因为表达式((a * b) == (c * d))总是被核算为true, 不论 a, b, c, d的值是什么!
// 一旦将代码重新写未等价的函数形式,很容易就可以了解出了什么意外:
if (operator==(operator*(a, b), operator*(c, d)))
{
/* code */
} // 虽然都对 static Rattional 对象值做了改变,但是因为都是 reference, 所以调用端看到的永远是该对象的“现值”
// 正确做法是:就让那个函数返回一个新对象
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
请记住:
- 绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回 reference 指向一个 local static 对象”提供了一份设计实例。
条款 22 :将成员变量声明为 private?
// 使用函数可以让你对成员变量的处理有更精确的控制
class AccessLevels
{
public:
...
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int onAccess;
int readOnly;
int readWrite;
int writeOnly;
};
// 封装性
/*
* protected 成员变量的封装性是不是高过public成员变量?答案令人惊讶:并非如此。
* 假设我们有一个 protected 成员变量,而我们最终取消了它,有多少代码被破坏?唔,所有使用它的
* derived classes 都会被破坏,那往往是个不可知的大量。 因此,protected 成员变量就像public
* 成员变量一样缺乏封装性。
*/
// 从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
请记住:
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected 并不比 public 更具封装性。
条款 23 :宁以non-member、non-friend 替换 member 函数?
class WebBrowsesr
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
// 许多用户会想一整个执行所有动作,因此 WebBrowsesr 也提供这样一个函数
class WebBrowser
{
public:
...
void clearEverything();
...
};
// 当然,这一机能也可以由一个 non-member 函数调用适当的 member 函数而提供出来
void clearBrowser(WebBrowsesr& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
// 那么,哪一个比较好呢?
/*
* 很显然,是后者。原因在于 non-memeber non-friend 函数不会增加能够访问class内之private
* 成分的函数数量。
*/
// c++ 中比较自然做法是让 clearBrowser 成为一个 non-member 函数并且位于 WebBrowser 所在的
// 同一个 namespace(命名空间)内:
namespace WebBrowserStuff {
class WebBrowser
{
public:
WebBrowser();
~WebBrowser();
...
};
void clearBrowser(WebBrowser& wb);
...
}
// 头文件 “webbrowser.h” —— 这个头文件针对 class WebBrowser 自身及 WebBrowser 核心机能
namespace WebBrowserStuff {
class WebBrowser
{
public:
WebBrowser();
~WebBrowser();
...
};
... // 核心机能,例如几乎所有客户都需要的 non-member 函数
}
// 头文件“webbrowserbookmarks.h”
namespace WebBrowserStuff {
... // 与书签相关的便利函数
}
// 头文件“webbrowsercookies.h”
namespace WebBrowserStuff {
... // 与 cookie 相关的便利函数
}
// 以此种方式切割机能并不适用于 class 成员函数,因为一个 class 必须整体定义,不能被分割为片片段段
请记住:
- 宁可拿 non-member non-friend 函数替换 member 函数。这样做可以增加封装性、包裹弹性和机能扩充性。
条款 24 :若所有参数皆需类型转换,请为此采用 non-member 函数?
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
private:
...
};
// 示例1 假设 operator* 写成 Rational 成员函数
class Rational
{
public:
...
const Rational operator*(const Rational& rhs) const;
};
Rational oneHalf(1, 2);
Rational result;
result = oneHalf * 2; // ok
result = 2 * oneHalf; // error
// 示例2 让 operator* 成为一个 non-member 函数
class Rational
{
public:
... // 不包括 operator*
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
请记住:
- 如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是一个 non-member.
条款 25 :考虑写出一个不抛出异常的 swap 函数
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
// pimple 手法:pointer to implementation,以指针指向一个对象,内含真正数据
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;
};
// 1. 提供一个成员 swap 函数
class Widget
{
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
...
};
// 2. 特化版 swap函数,调用成员 swap 函数
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
// 假设 上述类都为类模板
template<typename T>
class WidgetImpl
{
public:
WidgetImpl();
~WidgetImpl();
...
};
template<typename T>
class Widget
{
public:
Widget();
~Widget();
...
};
// 此时可将其类声明和 non-member swap 函数置于 WidgetStuff 内
namespace WidgetStuff {
... // 模板化的 WidgetImpl 等等
template<typename T>
class Widget { ... }; // 同前,内含 swap 成员函数
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b); // non-member 函数
}
}
请记住:
- 当 std:: swap 对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个 member swap,也该提供一个non-member swap 用来调用前者。对于classes(而非 templates),也请特化 std::swap.
- 调用swap时应针对 std::swap 使用 using 声明式,然后调用swap并且不带任何“命令空间资格修饰”。
- 为“用户定义类型”进行std templates 全特化是好的,但千万不要尝试在std 内加入某些对std 而言全新的东西。
|