effective c++读书笔记(四)

来源:互联网 发布:gif制作软件 编辑:程序博客网 时间:2024/05/17 22:22

设计与声明

让接口容易被正确使用,不容易被误用, 正确性,高效性,封装性,维护性,延展性,以及协议的一致性。

让接口容易被正确使用,不易被误用

1.应该做到接口不被误用,应该考虑客户可能做出什么样的错误。

class Date{public:    Date(int month, int day, int year);    //...};

上述的类的接口就不是好的设计方法,因为可能导致错误Date d(30,3,1995); Date d1(2,30,1995);,在类接口设计时,应该尽可能避免这种情况的调用发生。可以通过引入新类型进行预防接口被误用

struct Day{    explicit Day(int d):val(d){}    int val;};class Date{public:    Date(const Month& m, const Day& d, const Year& y);    //...};Date d(30,3,1995); //falseDate d(Day(30), Month(3), Year(1995));//falseDate d(Month(3),Day(30),Year(1995)); //true

明智而审慎的导入新类型对预防“接口被误用”有神奇疗效
2.保证正确使用的办法包括接口的一致性,以及与内置类型的行为兼容
除非有很好的理由,应该令你的types的行为与内置types一致。
3,阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
类型定义正确后,就应该限制其值在正确的范围内。办法一可以使用enum,但是要注意他不具备希望的类型安全性,应该可以把enum当作int来使用,另外一个方法是可以通过函数替换对象。应该良好的处理non-local static对象的初始化次序可能出现的问题。
4.shared_ptr 支持定制型的删除器,可以防止DLL问题,可能用来自动接触互斥锁。

设计class犹如设计type

1:新type的对象应该如何被创建和销毁。涉及到构造函数、析构函数以及内存分配函数和释放函数
2:对象的初始化和对象的赋值应该有什么样的差别。涉及到构造函数和赋值操作符的行为。
3:新type的对象如果被passed by value,意味着什么。 copy构造函数用来定义一个type的pass-by-value应该如何实现?
4:什么是新type的合法值。进行错误检测,边界值的检查,约束条件等。
5:新type需要配合某个继承图系嘛? 继承收到virtual或non-virtual函数的影响,特别是析构函数。
6:新type需要什么样的转换。如果需要显示类型转换,你就应该实现一个显示类型转换的函数。
7:什么样的操作符和函数对此新type而言是合理的?决定声明什么函数,哪些函数应该是member函数,某些是non-member函数。
8:什么样的标准函数应该驳回? 那些是应该声明为private 的。
9:谁该用新type的成员。 决定哪个成员为public、protected、private。决定哪个class或function应该是friend的,以及嵌套是否合理。
10:什么是新type的未声明接口?对效率、异常安全性以及资源运用提供何种保证?
11:新type有那么多一般化嘛? 是否需要template?
12:真的需要一个新的type嘛?

宁以pass-by-reference-to-const替换pass-by-value

缺省情况下c++以by value方式传递对象,传递的都是对象的一个副本,对于自定义对象来说,副本都是由构造函数产出,导致pass-by-value成为费时的操作。

class Person{public:    Person();    virtual ~Person();private:    std::string name;    std::string address;};class Student : public Person{public:    Student();    ~Student();private:    std::string schoolName;    std::string schoolAddress;  };bool validStudent(Student s);   //by valueStudent plato;bool platoIsOk = validateStudent(plato);

上述的函数在传值的时候一共调用了六次构造函数和六次析构函数,耗时巨大。而采用pass-by-reference-to-const 的方式传值可以避免构造函数和析构函数的调用

bool validateStudent(const Student &s);

通过by reference的方式传递参数可以避免slicing(对象切割)问题。

一般而言,可以合理的假设“pass-by-value”并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象

必须返回对象时,别妄想返回其reference

绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

class Rational{public:    Rational(int numerator = 0, int denominator = 1);private:    int n,d;    friend const Rational(const Rational& lhs, const Rational& rhs);  //by value};Rational a(1,2);Rational b(3,5);Rational c = a * b; //不合理

如果想要operator*返回一个reference指向如此数值,他必须自己创建那个Rational对象。

const Rational& operator*(const Rational& lhs, const Rational& rhs){    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);    return result;  //糟糕的代码 因为result是一个local对象,函数退出前被销毁了}const Rational& operator*(const Rational& lhs, const Rational& rhs){    Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);    return *result; //更糟糕的代码, 因为付出了一个构造函数的代价,并且没有delete}const Rational& operator*(const Rational& lhs, const Rational& rhs){    static Rational result;    result = Rational(lhs.n * rhs.n, lhs.d * rhs.d);    return result; //更糟糕的代码, 会导致多线程安全的问题。}bool operator==(const Rational &lhs, const Rational& rhs);Rational a,b,c,d;if((a*b) == (c*d)){}else{ }   //此操作if一直为true。因为operator*返回的是一个reference指向operator*内部定义的static Rational对象,但是static对象只有一个。

因此,一个必须返回新对象的函数的正确写法是:就让那个函数返回一个新对象。

inline const Rational operator*(const Rational& lhs, const Rational& rhs){    return Rational(lhs.n * rhs.n , lhs.d * rhs.d);}

将成员变量声明为private

将成员变量声明为private,可以赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
1.当要移除class内的public成员变量时,所有和class有关的代码都要重写
2.当要移除class内的protected成员变量时,所有使用他的derived class 都被破坏
3.当要移除class内的private时,只需要少量的修改对应使用的它的函数。

宁以non-member、non-friend替换member函数

对于封装:如果有东西被封装,它就不再可见。越多的东西被封装,越少的人可以看到它,越少的人可以看到它,我们就有很大的弹性去变化它,因为我们的改变仅仅直接影响看到改变的人事物。使得改变事物只影响有限的客户,越多函数可以访问它,数据的封装性就越低。上一条说过,成员变量应该是private的,因为如果它不是,就有无限量的函数可以访问他们。而能够访问private成员变量的只有class的member函数加上friend函数。因此如果你在一个member函数和一个non-member、non-friend函数之间做抉择的时候,而且两者提供相同的机能。那么导致较大封装性的是non-member、non-friend函数。因为它并不增加“能够访问class内的private成份”的函数数量。

若所有参数皆需类型转换,请为此采用non-member函数

class Rational{public:    Rational(int numerator = 0, int denominator = 1);    int numerator() const;    int denominator() const;private:};

如果想要上述class实现与有理数进行混合运算的函数,一般建议采用non-member函数。因为如果是member函数时,容易导致错误的调用。如

class Rational{public:    const Rational operator*(const Rational& rhs) const;};Rational one(1,8);Rational two(2,4);Rational result = one * two; //ture;result = result * one; //trueresult = one*2; //true;result = 2 * one;  //false

因为2没有class,也就没有operator*成员函数。result = one * 2;正确的原因是,因为存在一个隐式的类型转换。只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。为了解决这个问题,可以采用non-member函数进行处理:

class Rational{};const Rational operator*(const Rational& lhs, const Rational& rhs){    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());}result = one * 2; //true;result = 2 * one;//true;

无论何时如果你可以避免friend函数就该避免,不能够只因函数不该成为member,就自动让它成为friend
本条的真理,不是全部的真理。从oo-c++到template c++,让rational成为一个template class时,需要新的讨论。

考虑写出一个不抛出异常的swap函数

swap原本是STL的一部分,然后称为异常安全性编程的脊柱,以及采用处理自我赋值可能性的一种常见机制。
1.如果swap的缺省实现对你的class或class template提供可接受的效率,你不需要做任何事。任何尝试swap那种对象的人都会取得缺省版本,而那将有良好的运作。

namespace std{    template<typename T>    void swap(T& a, T& b){        T temp(a);        a = b;        b = temp;    }}

2.如果swap缺省实现的效率不足,(那几乎总是意味着你的class或template class使用了某种pointer to implementation手法),试着做以下事情:

class WidgetImp{public:    //...private:    int a, b,c;    std::vector<double> v;};class Widget{public:    Widget(const Widget& rhs);    Widget& operator=(const Widget& rhs){        *pImp = *(rhs.pImp);        //...    }private:    WidgetImp* pImp;};//置换两个Widget对象值时,只需要置换指针就行,但是使用缺省的swap算法,他会复制三个widget,同时还会复制三个widgetimp对象,效率低。

希望告诉std::swap函数,当widgets被置换时,真正该做的是置换内部的pimp指针。解决方法是:将std::swap针对widget特化。
通常我们不能够改变std命名空间的任何东西,但是可以为标准template制造特化版本,使他专属于我们自己的class
1)提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。

class Widget{public:    void swap(Widget& other){        using std::swap; //必要的        swap(pImp, other.pImp);    }};namespace std{    template<>  //全特化声明    void swap<Widget> (Widget& a, Widget& b){   //<Widget>意味针对这个class全特化        a.swap(b);    }};

class template情况时:

template<typename T>class WidgetImp{ //...};template<typename T>class Widget{ //...};namespace std{    template<typename T>    //错误,不合法    void swap<Widget<T>> (Widget<T>& a, Widget<T>& b){        a.swap(b);    }}//企图偏特化一个function template,但是c++只允许针对class template 偏特化, 在function templates上是行不通的。

2)在你的class 或 template 所在的命名空间提供一个non-member swap,并令它调用上述swap成员函数,不再将那个non-member swap声明为std::swap的特化版本或重载版本

//这个方法针对class或这template class都行的通namespace WidgetStuff{    template<typename T>    class Widget{ //...};    template<typename T>  //non-member swap 函数,不属于std命名空间    void swap(Widget<T>& a, Widget<T>& b){        a.swap(b);    }}

3)如果你编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。

template<typename T>void doSomething(T& obj1, T& obj2){    //...    swap(obj1,obj2);}

此时应该调用那个版本的swap函数?此时根据调用情况,决定调用哪个swap成员函数,但是为了很好的调用swap函数,应该把std::swap暴露出来:

template<typename T>void doSomethin(T& obj1, T& obj2){    using std::swap;    //...    swap(obj1,obj2);}

针对swap函数可以进行你需要的特定函数去实现,全特化,或者偏特化等。