一、让接口容易被正确使用,不易被误用
1.接口应该让客户使用时,如果未得到预期行为,该代码不该通过编译,如果通过了编译,应该得到客户所想要的行为。
以下代码中,用户可能会使用错误的顺序,或者是使用错误超出范围的值。
class Date{
public:
Date(int month, int day, int year);
...
};
Date d(30, 3, 1995);
Date d(2, 30 1995);
改进方法,最好struct还是改为class:
struct Day{
explicit Day(int d):val(d){ }
int val;
};
struct Month{
explicit Month(int d):val(d){ }
int val;
};
struct Year{
explicit Year(int d):val(d){ }
int val;
};
class Date{
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); //错误
Date d(Day(30), Month(3), Year(1995)); //错误
Date d(Month(3), Day(30), Year(1995)); //正确
有些类型确定后,其值是确定的:
struct Month{
public:
static Month Jan(){return Month(1); }
static Month Feb(){return Month(2); }
private:
explicit Month(int m);
};
Date d(Month::Mar(), Day(30), Year(1995));
2.shared_ptr虽然比原始指针大且慢,但是这额外的执行成本并不显著,降低客户错误的成效是更加显著的
二、设计class犹如设计type
三、宁以pass-by-reference-to-const替换pass-by-value
1.一般默认情况下,函数的参数以及返回值都是以实际值的复件(副本)传回的,通过对象的copy构造函数产出,这将是一个非常昂贵的操作。改为pass by reference-to-const方法,通过const方法可以让使用者不用为自己传入的对象担心。
bool validateStudent(const Student& s);
2.通过by reference方式传递参数时可以和避免slicing(对象切割)问题。在以下方法中,传入的是WindowWithScrollBars对象,但是会通过复制构造函数,构造为Window对象,因此以下调用的w.display()为Window对象的方法。
class Window{
public:
...
std::string name() const;
virtual void display() const;
};
class WindowWithScrollBars: public Window{
public:
...
virtual void display() const;
}
void printNameAndDisplay(Window w){
std::cout<<w.name();
w.dispaly();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
改进,在C++编译器的底层,往往以指针实现出来,因此pass by reference通常意味真正传递的是指针:
void printNameAndDisplay(const Window& w){
std::cout<<w.name();
w.dispaly();
}
通常对内置对象以及STL的迭代器和函数对象采用pass-by-value,更加高效
四、必须返回对象时,就不用返回reference
1.错误写法1
以下方法中,会导致严重问题,因为初始化的Rational为local对象,在函数退出前就被销毁了。
const Rational& operator* (const Rational& lhs, const Rational& rhs){
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
2.错误写法2,以下方法中x*y*z会生成两个对象,但是后面没有调用delete方法。
const Rational& operator* (const Rational& lhs, const Rational& rhs){
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
Rational w,x,y,z;
w = x * y * z;
3.正确写法,直接返回构造的对象
const Rational& operator* (const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
五、为了保证封装性,需要将成员变量声明为private
1.将成员变量声明为private,访问其的唯一方法就是通过成员函数,可以防止用户迷惑地记住是否该使用小括号
2.可以实现出“不准访问”、“只读访问”、“读写访问”。
3.可以通过函数访问成员变量,相当于把该成员变量封装起来了,以后我们要更改该成员变量的存储形式和内容,可以直接去private中修改,用户在其他地方调用不了该成员变量,因此我们下一次在修改该成员变量形式的时候可以少修改很多地方。某些东西的封装性与“成员变量的内容改变时所破坏的代码数量”成反比。
class SpeedDateCollection{
public:
void addValue(int speed); //添加一笔新数据
double averageSoFar() const; // 返回平均速度
}
以上代码中返回均速度,有两种形式,第一种是每一次添加新数据时,将其用来计算平均速度存下来,下一次调用返回平均速度直接读取就行。第二种是每次把新数据存下来,直到需要返回平均速度时再去计算。两种方法实现形式是不一样的,但是都能通过averageSoFar方法进行返回,根据系统的内存以及速度要求进行选择。
六、宁以non-member、non-friend替换member函数
在以下方法中,方法二提供了更好的封装性,
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
//方法一
void clearEverything();
}
//方法二
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
1.friend函数对class private成员的访问权力和member函数相同,以上论述只适用于non-meber non-friend函数
2.该non-member函数可以放在其他类里,也就是说可以声明一个工具类用于存放该函数
在C++中通常将该函数放在与类同一个命名空间中
namespace WebBrowserSStuff{
class webBrowser{...};
void clearBrowser(WebBrowser& wb);
}
七、若所有参数皆需类型转换,请为此采用non-member
class Rational{
public:
...
const Rational operator* (const Rational& rhs) const;
};
以下方法中,在编译器对整数2并没有相应的class,没有operator成员函数,编译器会去尝试寻找non-member operator,但在此例中会寻找失败
result = oneHalf.operator*(2);
result = 2.operator*(oneHalf);
解决办法,声明一个non-member operator方法
const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
八、考虑写出一个不抛异常的swap函数,当std::swap效率不高时提供一个swap成员函数,并命名在对应成员空间
Swap函数
namespace std{
template<typename T>
void swap(T& a, T& b){
T temp(a);
a = b;
b = temp;
}
}
传统的swap函数用于交换两个对象,他会对对象内部的所有数据进行复制和交换。在以下方法中,交换两个Widget对象,原本只需要交换两个指针pImpl,但是会先进行初始化,初始化的过程中会复制构造WidgetImpl,导致大量的运算量
class WidgetImpl{
public:
...
private:
int a, b, c;
std::vector<double> v;
};
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){
...
*pImpl = *(rhs.pImpl);
...
}
private:
WidgetImpl* pImpl;
};
1.重新定义特化版本swap,以下方法访问的变量是其private成员,无法访问,
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b){
swap(a.pImpl, b.pImpl);
}
}
2.修改方法,通过成成员函数进行swap
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){
...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
private:
WidgetImpl* pImpl;
};
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b);
}
}
3.如果两个类是类模板的话,可以尝试声明为类模板的函数
namespace std{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
以上方法无法进行,以上属于偏特化一个函数模板,无法进行,可以修改为以下重载函数,但是最好不要重载std命名空间里的模板
namespace std{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
可以将其声明在其他命名空间,实际在调用的时候,如果不指明命名空间,比如std::swap(obj1,obj2),编译器将会更具T的类型去对应命名空间中找对应函数,如果没有再去std中查找。
namespace WidgetStuff{
template<typename T>
class Widget{
}
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
4.当提供member swap时,应该提供一个non-member swap方法