Effective C++ 笔记 第四部分 设计与声明

来源:互联网 发布:朋克 知乎 编辑:程序博客网 时间:2024/05/19 03:45

18.让接口容易被使用,不易被误用(Make onterfaces easy to use correctly and hard to use incorrectly)


好的接口容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。

若希望将对象使用智能指针管理,在工厂中要返回智能指针,这样可以使用户不会忘记使用智能指针而导致内存问题。

class Duck{};std::shared_ptr<Duck> createDuck(){}

使用shared_ptr避免cross-DLL-problem

cross-DLL-problem:对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁。这样会导致运行期错误。使用shared_ptr缺省使用原来(new出对象的)的delete.


19.设计class犹如设计type(Treat class design as type design)


Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。

设计高效classes需要考虑的问题:
1.新type的对象应如何被创建和销毁
2.对象的初始化和对象的赋值该有什么样的差别
3.新type的对象如果被passed by value,意味着什么?(copy构造函数定义了passed by value如何实现。)
4.什么是新type的合法值。(构造函数,赋值操作符setter函数需做错误检查。)
5.你的新type需要配合某个继承图系吗?(如果其他class继承你的class则你的析构函数需要声明为virtual,见条款7)
6.你的新type需要什么样的转换?(使用类型转换函数配合,见条款15 )
7.什么样的操作符和函数对此新type而言是合理的?
8.什么样的函数应该被驳回?(声明为private,见条款6)
9.谁该取用新type成员?(决定成员是public,private,protected。和哪个function或class是friend.)
10.什么是新type的”未声明接口”?
11.你的新type有多么一般化?(定义class template还是class)
12.你真的需要一个新的type吗?


20.宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)


尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。

缺省情况下C++以by value方式传递对象至函数,调用端所获得的亦是函数返回值的一个附件,这样做会调用copy构造函数和析构函数,产生费时的操作。
使用by reference可防止调用copy构造函数和析构函数,使用const可防止被修改。
切割问题:
当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数就会被调用,造成derived所添加的功能被切割。

#include <iostream>class Base{public:    Base(){        printf("Base constructor\n");    }    Base(const Base& rhs){        printf("Base copy\n");    }    virtual ~Base(){        printf("Base DEL\n");    }    virtual void display() const{        printf("Base\n");    }};class Derived: public Base{public:    Derived(){        printf("Derived constructor\n");    }    Derived(const Derived& rhs)    :Base(rhs)    {        printf("Derived copy\n");    }    ~Derived(){        printf("Derived DEL\n");    }    virtual void display() const{        printf("Derived\n");    }};void function(Base c){    //切割问题,pass-by-value的效率问题    //输出Base copy    c.display();//输出Base,切割问题    //输出Base DEL}int main(int argc, const char * argv[]) {    Derived d; //输出Base constructor、Derived constructor    function(d); //输出Base copy、Base、Base DEL    return 0; //输出Derived DEL、Base DEL}输出:Base constructorDerived constructorBase copyBaseBase DELDerived DELBase DEL

将function修改为pass-by-reference-to-const后

void function(const Base& c){    c.display();//输出Derived}输出Base constructor    //mainDerived d;语句输出的Derived constructor //mainDerived d;语句输出的Derived             //function输出的Derived DEL         //main结束输出的Base DEL            //main结束输出的

调用function并没有使用copy构造函数和析构函数,并解决了切割问题。
一般而言pass-by-value并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象。


21.必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)


绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为”在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

返回一个指向stack空间的reference,当离开作用域后reference将指向一个被删除了的对象。造成未定义结果。
若函数使用new在heap上创建对象并以reference方式返回。则会造成内存泄露,因为谁new了对象谁就delete他,但是用这种方式我们并没有合理的办法delete该reference。


22.将成员变量声明为private(Declare data members private)


切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。

为保持语法一致性。我们应该使public接口内每样东西都是函数,客户访问member data的唯一方式是通过public function。
使用private成员变量可以实现访问控制,例如读写访问,错误输入筛选。
使用private成员变量可以实现良好的封装性。


23.宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions.)


宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。

若我们需要一个便利函数,他的作用是调用类中其他的函数来完成自己的任务,那么我们需要将该还是设计为non-member还是member?
例如下面的例子,我们需要在浏览器类中提供一个便利函数以清除数据。

