设计与声明(一)

来源:互联网 发布:网络大仲马什么意思 编辑:程序博客网 时间:2024/06/05 11:21

条款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对象(数组),需要复制对象的时候,就让它去复制。

 

1 0
原创粉丝点击