网站首页 文章专栏 改善程序与设计的55个做法.md
最近在看《Effective C++》这本书,这博客相当于是个浓缩版的总结吧。
在这里你可以大致游览下在 C++ 开发中前人给了我们哪些建议,有机会我觉得最好还是可以看看原书,因为里面会有不少具体的例子告诉你为什么这么做以及这么做的好处。
我们可以视 C++ 为一个由相关语言组成的联邦而非单一语言,例如:
const
,enum
,inline
替换 #define
const
class TextBlock {
public:
const char& operator[] (std::size_t position) const {
// do something
return text[position];
}
char& operator[] (std::size_t position) {
// 调用 const 版本的重载函数,避免代码重复
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
12345678910111213
其中构造函数最好使用使用成员初值列,而不是在构造函数中使用赋值操作。如:
Person::Person(const std::string& name, const int age)
:mName(name), mAge(age) // 成员初始列
{
// mName = name; // 赋值操作
// mAge = age; // 赋值操作
}
123456
编译器会自动给类创建 default 构造函数、copy 构造函数、赋值操作符(operator=)、析构函数。
class Empty {}; // 等同于下面写法
class Empty {
public:
Empty() {} // default构造函数
Empty(const Empty& rhs) {} // copy构造函数
~Empty() {} // 析构函数
Empty& operator=(const Empty& rhs) {} // copy assignment 操作符
};
123456789
将相应的成员函数声明为 private
,并不予实现即可。
如果一个类带有任何 virtual 函数,它都应该拥有一个 virtual 析构函数。
析构函数不要抛出异常。如果析构函数中调用的函数可能抛出异常,那么也要捕捉并吞掉这个异常或结束程序。
在父类构建的过程,virtual 函数还没有下降到子类中去。
*this
的引用operator=
,operator+=
,operator-=
等等赋值运算符,都返回一个 reference to *this。如:
Widget& operator=(const Widget& rhs) {
// do something
return *this;
}
1234
operator=
中处理 “自我赋值”Widget& operator=(const Widget& rhs) {
if (this == &rhs) {
return *this; // 如果是自我赋值,就不做任何事
}
// do something
return *this;
}
1234567
auto_ptr
和 shared_ptr
)来管理资源类,避免你忘记 delete 资源类。例如提供一个 get()
方法获取原始资源。
new
和 delete
时要采取相同形式避免发生异常时,导致难以察觉的资源泄漏产生。如:
std::tr1::shared_ptr<Widget> pWidget(new Widget);
processWidget(pWidget);
// 不建议下面这样做,如果 processWidget 发生异常可能造成泄漏
// processWidget(std::tr1::shared_ptr<Widget> (new Widget));
1234
size
的成员函数返回目前容器的大小。当一个 “必须返回新对象” 的函数,我们就直接返回一个新对象,而不要返回引用或者指针。
private
实现数据的封装性。且可以更方便的控制数据的访问。
如果一个有理数的类 Rational,我们常常喜欢直接使用 int 类型的数和 Rational 对象进行混合运算。那么使用 non-member 函数将是更好的选择,它允许每一个参数都进行隐式类型转换。
class Rational {
public:
Rational(int numerator = 0, int denominator = 1)
: mNumerator(numerator), mDenominator(denominator) {}
int numerator() const { return mNumerator; }
int denominator() const { return mDenominator; }
private:
int mNumerator; // 分子
int mDenominator; // 分母
};
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
int main() {
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
}
123456789101112131415161718192021
当系统的 std::swap
对你的类型效率不高时,提供一个 swap 成员函数,并确保这函数不会抛出异常。
如果提供一个 member swap,也该提供一个 non-member swap 来调用前者。例如:
class WidgetImpl {
public:
// ...
private:
int a, b, c; // 可能有很多数据
std::vector<double> v; // 意味着复制时间很长
// ...
};
// 这个 class 使用 pimpl 手法
// pimpl 是 pointer to implementation 的缩写
class Widget {
public:
Widget(const Widget& rhs) {}
Widget& operator=(const Widget& rhs) {
// ...
*pImpl = *(rhs.pImpl);
// ...
}
// 我们需要交换两个 Widget 对象,但是直接使用 std::swap 将导致很多多余的复制
// 好的做法是只交换两个 WidgetImpl* 指针对象即可,所以我们考虑写一个 swap 方法
void swap(Widget& other) {
using std::swap; // 这个声明至关重要,使得C++编译器能够找到正确的函数调用
swap(pImpl, other.pImpl); // 我们只需要交换 pImpl 即可
}
private:
WidgetImpl* pImpl;
};
// 提供一个 non-member swap 来调用 member swap
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b);
}
}
将一些变量定义式放在常规的参数检查后面,避免无用的构造方法带来的耗费。有助于增加程序清晰度和改善程序效率。
dynamic_cast
。operator[]
操作允许你获取个别元素的引用。inline
里里外外继承是 is-a 关系,指 “是一个”,即父类的每条属性和方法都应该适用于子类。
using
声明或使用 转交函数(forwarding functions)。如:class Base {
public:
void fun1() {
std::cout << "Base fun1" << std::endl;
}
};
class Derived : public Base {
public:
// 1. 使用 using 声明
using Base::fun1;
// 2. 使用转交函数
void fun1() {
Base::fun1();
}
// 同名函数会遮掩掉父类的 fun1()
void fun1(int a) {
std::cout << "Derived fun1: " << a << std::endl;
}
};
1234567891011121314151617181920
当我们为了解决问题而寻找某个设计方法时,可以考虑一些 virtual 函数的替代方案,如:
tr1::function
。任何情况下都不该重新定义一个继承而来的 non-virtual 函数。
// 下面两种写法对编译器来说完全相同
template<class T> class Widget;
template<typename T> class Widget;
这一点我们慢点说,首先解释 “使用 template 标识嵌套从属类型名称”,看下面这个例子:
template<typename T>
void print(const T& container) {
// 这样是不行的
// T::const_iterator iter(container.begin());
// 必须明确告诉 C++ 这个 T::const_iterator 是个类型而不是变量
typename T::const_iterator iter(container.begin());
}
也许我们初衷是获取到集合的迭代器,但是由于泛型 T 可能并不是一个集合,它里面没有一个叫做 const_iterator 类型的嵌套类,编译器也可以把 const_iterator 当作一个类的静态变量,所以这里会存在歧义。因此我们必须使用 typename 告诉 C++ 这是一个类型而不是静态变量。
typename 被用来标识一个嵌套从属类型名称,然而还有一个例外,即不能在基类列或成员初始列内以它作为基类的修饰符。如:
// Base 是一个基类,Nested 是基类的一个嵌套类
template<typename T>
class Derive
: public Base<T>::Nested { // base class list 中不允许 typename
public:
explicit Derive(int x)
: Base<T>::Nested(x) // member initialization list 中不允许 typename
{
typename Base<T>::Nested temp; // 其余情况需要加上 typename
}
};
编译器往往会拒绝在模板化基类中寻找继承而来的名称,因为基类的模板化可能被特化而那个特化版本也许并不会提供某一接口。所以我们可以使用 “this->” 指定,或使用 “using” 告诉编译器假设它存在。
template<typename T>
class Derived : public Base {
public:
// 方式1,告诉编译器 func 位于基类中
using Base<T>::func();
void test() {
// 方式2,假设 func 将被继承
this->func();
}
};
Templates 会生成多个 classes 和函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
// 子类 Y 的智能指针指向父类 T 时就不会出问题了
template<class T>
class shared_ptr {
public:
// copy 构造函数
shared_ptr(shared_ptr const& rhs);
// 泛化 copy 构造函数
template<class Y>
shared_ptr(shared_ptr<Y> const& rhs);
// copy assignment
shared_ptr& operator=(shared_ptr const& rhs);
// 泛化 copy assignment
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& rhs);
};
为了让类型转换可能发生于所有实参上,我们需要一个非成员函数(条款 24)。在模板中,为了令这个函数被自动具现化,我们需要将它声明在类内部。为了在类内部声明非成员函数,唯一的办法是令它成为一个 friend。如:
template<typename T>
class Rational {
public:
// 定义在class内的函数都暗自inline,对于复杂函数而言,我们可以令friend函数调用一个辅助函数避免
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Rational(int numerator = 0, int denominator = 1)
: mNumerator(numerator), mDenominator(denominator) {}
int numerator() const { return mNumerator; }
int denominator() const { return mDenominator; }
private:
int mNumerator; // 分子
int mDenominator; // 分母
};
int main() {
Rational<int> oneFourth(1, 4);
Rational<int> result;
result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
cout << "numerator: " << result.numerator() << endl;
cout << "denominator: " << result.denominator() << endl;
}
例子:
以 iterator_traits 为例介绍如何实现和使用 traits classes。STL 提供了很多的容器、迭代器和算法,其中的 advance 便是一个通用的算法,可以让一个迭代器移动给定距离:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // d < 0 就向后移动
STL 迭代器回顾:
struct input_iterator_tag {}; // 输入迭代器,只能向前移动
struct output_iterator_tag {}; // 输出迭代器,只能向前移动
struct forward_iterator_tag: public input_iterator_tag {}; // 稍强的是前向迭代器,可以多次读写它的当前位置
struct bidirectional_iterator_tag: public forward_iterator_tag {}; // 双向迭代器,支持前后移动
struct random_access_iterator_tag: public bidirectional_iterator_tag {}; // 随机访问迭代器,可以支持 +=, -= 等移动操作
回到 advance 上,它的实现取决于 Iter 类型:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
// 针对 random access 迭代器使用这种方式实现
if (iter is a random access iterator) {
iter += d;
} else {
// 其它迭代器使用 ++ 或 -- 实现
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}
接下来就是怎么判断 Iter 的类型是否是 random access 迭代器了,也就是需要知道它的类型。这真是需要使用到 Traits classes 的地方。
实现 Traits classes:
用户自定义类型:
template<typename IterT>
struct iterator_traits {
// 类型 IterT 的 iterator_category 就是用来标识迭代器的类别
typedef typename IterT::iterator_category iterator_category;
};
指针类型:
指针本身就是可以支持随机访问(random access)的,所以我们对指针类型提供一个偏特化版本即可:
template<typename IterT> // template偏特化
struct iterator_traits<IterT*> { // 针对内置指针
typedef random_access_iterator_tag iterator_category;
};
advance 实现:
不好的实现方式:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag))
// ...
}
IterT
和 iterator_traits<IterT >::iterator_category
都是可以在编译期间确定的,而 if 判断却要在运行期间核定,这样不仅浪费时间,也会导致代码膨胀。
建议做法是建立一组重载函数(doAdvance),接受不同的类型,原函数(advance)调用这些重载函数。
// 原函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
// 调用不同版本的重载函数
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
// 下面是一系列重载函数
// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
iter += d;
}
// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
if (d < 0 ) {
throw std::out_of_range("Negative distance");
}
while (d--) ++iter;
}
set_new_handler
允许客户指定一个函数,在内存分配无法获得满足时被调用。new (std::nothrow)
是一个颇为局限的工具,它只适用于内存分配,后续的构造函数调用还是可能抛出异常。set_new_handler
即可)。set_new_handler
,然后 new 操作会在内存分配失败后抛出异常。请严肃对待编译器的警告,努力在编译器最高警告级别下争取 “无警告”。
std::tr1::
命名空间下,以下是组件实例:tr1::function
,常用于实现回调函数。tr1::bind
,能够做 STL 绑定器 bind1st 和 bind2nd 所做的每一件事,而又更多。tr1::tuple
可持有任意个数对象。tr1::array
,本质是个 STL 化的数组,即一个支持成员函数 begin 和 end 的数组。不过它大小固定,并不使用动态内存。tr1::mem_fn
,这是一个语句上构造与成员函数指针(member function pointers)一致的东西。同样容纳并扩充了 C++98 的 mem_fun
和 mem_fun_ref
的能力。tr1::reference_wrapper
,一个让引用的行为更像对象的设施。tr1::result_of
,这是一个用来推导函数调用的返回值类型的模版。