条款18:让接口容易被正确使用,不易被误用
开发一个“容易被正确使用,不易被误用”的接口,首先必须考虑用户会做出什么样的错误。以下为例:
class Date { public: Date(int month, int day, int year); … };
乍见之下这个接口通情达理,但是至少容易犯两个错误。第一,他们可能以错误的次序传递参数;第二, 他们可能传递一个无效的参数。许多客户端错误可以因为导入新的类型而获得预防:
class Month { private: int m_month; public: explicit Month(int month): m_month(month){} }; class Day { private: int m_day; public: explicit Day(int day): m_day(day){} }; class Year { private: int m_year; public: explicit Year(int year): m_year(year){} }; class Date { private: Year m_year; Month m_month; Day m_day; public: Date(Year year, Month month, Day day):m_year(year), m_month(month), m_day(day){} };
预防客户错误的另一个办法是限制类型内什么事可做,什么事不可做,常见的限制是加上const。如下例:
if(a*b = c) …//其实原意是做一次比较动作
如果重载的乘法操作返回的是const,编译器就会识别出这种赋值运算不恰当了。
另外,“除非有好理由,否则应该尽量令你的types的行为与内置types一致”。比如不要在乘法运算中做加法运算。很少有其他性质比“一致性”更能导致接口容易被正确使用。比如长度,不要有的类型用size表示,有的类型用length表示。虽然有些IDE插件能够自动去寻找相应的方法名,但“不一致性对开发人员造成的心理和精神上的摩擦与争执,没有任何一个IDE可以完全抹除”。
任何接口如果要求客户必须记得做某件事,就有着“不正确使用”的倾向,因为客户可能忘记做某件事,最容易造成资源泄露。在前面资源管理中提到的智能指针,让专门的资源管理来掌握资源的回收:
auto_ptr<Investment> createInvestment();
shared_ptr<Investment> createInvestment();
shared_ptr有一个特别好的性质:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“Cross-DLLproblem”。这个问题发生于在不同的DLL中出生(new)和删除(delete)的情况(对象生命周期横跨两个DLL,但在第二个DLL中结束生命的时候却希望调用的是第一个DLL的析构函数),自定义删除器则会在删除时仍然调用诞生时所在的那个DLL的析构函数。智能指针比原始指针大且慢,而且使用辅助动态内存。在许多应用程序中这些额外的执行成本并不高,然而其降低客户错误的成效确很明显。
请记住:
1、 好的接口容易被正确使用,不易被误用
2、 接口的一致性,以及与内置类型的行为兼容
3、 “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值、以及消除客户的资源管理责任
4、 多使用资源管理类如智能指针shared_ptr、auto_ptr等进行资源管理
条款19:设计class犹如设计type
新type的对象应该如何被创建和销毁?这影响到class的构造函数、析构函数以及内存分配和释放函数的设计。
对象的初始化和对象的赋值该有什么样的差别?决定拷贝构造函数和赋值操作符的行为,以及之间的差异
新type的对象如果被passed by value,意味着什么?拷贝构造函数的实现
什么是新的type的“合法值”?对于class的成员变量而言,通常只有某些数值是有效的,所以通常要进行错误检查、异常处理等
新的type需要配合某个继承的类吗?继承某些classes必然受到那些classes的设计的束缚,特别是virtual和non-virtual的影响
新的type需要什么样的转换?如果期待进行允许的类型T1对象隐式转换为类型T2对象,就必须在classT1写一个类型转换函数(operator T2)或class T2内写一个可被单一实参调用的构造函数。如果只允许explicit构造函数,就得写出专门负责指向转换的函数
什么样的操作符和函数对此type而言是合理的?决定class声明哪些函数
什么样的标准函数应该驳回?那些正是你必须声明为private的
谁该取用新type的成员?决定函数为public、private、protected或friends
什么是新type的“未声明接口”它对效率、异常安全性以及资源的运用提供何种保证
新type有多一般化?如果不仅是定义一个新type,而是一整个types家族,就应该定义新的classtemplate
真的需要一个新type吗?
请记住:
Class的设计就是type的设计,在定义一个新的type之前,请确定你已经考虑过本条款中所有讨论的主题。
条款20:宁以pass-by-reference-to-const替换pass-by-value
在缺省的情况下,c++以by value的方式传递对象至函数,这些对象由copy构造函数产出,使得pass-by-value成为昂贵的操作,考虑以下继承:
classPerson{ public: Person(); vitual~Person(); private: std::stringname; std::stringaddress; }; classStudent:publicPerson{ public: Student(); virtual~Student(); private: std::stringschoolName; std::stringschoolAddress; };
现在有一个调用函数validateStudent,要调用一个Student实参并返回它是否有效?
boolvalidateStudent(Students); Student plato; boolplatoIsOK=validateStudent(plato);
Person一次copy构造函数、Student一次copy构造函数、四次stringcopy构造函数,以及他们的析构函数。改进方法:
bool validateStudent(const Student& s);
这种方式高效:没有任何构造函数和析构函数,也没有任何新对象构建。此外,这种方式还可以避免对象的切割(slicing)。
如果窥视c++编译器的底层,reference往往是以指针的方式实现的,因此对于内置类型passe-by-value更合理,这忠告同样是适用于STL迭代器和函数对象。
请记住:
1、 尽量以passby reference to const替换pass by value。前者通常比较高效,并可避免切割的问题。
2、 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,passby value更加适当。
条款21:必须返回对象时,别妄想返回其reference
在上条款中谈到的pass-by-value一般都以pass-by-reference替代,但是容易存在过度使用的问题:只要看到是一个非内置类型,就去使用引用传值。
class Rational { private: int n; int d; public: Rational(int numerator = 0, int denominator= 1):n(numerator), d(denominator){} friend const Rational& operator* (constRational& r1, const Rational& r2){…} };
函数创建对象有两种方式:在stack空间或heap空间建立。首先如果定义一个local变量,就在stack创建对象:
friend constRational& operator* (const Rational& r1, const Rational& r2) { Rational temp(r1.n * r2.n, r1.d * r2.d); return temp; }
返回local对象的引用是不可接受的,因为local在函数退出前已经被销毁了。于是让我们考虑在heap内构造一个对象,并返回reference指向它:
friend constRational& operator* (const Rational& r1, const Rational& r2) { Rational* temp = new Rational(r1.n *r2.n, r1.d * r2.d); return *temp; }
无法析构对象,导致资源泄露。
static对象位于全局静态区,它的生命周期与这个程序的生命周期是相同的,所以不用担心它会像栈对象那样很快消失掉,也不用担心它会像堆对象那样有资源泄露的危险。可以像这样写:
friend constRational& operator* (const Rational& r1, const Rational& r2) { static Rational temp; temp = …; return temp; }
这样可以正常编译通过,但是对于:
Rational a, b, c,d; If((a*b) == (c*d))
if条件恒为真,这就是静态对象做的!因为返回值共享这个静态对象。
一个必须返回新对象的正确写法是去掉引用,返回一个新对象:
friend constRational operator* (const Rational& r1, const Rational& r2) { return Rational(r1.n * r2.n, r1.d *r2.d); }
当然,你需要承受返回值的构造和析构成本,但是从长原来看是为了获得正确行为而付出的一个小小的代价。
请记住:
绝对不要返回pointer或reference指向一个localstack对象,指向一个heap-allocated对象也不是好方法,更不能指向一个local static对象(数组),需要复制对象的时候,就让它去复制。