4 设计与声明
18 让接口容易被正确使用,不易被误用
理想上,如果用户企图使用一个接口而却没有获得预期的行为,代码就应该不通过编译,例如一个接口要求输入一个月份,此时输入小于1和大于12的数都是无效的,我们应该尽量使用不兼容这些数的接口,即输入小于1或者大于12的数就无法通过编译。
可选的一个方法是使用enum,但是enum不具备类型安全性,也就是说可以被转换成其他的类型使用(如int)。或者使用类,预先定义所有有效的月份。
class Month{
static Month Jan(){ return Month(1); }
static Month Feb(){ return Month(2); }
...
static Month Dec(){ return Month(12); }
...
private:
ecplict Month(int m);
...
};
DateaAPI(Month::Mar(), ... ); //可以杜绝不正确的输入
再或者对于工厂函数返回原始指针,但是用户可能会忘记将其赋予给智能指针从而导致内存泄漏,于是我们可以直接让工厂函数返回智能指针,并在函数内部直接指定删除器(deleter)。即:
shared_ptr<class_type> create(); //返回指向class_type的智能指针
20 宁以pass-by-reference-const替换pass-by-value
1 通过const引用传递可以避免多余的构造和析构过程,传值会因为临时对象而产生多余的构造和析构过程。
通过const引用传递可以实现多态,C++的多态只能通过引用(引用的底层也是通过指针实现的)和指针来实现,那么如果接口是父类指针的话,按值传递子类对象,将会造成拷贝构造函数仅仅构造父类部分,子类部分不予以拷贝的情况(不支持多态),只有通过const引用传递子类对象,才能正确的调用子类重写的父类虚函数。
2 对于内置类型(int、double等),以及STL的迭代器和函数对象等,最好使用pass-by-value。
21 必须返回对象时,别妄想返回reference
对于函数内部的局部变量是不可以返回其引用的,同样也不可以对内部的指针返回引用,因为会产生内存泄漏(假设T *f(args)
,那么f(f(args))
会产生内存泄漏并且无法进行delete)。如果使用static,那可能数据会被改变,无论是单线程还是多线程都是数据不安全的。
22 将成员变量声明为private
将数据设置为private可以提供较好的封装性,但是public和protected都不能提供太好的封装性,public的成员变量可以被外部访问,而protected的成员变量可以被派生类自由访问。
23 宁以non-member、non_friend替换member函数
对于某些可以解耦的函数来说,定义为成员函数会使这些函数可以自由的访问私有成员变量,这是减少了封装性的,所以可以定义为非成员函数或者非友元函数(友元函数和成员函数访问权限相同)。C++中通用的方法是定义于namespace中,因为namespace可以跨文件进行扩展,所以我们可以将不同种类的这些函数放在不同的文件中,但是属于同一个namespace。所以在扩展功能的时候,只需要在namespace中建立头文件并声明函数就行(C++标准库函数的做法)。
24 若所有参数皆需类型转换, 请为此采用non-member函数
如果要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。
class A{
private:
int a;
int b;
public:
A(int _a = 0, int _b = 1) : a(_a), b(_b){ }
int aa(){ return a; }
int bb(){ return b; }
const A operator(const A &aaa) const{
return A(a * aaa.a, (b * aaa.b));
}
};
A a;
A ret = a * 2; //可以
A ret2 = 2 * a; //不可以
但是如果添加了非成员函数,就可以通过隐式类型转换,完成这种乘法运算(支持数据交换)[或者采用友元函数也可以]
class A{
...
};
const A operator*(const A &a, const B &b){
return ret(a.aa() * b.aa(), a.bb() * b.bb());
}
A a;
A ret = a * 2; //可以
A ret2 = 2 * a; //可以
25 考虑写出一个不抛异常的swap函数
对于用户自建类的swap操作,可以自行编写模版全特化的swap以增加运行速度
class Aimpl{
int a, b, c; //很多数据
vector<double> vec; //复制时间很长
};
class A{
public:
Aimpl *ptr;
};
对于这个自建类我们可以仅仅交换指针,所以可以自行扩充std名称空间(我们不被允许改变std命名空间内的任何东西,但可以为标准的模版(如swap()
)增加特化版本),增加为我们这个类的特化版swap<A>()
namspace std{
template <>
void swap<A>(A &lhs, A &rhs){
swap(lhs.ptr, rhs.ptr);
}
}
但是如果指针是private类型的呢,这函数就无法通过编译了,所以要换一个思路,即调用公有成员函数的思路,我们自定义一个名为swap()
的public成员函数做真正的置换工作,然后特化swap,使其调用该成员函数。
class Aimpl{
int a, b, c; //很多数据
vector<double> vec; //复制时间很长
};
class A{
private:
T *ptr
public:
...
void swap(A &other){
using std::swap; //使用标准库中的swap函数避免递归,但是用using声明可以使代码去搜索有没有用户自定的特化版本(见底下详细解释)
swap(ptr, other.ptr);
//不能写成std::swap(ptr, other.ptr);,否则编译器不能去找到自定义的特化版本,只会使用标准库的版本
}
...
};
namespace{
template <>
void swap<A>(A &lhs, A &rhs){
a.swap(b); //调用公有成员函数
}
}
但是这样做还是有个不足,那就是如果A和Aimpl是模版类的话,就不能按照这种方法写了:
template <typename T>
class Aimpl{
...
};
template <typename T>
class A{
...
};
namespace{
template <T>
void swap<A<T> >(A<T> &lhs, A<T> &rhs){ //方法错误,无法通过编译
a.swap(b); //调用公有成员函数
}
}
这是因为函数模版只可以被全特化,不可以被偏特化,(只有类模板可以被全特化)原因是可以用重载的方法去代替偏特化。
这个时候还是可以声明一个非成员函数swap()
让其调用成员函数swap()
,但是非成员函数不再声明为特化版本,并放入用户自定义的命名空间内。
namespace AStuff{
...
template <typename T>
class A{ ... };
...
template <typename T>
void swap(A<T> &lhs, A<T> &rhs){
a.swap(b);
}
}
但是从客户的观点来看,我们写下一个模版函数
template <typename T>
void func(T &a, T &b){
using std::swap;
swap(T &a, T &b);
}
应该调用什么swap()
函数呢?可能是一般版本或者是特化版本。
C++的名称查找法则确保找到globa作用于或T所在之命名空间内的任何T专属的swap
,使用这种调用方式(使用using声明,并且调用swap的时候不加任何的命名空间修饰)
using std::swap;
swap(T &a, T &b);
如果T恰好是A并且位于命名空间AStuff中,编译器就会找到AStuff中特化的版本并使用,若未找到则调用一般版本。
使用这种调用方式(调用swap的时候加命名空间修饰std::)
std::swap(T &a, T &b);
会强迫编译器只认std内的swap
(包括其任何模版全特化版本)。