4 设计与声明
条款18:让接口容易被正确使用,不易被误用
条款19:设计class犹如设计type
条款20:宁以pass-by-reference-to-const替换pass-by-value
by value方式比by reference方式成本大,因为会调用类的拷贝构造函数,销毁时会调用析构函数,如果类内还有类,对应的类的拷贝构造函数和析构函数都会被调用,所以by value方式成本非常大。
另外,by value方式还可能造成对象的切割问题:当一个派生类对象以by value方式传递并被视为一个基类对象时,基类的拷贝构造函数会被调用,而“造成此对象的行为像个派生类对象”的那些特化性质会被切割掉,仅剩下一个基类对象。
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.display();
}
当给上述函数传递一个WindowWithScrollBars对象,上述函数内调用display调用的总是Window::display。
解决切割问题的办法,就是以by reference-to-const的方式传递w。
如果窥视C++编译器的底层,reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果你有一个对象属于内置类型,pass by value往往比pass by reference效率高些。对于内置类型而言,当你有机会采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。这个忠告也适用于STL的迭代器和函数对象,因为习惯上他们被设计为passed by value。(??????????)
一般而言,你可以合理假设pass-by-value并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象。至于其他任何东西都请遵守本条款的忠告,尽量以pass-by-reference-to-const替换pass-by-value。
请记住:
1.尽量以pass-by-reference-to-const替换pass-by-value.
2.以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。
条款21 必须返回对象时,别妄想返回期reference
有可能传递一些references指向其实并不存在的对象:
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
private:
int n, d;
friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};
如果operator*的实现内返回的如果是一个stack对象,那么当函数返回时该对象根本就不存在;如果返回的是一个heap对象,那么就会存在忘记delete的风险,因为:
Rationl w, x, y, z;
w = x * y * z;
这里,同一个语句内调用了两次operator*, 因而两次使用new,也就需要两次delete。但却没有合理的办法让operator*使用者进行那些delete调用,因为没有合理的办法让他们取得operator*反悔的reference背后隐藏的那个指针。有人说可以使用static Rational对象:
const Rationl& operator* (const Rational& lhs, const Rational& rhs){
static Rational result;
result = ...;
return result;
}
就像所有用上static对象的设计一样,这一个也立刻造成我们对多线程安全性的疑虑。而且想想更深层次的瑕疵,考虑下面这些完全合理的客户代码:
bool operator== (const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
if((a*b) == (c * d)){
}else{
}
等价形式:
if(operator==(operator*(a, b), operator*(c, d)))
operator==被要求将operator*内部定义的static Rational对象值拿来和operator*内的static Rational对象值 比较,如果不相等那才怪呢。
一个必须返回新对象的函数的正确写法是:就让函数返回一个新对象:
inline const Rational operator* (const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
请记住:
绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或者reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22: 将成员变量声明为private
将成员变量声明为private的好处:
1.不用去考虑要不要加小括号,如果都是private,那么访问变量肯定都需要加括号(语法一致性)
2.使用函数可以让你对成员变量的处理有更精确的控制:
class AccessLevels{
public:
int getReadOnly() const {return readOnly;}
void setReadOnly(int value) {readWrite = value;}
int getReadWrite() const {return readWrite;}
void setWriteOnly(int value) {writeOnly = value;}
private:
int noAccess;
int readOnly;
int readWrite;
int writeOnly;
};
3.封装:
class SpeedDataCollection{
public:
void addValue(int speed);
double averageSoFar() const;
};
成员函数averageSoFar的设计:做法一是在类内设计一个成员变量,记录至今以来所有速度的平均值,当函数被调用时,只需返回那个成员变量就好。这样做使得函数的反应速度更快,因为不再需要计算,但会使得对象变大,因为需要给很多计算速度相关的成员分配空间。
做法二是在函数调用时才计算,这样函数的反应速度会变慢,但是对象可以做的比较小。
两种不同的做法,需要把成员变量隐藏在函数接口的背后,可以为“所有可能实现”提供弹性。
请记住:
1.切记将成员变量声明为private。这可赋予客户访问数据的一致性,可细微划分访问控制,允许约束条件获得保证,并提供class作者以充实的实现弹性。
2.protected并不比public更具封装性(???????)