四:设计与声明
条款18:让接口容易被正确使用,不易被误用
请记住:1.好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这些性质。
2.“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
3.“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
4.tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。
条款19:设计class犹如设计type
请记住:Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。p84-86
条款20:宁以pass-by-reference-to-const替换pass-by-value
尽量以pass-by-reference-to-const替换pass-by-value,因为前者没有任何构造函数或析构函数被调用,没有任何新对象被创建。并且前者还可以避免切割问题。
切割问题:当一个derived class 对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。
//图形窗口系统
class Window{
public:
...
std::string name() const;
virtual void display() const;//display是个virtual函数,这意味着Window对象的显示方式和WindowWithScrollBars的显示方式不同
};
class WindowWithScrollBars :public Window{
...
virtual void display() const;
};
//假设写个函数打印窗口名称
void printNameAndDisplay(Window w) { //不正确 参数可能被切割
std::cout<<w.name();
w.display();
}
//当调用上述函数并传递给他一个WindowWithScrollBars对象
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
/*
参数w会被构造成一个Window 对象:它是passed by value,造成WindowWithScrollBars的行为像个WindowWithScrollBars对象的那些特化性质全
被切割掉了,在printNameAndDisplay函数内不论传递过来的对象原来是什么类型,参数w就像一个Window对象,因此printNameAndDisplay内调用的
display调用的总是Window::display,绝不会是WindowWithScrollBars::display.
*/
//解决办法,传递引用
void printNameAndDisplay(const Window &w) { //不正确 参数可能被切割
std::cout<<w.name();
w.display();
}
请记住:1.尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
2.以上规则不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较合适。
条款21:必须返回对象时,别妄想返回reference
容易犯下一个致命错误:开始传递一些references指向其实并不存在的对象。
//表现有理数的class,内含一个函数用来计算两个有理数的乘积
class Rational{
public:
Rational(int numerator = 0,int denominator = 1);
private:
int n, d;
//以by value方式返回其计算结果(一个对象),会付出该对象构造和析构成本。
//friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};
//改变一:以by reference方式返回其计算结果
const Rational& operator*(const Rational &lhs, const Rational &rhs){
/*
警告,糟糕的代码,不仅需要构造,更严重的是:这个函数返回一个reference指向local对象,而local对象在函数退出前被销毁了,
任何调用者甚至只是对此函数的返回值做一点点运用,都会发生致命的错误。
*/
Rational result(lhs.n * rhs.n, lhs.d*rhs.d);
return result;
}
//改变二:考虑在heap内构造一个对象,并返回reference指向他。
const Rational& operator*(const Rational &lhs, const Rational &rhs){
/*
还是必须付出一个“构造函数调用”代价,并会留下一个问题,谁该对着被你new出来的对象实施delete,并且某些情况下
无法阻止资源泄漏,如下:
Rational w,x,y,z;
w = x*y*z;
没有合理的办法让调用者取得operator*返回的reference背后隐藏的那个指针
*/
Rational *result = new Rational(lhs.n * rhs.n, lhs.d*rhs.d);
return *result;
}
//改变三:返回的reference指向一个被定义于函数内部的static Rational对象。
const Rational& operator*(const Rational &lhs, const Rational &rhs){
/*
考虑以下这些完全合理的客户代码
bool operator == (const Rational& lns,const Rational& rhs);
Rational a,b,c,d;
if((a*b)==(c*d)){
}else{
}
表达式(a*b)==(c*d)总是被核算为true,不论a,b,c,d的值是什么,等价形式if(operator==(operator*(a,b),operator*(c,d)))
两次operator*调用的确各自改变了static Rational对象值,但由于他们返回的都是reference,因此调用端看到的永远是static Rational
对象的“现值”。
*/
static Rational result; // 警告,又一堆烂代码。
result = ...;
return result;
}
//一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象
inline const Rational operator*(const Rational &lhs, const Rational &rhs){
//ps:关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用
return Rational(lhs.n * rhs.n, lhs.d * rhs);
}
请记住:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
条款22:将成员变量声明为private
将成员变量声明为private,使用函数可以让你对成员变量的处理有更精确的控制。如下:
class AccessLevels{
public:
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { WriteOnly = value; }
private:
int noAccess; //对此int无任何访问动作
int readOnly; //对此int做只读访问
int readWrite; //对此int做读写访问
int WriteOnly; //对此int做惟写访问
};
封装!如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。将成员变量隐藏在函数接口背后,可以为“所有可能的实现”提供弹性。
请记住:1.切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分的实现弹性。
2.protected并不比public更具封装性。
条款23:宁以non-member、non-friend替换member函数
namespace可跨越多个源码文件。将所有便利函数放在多个头文件内但隶属用同一个命名空间,意味客户可以轻松扩展这一组便利函数。他们需要做的就是添加更多non-member non-friend函数到此命名空间内。p101
请记住:宁以non-member、non-friend替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
class Rational{
public:
Rational(int numerator = 0,int denominator = 1);//构造函数刻意不为explicit;允许 int-to-Rational隐式转换
int numerator() const; //分子分母的访问函数
int denominator () const;
const Rational operator*(const Rational &rhs);
private:
int n, d;
};
//看下面这些例子
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneEighth * oneHalf; //很好
result = result * oneEighth; //很好
//----------------------------------------------------
result = oneHalf * 2; // 没问题,在non-explicit构造函数的情况下
result = 2 * oneHalf; // 错误,不满足乘法交换律,不符合实际。
/*
结论是,只有当参数被列于参数列内,这个参数再试隐式类型转换的合法参与者。地位相当于“被调用之成员函数所隶属的那个对象”————
即this对象————的那个隐喻参数,绝不是隐式转换的合法参与者。这就是为什么上述第一次调用可以通过编译(result = oneHalf * 2;)
,而第二次则否(result = 2 * oneHalf;)
*/
请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
条款25:考虑写出一个不抛异常的swap函数
全特化版本:
namespace std{
template<>
void swap<Widget>(Widget &a,Widget &b){
swap(a.pImpl,b.pImpl);
}
}
/*
这个函数一开始的“template<>”表示它是std::swap的一个全特化版本,函数名称之后的“<Widget>”表示这一特化版本系针对“T是Widget”而设计。
换句话说当一般性的swap template施行于Widgets身上便会以(被允许)为标准template(如swap)制造特化版本,使他专属于我们自己的classes(例如Widget)。
请记住:1.当std::swap对你的类型效率不高时,提供一个 swap成员函数,并确定这个函数不抛出异常。
2.如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap。
3.调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。(令std::swap在此函数内可用)
4.为“用户定义类型”进行std template 全特化是好的,但千万不要尝试在std内加入某些std而言全新的东西。
提升接口易用性与封装技巧:C++编程最佳实践
本文探讨了C++编程中的关键设计原则,如接口一致性、防止误用、正确参数传递、返回对象策略、成员变量封装和非成员函数使用,帮助开发者创建高效、易用且不易出错的代码。
297

被折叠的 条评论
为什么被折叠?



