设计与声明
让接口容易被正确使用,不容易被误用, 正确性,高效性,封装性,维护性,延展性,以及协议的一致性。
让接口容易被正确使用,不易被误用
1.应该做到接口不被误用,应该考虑客户可能做出什么样的错误。
class Date{
public:
Date(int month, int day, int year);
//...
};
上述的类的接口就不是好的设计方法,因为可能导致错误Date d(30,3,1995); Date d1(2,30,1995);
,在类接口设计时,应该尽可能避免这种情况的调用发生。可以通过引入新类型进行预防接口被误用
struct Day{
explicit Day(int d):val(d){}
int val;
};
class Date{
public:
Date(const Month& m, const Day& d, const Year& y);
//...
};
Date d(30,3,1995); //false
Date d(Day(30), Month(3), Year(1995));//false
Date d(Month(3),Day(30),Year(1995)); //true
明智而审慎的导入新类型对预防“接口被误用”有神奇疗效。
2.保证正确使用的办法包括接口的一致性,以及与内置类型的行为兼容
除非有很好的理由,应该令你的types的行为与内置types一致。
3,阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
类型定义正确后,就应该限制其值在正确的范围内。办法一可以使用enum,但是要注意他不具备希望的类型安全性,应该可以把enum当作int来使用,另外一个方法是可以通过函数替换对象。应该良好的处理non-local static对象的初始化次序可能出现的问题。
4.shared_ptr 支持定制型的删除器,可以防止DLL问题,可能用来自动接触互斥锁。
设计class犹如设计type
1:新type的对象应该如何被创建和销毁。涉及到构造函数、析构函数以及内存分配函数和释放函数
2:对象的初始化和对象的赋值应该有什么样的差别。涉及到构造函数和赋值操作符的行为。
3:新type的对象如果被passed by value,意味着什么。 copy构造函数用来定义一个type的pass-by-value应该如何实现?
4:什么是新type的合法值。进行错误检测,边界值的检查,约束条件等。
5:新type需要配合某个继承图系嘛? 继承收到virtual或non-virtual函数的影响,特别是析构函数。
6:新type需要什么样的转换。如果需要显示类型转换,你就应该实现一个显示类型转换的函数。
7:什么样的操作符和函数对此新type而言是合理的?决定声明什么函数,哪些函数应该是member函数,某些是non-member函数。
8:什么样的标准函数应该驳回? 那些是应该声明为private 的。
9:谁该用新type的成员。 决定哪个成员为public、protected、private。决定哪个class或function应该是friend的,以及嵌套是否合理。
10:什么是新type的未声明接口?对效率、异常安全性以及资源运用提供何种保证?
11:新type有那么多一般化嘛? 是否需要template?
12:真的需要一个新的type嘛?
宁以pass-by-reference-to-const替换pass-by-value
缺省情况下c++以by value方式传递对象,传递的都是对象的一个副本,对于自定义对象来说,副本都是由构造函数产出,导致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;
};
bool validStudent(Student s); //by value
Student plato;
bool platoIsOk = validateStudent(plato);
上述的函数在传值的时候一共调用了六次构造函数和六次析构函数,耗时巨大。而采用pass-by-reference-to-const 的方式传值可以避免构造函数和析构函数的调用
bool validateStudent(const Student &s);
通过by reference的方式传递参数可以避免slicing(对象切割)问题。
一般而言,可以合理的假设“pass-by-value”并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象。
必须返回对象时,别妄想返回其reference
绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
private:
int n,d;
friend const Rational(const Rational& lhs, const Rational& rhs); //by value
};
Rational a(1,2);
Rational b(3,5);
Rational c = a * b; //不合理
如果想要operator*返回一个reference指向如此数值,他必须自己创建那个Rational对象。
const Rational& operator*(const Rational& lhs, const Rational& rhs){
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //糟糕的代码 因为result是一个local对象,函数退出前被销毁了
}
const Rational& operator*(const Rational& lhs, const Rational& rhs){
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result; //更糟糕的代码, 因为付出了一个构造函数的代价,并且没有delete
}
const Rational& operator*(const Rational& lhs, const Rational& rhs){
static Rational result;
result = Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //更糟糕的代码, 会导致多线程安全的问题。
}
bool operator==(const Rational &lhs, const Rational& rhs);
Rational a,b,c,d;
if((a*b) == (c*d)){
}else{ } //此操作if一直为true。因为operator*返回的是一个reference指向operator*内部定义的static Rational对象,但是static对象只有一个。
因此,一个必须返回新对象的函数的正确写法是:就让那个函数返回一个新对象。
inline const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n , lhs.d * rhs.d);
}
将成员变量声明为private
将成员变量声明为private,可以赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
1.当要移除class内的public成员变量时,所有和class有关的代码都要重写
2.当要移除class内的protected成员变量时,所有使用他的derived class 都被破坏
3.当要移除class内的private时,只需要少量的修改对应使用的它的函数。
宁以non-member、non-friend替换member函数
对于封装:如果有东西被封装,它就不再可见。越多的东西被封装,越少的人可以看到它,越少的人可以看到它,我们就有很大的弹性去变化它,因为我们的改变仅仅直接影响看到改变的人事物。使得改变事物只影响有限的客户,越多函数可以访问它,数据的封装性就越低。上一条说过,成员变量应该是private的,因为如果它不是,就有无限量的函数可以访问他们。而能够访问private成员变量的只有class的member函数加上friend函数。因此如果你在一个member函数和一个non-member、non-friend函数之间做抉择的时候,而且两者提供相同的机能。那么导致较大封装性的是non-member、non-friend函数。因为它并不增加“能够访问class内的private成份”的函数数量。
若所有参数皆需类型转换,请为此采用non-member函数
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
private:
};
如果想要上述class实现与有理数进行混合运算的函数,一般建议采用non-member函数。因为如果是member函数时,容易导致错误的调用。如
class Rational{
public:
const Rational operator*(const Rational& rhs) const;
};
Rational one(1,8);
Rational two(2,4);
Rational result = one * two; //ture;
result = result * one; //true
result = one*2; //true;
result = 2 * one; //false
因为2没有class,也就没有operator*成员函数。result = one * 2;
正确的原因是,因为存在一个隐式的类型转换。只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。为了解决这个问题,可以采用non-member函数进行处理:
class Rational{};
const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
result = one * 2; //true;
result = 2 * one;//true;
无论何时如果你可以避免friend函数就该避免,不能够只因函数不该成为member,就自动让它成为friend。
本条的真理,不是全部的真理。从oo-c++到template c++,让rational成为一个template class时,需要新的讨论。
考虑写出一个不抛出异常的swap函数
swap原本是STL的一部分,然后称为异常安全性编程的脊柱,以及采用处理自我赋值可能性的一种常见机制。
1.如果swap的缺省实现对你的class或class template提供可接受的效率,你不需要做任何事。任何尝试swap那种对象的人都会取得缺省版本,而那将有良好的运作。
namespace std{
template<typename T>
void swap(T& a, T& b){
T temp(a);
a = b;
b = temp;
}
}
2.如果swap缺省实现的效率不足,(那几乎总是意味着你的class或template class使用了某种pointer to implementation手法),试着做以下事情:
class WidgetImp{
public:
//...
private:
int a, b,c;
std::vector<double> v;
};
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){
*pImp = *(rhs.pImp);
//...
}
private:
WidgetImp* pImp;
};
//置换两个Widget对象值时,只需要置换指针就行,但是使用缺省的swap算法,他会复制三个widget,同时还会复制三个widgetimp对象,效率低。
希望告诉std::swap函数,当widgets被置换时,真正该做的是置换内部的pimp指针。解决方法是:将std::swap针对widget特化。
通常我们不能够改变std命名空间的任何东西,但是可以为标准template制造特化版本,使他专属于我们自己的class。
1)提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。
class Widget{
public:
void swap(Widget& other){
using std::swap; //必要的
swap(pImp, other.pImp);
}
};
namespace std{
template<> //全特化声明
void swap<Widget> (Widget& a, Widget& b){ //<Widget>意味针对这个class全特化
a.swap(b);
}
};
class template情况时:
template<typename T>
class WidgetImp{ //...};
template<typename T>
class Widget{ //...};
namespace std{
template<typename T> //错误,不合法
void swap<Widget<T>> (Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
//企图偏特化一个function template,但是c++只允许针对class template 偏特化, 在function templates上是行不通的。
2)在你的class 或 template 所在的命名空间提供一个non-member swap,并令它调用上述swap成员函数,不再将那个non-member swap声明为std::swap的特化版本或重载版本
//这个方法针对class或这template class都行的通
namespace WidgetStuff{
template<typename T>
class Widget{ //...};
template<typename T> //non-member swap 函数,不属于std命名空间
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
3)如果你编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。
template<typename T>
void doSomething(T& obj1, T& obj2){
//...
swap(obj1,obj2);
}
此时应该调用那个版本的swap函数?此时根据调用情况,决定调用哪个swap成员函数,但是为了很好的调用swap函数,应该把std::swap暴露出来:
template<typename T>
void doSomethin(T& obj1, T& obj2){
using std::swap;
//...
swap(obj1,obj2);
}
针对swap函数可以进行你需要的特定函数去实现,全特化,或者偏特化等。