条款18
设计出“容易使用,不容易被误用”的接口
例如设计一个Date类
class Date {
public:
// 因为三个成员的类型相同,就很容易被误用,不是所有人都会按照 月 日 年 来传入参数
// 并且 年 月 日 都有明显的界限,若超过这个界限就是不合适的,我们设计接口时应该 使用户不会出现这种情况,不能寄希望于用户自己
Date(int _month, int _day, int _year) : month(_month), day(_day), year(_year) {}
private:
int day;
int month;
int year;
};
因此此时我们需要对导入的新类型的错误进行预防。
所以我们要对这些数据进行封装
struct Day {
explicit Day(int _val) :val(_val){}
int val;
};
struct Month {
explicit Month(int _val) :val(_val){}
int val;
};
struct Year {
explicit Year(int _val) :val(_val){}
int val;
};
class Date {
}
条款19
设计新的对象的需要考虑的点:
- 是否真的需要一个新对象?
- 新对象应该如何被创建和销毁,在涉及到
new和delete, new[]和delete[]
时尤其如此 - 对象的初始化和赋值应该有什么差别?
- 新对象若被值传递,意味着什么?拷贝构造函数用来定义一个类的值传递该如何实现
- 什么是新对象的“合法值”?通常有些数值集是无效的,如上面的日期类,此时类必须维护一些约束条件,此时成员函数(特别是构造函数、赋值操作和所谓的“setter”函数)必须进行错误检查工作。
- 新对象需要配合某个继承图谱吗?
- 新的对象需要转换行为吗?
- 新的对象需要什么合法的操作符和函数
- 成员变量的访问权限
- 新对象的“未声明接口”?对效率、异常安全性和资源运用(多任务锁定和动态内存)提供何种保证
- 新类型有多么一般化,到底是设计为
class
还是设计为class template
条款20
最好以 past-by-reference-to-const
替换 pass-by-value
,用 const引用传递
替换 值传递
- 引用传递可以实现多态
- 可以减少对象的构造次数和成员变量构造函数的执行次数,效率更好
- 内置类型一般使用值传递效果更高
条款21
const引用
使用比较方便,但是并不是所有的情况都要使用 const引用
, 当必须返回对象时,别妄想返回其引用
不要使用指针或引用指向一个局部变量,或者引用指向一个堆上定义的对象,或返回指针或引用指向一个静态局部变量,并且可能同时需要多个这样的对象。
总之,必须返回返回对象时,别偷懒。
class Rational {
// 此处一般来说必须返回对象本身,而不能返回引用
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Raional(lhs.denominator * rhs.denominator, lhs.numerator *rhs.numerator);
}
private:
int denominator; // 分母
int numerator; // 分子
};
条款22
成员变量设计为private
- 可以设计相应的
getter
和setter
函数来实现对所需变量的控制 - 将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如可以使成员变量被读或者被写时轻松通知其他对象。可以验证
class
的约束条件以及函数的前提和事后状态。可以在多线程环境中执行同步控制。 - 可以实现封装。封装很重要。若你对客户隐藏成员变量,你可以确保
class
的约束条件总会得到维护,只有成员函数能够影响它们。 public
和protected
成员变量,若我们取消了它。所有使用它的客户代码都会被破坏,这是一个未知的量,。因此public
完全不具有封装性。- 从封装的角度看,只有两种访问权限:
private
提供封装和 其他不提供封装
条款23
/*
TODO
*/
条款24
若所有参数皆需类型转换,采用 non-member
函数
- 采用成员变量时,
this
指向的对象不能进行类型转换 - 类型转换只能发生在参数列表中的对象
class Rational {
const Rational operator*(const Rational &rhs) const; // 针对r * 2可以,但2*r错误,因为2是this,不能类型转化为Rational
};
const Rational operatro*(const Rational &lhs, const Rational &rhs); // 两者都能进行类型转换
条款25
写一个不抛异常的 swap
函数
在赋值运算符中解决自我赋值问题的一个 常见机制
高效的swaps
几乎总是基于对内置类型的操作(例如pimpl
手法的底层指针),而内置类型上的操作绝对不会抛出异常,一次要实现不抛异常的swaps
时需要全部地基于对内置类型的操作。
-
std::swap
效率不高时,需要提供一个swap
函数,并确定该函数不抛出异常。一般这种情况发生在自建对象变量中有指针的情况。此时我们不应该交换指针指向的所有对象内容,而是应该交换指针的指向就好了,这样效率更高,而且指针是内置类型,不会抛出异常
-
提供的
swap
函数有几种形式member swap
,此时还需要提供一个non-member swap
来调用前者non-member swap
- 对于
class
时,可以提供std::swap
的特化版本 - 对于
template
时,不存在std::swap
的特化版本,只能自己实现一个全特化版本,但不能放入std
中
// std 中的swap函数
// 在针对成员变量有指针的对象时,由于会调用复制运算符和拷贝构造函数,会发生大量数据的拷贝,效率很低
namespace std {
tempalte<typename T>
void swap(T &a, T &b) {
T temp(a);
a = b;
b = temp;
}
}
class Widget {
public:
// 成员变量的swap
void swap(Widget &w) {
using std:swap();
swap(propeties, w.propeties);
swap(name, w.name); // 此时只交换指向,而并不是复制每一个元素
}
private:
int propeties;
char *name;
};
// 此时我们希望能够告诉 std::swap,当 Widget被置换时真正应该交换的是其内部的pImpl指针
// 此时可以使用偏特化来实现
// 编译错误
namespace std{
template<>
void swap<Widget> (Widget &a, Widget &b) {
swap(a.propeties, b.properities); // 成员变量是私有变量,我们不能这样使用它们,所以有问题
swap(a.name, b.name);
}
}
// 此时我们可以声明一个名为swap的public成员函数来做真正的置换工作,然后将std::swap特化
// 这种情况可以通过编译,而且与STL具有一致性,STL容器都是这样提供的特化版本
namespace std{
template<>
void swap<Widget> (Widget &a, Widget &b) {
a.swap(b); // 使用成员函数中的swap来真正实现
}
}
// 若Widget是模板类,而不是单个对象
template<typename T>
class Widget { };
// 此时想要将其swap函数放入std中,会出错
// C++只允许对类模板偏特化,而不能在函数模板上实现偏特化
namespace std {
template<typename T>
void swap(T &a, T &b) { // 错误的,不能通过编译
a.swap(b);
}
}
// 此时对于类模板的交换,我们需要声明一个non-member swap来调用member swap,但此时不应该将其声明为std::swap的特化版本或重载版本
template<typename T>
void swap(T &a, T &b) { // 错误的,不能通过编译
a.swap(b);
}
在定义了这么多的swap
后,在使用时应该调用哪个呢?
// 使用using::swap之后,编译器会先寻找当前global作用域或T所在的明明空间中的swap,不存在的化则会对std中的偏特化进行查找,不存在的话才会使用非一般化的template版本
template<typename T>
void doSomething(T &obj1, T &obj2) {
using std::swap;
swap(obj1, obj2); // 将为T调用最佳的swap版本
}
// 注意std::swap(obj1, obj2)与上述不同,它是只会从std中寻找合适的函数,包括新加入的偏特化
总之:
- 实现的
swap
函数不能抛出异常,应该全部实现的是内置类型的交换,效率更高,错误更少 - 要在类的外部实现
swap
函数,实现与所有的STL
容器具有一致性,可以实现同一函数对多个不同的类进行操作 - 若不是模板类,最好将
swap
写入std
中,实现偏特化 - 若是模板类的话,就在当前的作用域中实现一个版本
- 偏特化只能实现类的偏特化,函数不存在偏特化