4 设计与声明
条款 18 让接口容易被正确使用
第一点 设计接口的时候,应该考虑用户会做出什么样的错误。
? 对于一些有特定含义的参数,可以使用外覆类型(wrapper types)来区别参数。
例 :
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);
...
};
? 也可以使用自定义的类来限制该类的范围和操作。
例
class Month{
public:
static Month Jan() { return Month(1); }
...
static Dec() { return Month(12); }
private:
explicit Month(int m);
...
};
? 将构造函数定义在 private 中,防止用户创建新对象。
第二点 让 type 容易被正确使用,不容易被误用。
? 应该要让自定义的 types 的行为和内置的 types 一致,特殊情况除外,这样叫保持一致性。
第三点 防止用户有“不正确使用”的倾向。
? 简单来说就是,应该要防止用户因为忘记而做出正确的事,例如要防止客户忘记使用 delete 释放内存,我们应该主动将分配的指针存储在智能指针之中。
请记住 :
? 1. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
? 2. “促进正确使用”的方法包括接口的一致性,以及与内置类型的行为兼容。
? 3. “阻止误用”的方法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
? 4. shared_ptr 支持定制兴删除器,这可防范 DLL 问题(对象在一个 DLL 中被 new ,在另一个 DLL 中被 delete),可被用来自动解除互斥锁。
条款 19 设计 class 犹如设计 type
? 设计 classes 面对的问题 :
- 新 type 的对象应该如何被创建和销毁? 设计到构造函数、析构函数、内存分配释放函数。
- **对象的初始化和对象的赋值该有什么样的区别? ** 初始化是构造新对象时给定初值,赋值是在构建完对象后给定值。
- 新 type 的对象如果被 passed by value ,意味着什么? 与 copy 构造函数有关。
- 什么是新 type 的合法值? 对于成员变量而言,如果存在一定的约束条件,如前面的 Month 类,必须在相关的构造函数、赋值操作符等涉及到该限制的成员函数中进行范围检查,错误检查。
- **你的新 type需要配合某个继承图系吗? ** 若是则要考虑 virtual 和 non-virtual 的问题,尤其是析构函数。
- 你的新 type 需要什么样的转换? 显式转换还是隐式转换,需不需要 explicit。
- 什么样的操作符和函数对此新 type 而言是合理的? 哪些应该是 member 哪些是 non-member。
- 什么样的标准函数应该驳回? 可声明为 private 或 =delete 禁止自动生成。
- 谁该取用新 type 的成员? 哪些为 public ? 哪些为 protected ? 哪些为 private ? 哪些应该为 friends。
- 什么是新 type 的 “未声明接口”? 见条款 29。
- 你的新 type 有多么一般化? 可以考虑 class template
- 你真的需要一个新 type 吗? 如果只是定义新的 derived class 以便为既有的类增加新机能,不妨考虑定义一个或多个 non-member函数或 templates 。
请记住 :
? 1. Class 的设计就是 type 的设计。在定义一个新 type 之前,请确定你已经考虑以上条例。
条款 20 宁以 pass-by-reference-to-const 替换 pass-by-value
1. pass-by-reference-to-const 可以使用 C++ 的多态性。
2. pass-by-reference-to-const效率更高,pass-by-value 会复制一个副本,这增大了开销。
3. 当 pass 的对象是内置类型时,pass-by-value 并不昂贵。
请记住 :
? 1. 尽量以 pass-by-reference-to-const 替换 pass-by-value 。前者通常比较高效,并可以避免切割问题。
? 2. 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对它们进行 pass-by-value 更加恰当。
条款 21 必须返回对象时,别妄想返回其 reference
? 其实,当你想让返回值为 reference 时,你应该时希望,在返回值时避免不必要的构造开销,因为返回一个 local object 其实是返回其副本,我们需要调用其复制构造函数。
? 但其实除了类的 member 方法,而且是返回类的成员变量,以及与之类似的返回 non-local 变量的函数才应该返回 reference ,可能概括的有点不太准确。但想要说的是,当我们返回的是一个函数内的局部变量,应该避免返回值为 reference ,因为无论是在 stack 还是在 heap 上构建的对象,前者会因为函数结尾局部变量被释放而导致返回值(reference)指向一个被释放的值,后者会因为在函数内使用 new 且为在函数结束时 delete 掉,容易导致这种没有 delete 的指针满天飞,难以察觉。
? 总的来说,就是当你面临“返回 reference 还是 object ?”时,不要一味追求性能,首先要保证行为正确。
*请记住 : *
? 1. 绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。条款4已经为 “在单线程环境中合理返回 reference 指向一个local static 对象提供了一份设计实例。(Singleton 模式)”
条款 22 将成员变量声明为 private
理由 :
- 语法一致性,统一将成员变量声明为 private ,将成员函数声明为 public,可以让每一个人都知道,我们访问对象时,只能使用其函数,也就是要加括号,这样就不需要用户事先了解哪些是变量,哪些是函数了。
- 精准控制,如果说我们希望用户使用对象的成员,我们可以随意设定成员变量的访问权限。
例
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 noAccess;
int readOnly;
int readWrite;
int writeOnly;
};
? 我们可以通过定义相关的成员函数,进而对成员变量的访问权进行变换,并不是说成员变量声明在 private 内,外部就无法访问了。
- 封装,面向对象的初衷就是对用户隐藏成员变量,这样可以防止用户私自更改对象内部变量,造成代码的破坏。当然代码的封装性与成员变量的内容改变时所破坏代码的数量成反比,因此 protected 并不比 public 更具有封装性。
*请记住 : *
? 1. 切记将成员变量声明为 private 。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
? 2. protected 并不比 public 更具封装性。
条款 23 宁以 non-member、non-friend 替换 member 函数
? 我们接着讨论封装性,这可以帮助我们更好地在 non-member、non-friend 、member 函数中进行抉择。
例
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
void clearAll();
...
};
void WebBrowser::clearAll()
{
clearCache();
clearHistory();
removeCookies();
}
void clearAll(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
? 我们来讨论哪个 clearAll() 好呢?
? 首先,愈多的东西被封装,愈少的人看见它,我们就有愈大的弹性区改变它。显然,成员方法版本的明显增加了能访问该类的函数,因此其封装性更差。
? 以上讨论仅限于 non-member non-friend 函数。
? 通常为了将这一类的 non-member non-friend 函数与类“绑定起来”,可以使用 namespace。
例
namespace WebBrowserStuff {
class WebBrowser {...};
void clearAll(WebBrowser& wb) {...}
}
而且当我们有大量的 non-member non-friend 函数时,为了降低编译依存性,我们可以将这些函数分别在不同的头文件的同一个命名空间中定义,这设计到了 namespace 的扩展性。
例
//webbrowswe.h
namespace WebBrowserStuff {
class WebBrowser {...};//核心类
//non-member non-friend 函数
}
//webbrowserbbookmarks.h
namespace WebBrowserStuff {
//与bookmarks相关的non-member non-friend 函数
}
//webbrowsercookies.h
namespace WebBrowserStudd {
//与cookies相关的non-member non-friend 函数
}
请记住 :
? 1. 宁可拿 non-member non-friend 函数替换 member 函数。这样做可以增加封装性,包囊弹性和机能扩充性。
条款 24 若所有参数皆需类型转换,请为此采用 non-member 函数
? 没什么好讲的,因为 this 对象作为调用成员函数的那个对象时,无法进行隐式转换。
例
class Rational{
public:
...
const Rational operatotr*(const Rational &rhs) const;
...
};
Rational a,b,c;
a = b * c;//成功 b.operator*(c);
a = b * 2;//成功 b.operator*(Rational(2));
a = 2 * c;//失败
//正确做法 non-member 函数
const Rational operatotr*(const Rational &lhs,const Rational &rhs)
{
...
}
请记住 :
? 1. 如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。
考虑写出一个不抛出异常的 swap 函数
class WidgetImpl{
public:
...
private:
int a,b,c;
...
};
class Widget{
public:
...
private:
WidgetImpl *pImpl;
};
std::swap(Widget ¶m1,Widget ¶m2) 可以交换两个参数,如果考虑自己实现,我们肯定是想着交换两个参数内部的 pImpl 指针,而不是其所指的东西,这样开销太大。很可惜的是,std::swap 并不会这样干。
? OK,我们的目标是实现这样一种特化的情况。
首先,考虑到要交换 Widget 内部的 pImpl 指针,我们必须使用成员函数或者 friend 函数,我们这里使用 member 函数
namespace WidgetStuff {
class Widget{
public:
...
void swap(Widget &rhs)
{
using std::swap;
//暴露标准版本的swap,保证其最佳匹配时能够找到
//C++名称查找法则会在该类所属名字空间中对应的“东西“以及该文件中的global “something“
//而不会跑去namespace std中查找,因此必须使用using将其暴露出来
swap(pImpl,rhs.pImpl);
}
private:
WidgetImpl *pImpl;
};
void swap(Widget &w1,Widget &w2)
{
w1.swap(w2);
}
}
//这样,你就可以在一些用到交换的场合(如copy assignment operator)使用以上函数
template <typename T>
void doSomething(T &o1, T &o2)
{
using std::swap;//若T为一般类型,即非pimpl类型,可以正常使用std::swap
swap(o1, o2);
}
//以上可推广到 template class
若缺省版本的 swap 实现效率太低,可以 :
- 提供一个 public swap 成员函数,让他高效地进行置换。
- 在该 class 或 template 所在的命名空间内提供一个 non-membe swap,并令它调用上述的 swap 成员函数。
- 如果你在编写一个 class(非 class template),为你的类特化 std::swap ,并令他调用你的 swap 成员函数。
最后,如果你调用 swap ,请确保使用一个 using 声明式,一遍让 std::swap ,然后赤裸裸地调用 swap 。
请记住 :
? 1. 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
? 2. 如果你提供一个 member swap ,也应该提供一个 non-member swap 用来调用前者。对于 classes(非 template),也请特化 std::swap。
? 3. 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何“命名空间资格修饰”。
? 4. 为“用户定义类型”进行 std template 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
|