class WebBrowser{public:    void clearCache();    void clearHistory();    void removeCookies();    //方法一:使用member函数    void clearEverything(){        clearCache();        clearHistory();        removeCookies();    }};//方法二:使用non-member函数void clearBrowser(WebBrowser& wb){    wb.clearCache();    wb.clearHistory();    wb.removeCookies();}

结果是使用方法二更好,下面将论证:
面向对象的守则要求数据应该尽可能的封装,member函数带来的封装性比non-member函数低。在对象内的数据,越少的代码可以访问他,那么封装性将会越高。所以应该尽可能的降低访问对象内数据的代码。如member函数。
而使用很多non-member便利函数将会带来代码混乱,使我们不清楚哪个函数匹配哪一个类,解决方法是使用namespace。

//头文件WebBrowser.hnamespace WebBrowserStuff {    class WebBrowser{    public:        void clearCache();        void clearHistory();        void removeCookies();    };    void clearBrowser(WebBrowser& wb){        wb.clearCache();        wb.clearHistory();        wb.removeCookies();    }}//头文件WebBrowserBookmarks.h namespace WebBrowserStuff {    //与书签相关的便利函数}

使用namespace不仅可以清晰的使用non-member函数,还可以在不同的文件中声明同一namespace下的其他non-member函数。这也是c++标准程序库使用的组织方式,在数十个头文件(vector,memory等)中声明namespace std内的不同功能。从而降低编译依存性(客户只对他们所用的一小部分系统形成编译相依),并使客户可以自定义一些功能到namespace中, 而member函数并不能这样。
使用non-member函数可以提供封装性,包裹弹性,机能扩充性。

24.若所用参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters )


如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member.

一个有理数类,有分子和分母两个成员变量,我们希望通过operator*实现一个有理数类对象与一个int类型相乘,得到正确的结果。我们需要将int转换为有理数类在调用operator*。现考虑使用类内声明的operator*。

#include <iostream>class Rational{public:    //non-explicit的构造函数    Rational(int numerator = 0,int denominator = 1):    _numerator(numerator),    _denominator(denominator)    {}    //getter函数    int numerator() const{        return _numerator;    }    int denominator() const{        return _denominator;    }    //member方式的operator    const Rational operator*(const Rational& rhs) const{        return Rational(this->numerator()*rhs.numerator(),                        this->denominator()*rhs.denominator()                        );    }private:    int _numerator;//分子    int _denominator;//分母};int main(int argc, const char * argv[]) {    Rational onHalf(1,2);    Rational result = onHalf * 2;//OK    result = 2 * onHalf;//ERREOR:Invalid operands to binary expression ('int' and 'Rational')    return 0;}

我们通过测试可以看出onHalf * 2可以得到正确的结果。但是2 * onHalf缺导致了error。其原因是2 * onHalf将会被转换为:

2.operator*(oneHalf);

2并没转换为Rational类型,也没有operator*函数。
当函数中的参数需要进行类型转换时,声明为member函数会导致一些情况下的错误。解决方法是声明为non-member函数。

const Rational operator*(const Rational& lhs,const Rational& rhs){    return Rational(lhs.numerator()*rhs.numerator(),                    lhs.denominator()*rhs.denominator()                    );}int main(int argc, const char * argv[]) {    Rational onHalf(1,2);    Rational result = onHalf * 2;//OK    result = 2 * onHalf;//OK    return 0;}

25.考虑写出一个不抛异常的swap函数(Consider support for a a non-throwing swap)


当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。
为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

如果swap缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。
如果swap缺省版本的效率不足,几乎因为使用了pimpl手法(pimpl:pointer to implementation),试着做以下事情:
1.提供一个public swap成员函数,让他高效的置换你的类型的两个对象值。
2.在你的class或template所在的命名空间内提供一个non-member swap,并令他调用上述swap成员函数。
3.如果你正编写一个class(非class temple),为你的class特化std::swap。并令它调用你的swap成员函数。
4.如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
并注意成员版swap绝不要抛出异常,因为许多线程安全是由此保证的。

#include <iostream>class Data{public:    Data(int data):    _data(data)    {}    void serData(int data){ _data = data; }    int getData(){ return _data; }private:    int _data;};namespace WidgetStuff {    template<typename T>    class Widget{    public:        Widget(Data* data =  new Data(100) ,T Tdata = nullptr):        _data(data),        _Tdata(Tdata)        {}        void swap(Widget& other){            printf("swap\n");            using std::swap;            swap(_data, other._data);            swap(_Tdata, other._Tdata);        }        T getTdata(){            return _Tdata;        }        int getData(){            return _data->getData();        }    private:        T _Tdata;        Data* _data;    };    //c++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap    template<typename T>    void swap(Widget<T>& a,Widget<T>& b){        a.swap(b);    }}int main(int argc, const char * argv[]) {    WidgetStuff::Widget<double> a(new Data(200),205.5);    WidgetStuff::Widget<double> b(new Data(100),101.5);    //使用std的swap,为T类型对象调用最佳swap版本。但是并不能以std::swap方式调用,这样会强迫使用std的swap    using std::swap;    swap(a, b);    printf("a.data = %d,a.Tdata = %f\nb.data = %d,b.Tdata = %f\n",a.getData(),a.getTdata(),b.getData(),b.getTdata());    return 0;}输出:swapa.data = 100,a.Tdata = 101.500000b.data = 200,b.Tdata = 205.500000
0 0
原创粉丝点击