• C++我们必须要熟悉的事之具体做法(3)——类的设计与声明


    1. 让接口被正确使用

    最重要的方法是:保持与内置类型的一致性。

    方法1:外覆类型(wrapper types)

    例如在需要年月日时,使用

    struct day {

    explicit day(int d) : val(d) { }

    private:

       int val;

    };

    方法2:函数替代对象

    class month {

    public:

             static month jan() { return month(1); }

    private:

             explicit month(int);    //禁止生成新的月

             …

    };

    month::jan();等等

    方法3:返回至限制为cont作为右值

    方法4: 返回指针时,返回shared_ptr类型

    testclass *create();虽然使用shared_ptr可以避免delete,但是最好像这样申明。

    shared_ptr<testclass> create();

    这个方法我们可以指定删除器,还可以“cross-DLL problem”(在一个DLL中new在另外一个DLL中delete)。

    2. 设计类时就像设计一个type

    类代表了一个新的类型和新的作用域。

    需要考虑的问题:

    (1) 是否需要新type

    可能使用derived class、一个或多个non-member函数或者模板就能达到要求。

    (2) 一般性

    是否要作为class template。

    (3) 新的类型如何创建和销毁

    这影响类的构造函数、析构函数。

    首先考虑我们需不需要自己写构造函数:当类含有普通的内置类型、指针时需要我们自己编写构造函数。

    自己编写构造函数: 要考虑是否要构造基类、是否为explicit、参数传递是否const、是否要&。

    往往当类:含有指针、需要复制资源时需要析构函数,我们可能可以通过智能避免需要自己写析构函数。

    (4)对象的复制控制

    copy构造函数的行为、copy assign的行为,这里需要考虑是否有继承,要复制基类的数据。

    追重要的是考虑我们是否需要这两个函数。

    若不需要我们就明确禁止它们。

    若需要,则考虑默认实现是否能满足我们的要求,若可以则不必写。

    一般当需要析构函数时,也需要它们两个。

    (5) 是否需要转换

    若禁止其他类型转化为本类类型,则可使单实参的构造函数为explicit。

    若类需要转换为其他类型,考虑重载operator。

    (6) 新类型的合法值

    这个主要涉及到某些成员函数(构造函数、赋值操作符和setter函数等)的错误检测。

    (7) 类的继承关系

    若类继承了某些类或者作为基类,则需要考虑哪些成员函数需要为virtual、哪些不需要。

    作为基类时,往往要使析构函数为virtual。

    (8)考虑数据成员

    哪些为public、protect、private。

    非需要继承的都为private、否则protect。

    是否static成员、是否const成员、是否是&。

    (9) 考虑函数

    哪些函数需要成为它的成员函数、哪些非成员、哪些函数和类是friend。

    我们需要哪些成员函数,哪些作为public、哪些作为protect、private。

    函数接口:是否为virtual、是否const成员函数、形参是否const是否要&、返回值是否const是否&。

    (10) 未声明接口

    对效率、异常安全性、资源运用提供什么保证,这些保证将为你的类加上相应的约束条件。

    3. 考虑reference-to-const作为参数传递

    在by value传递参数时,传递的是副本,对于类设计到类的复制构造函数、析构函数。带来额外的开销。

    我们多数情况下应该by reference-to-const作为参数传递,这样有两个好处:

    (1) 避免了复制和析构。提高了效率。const一般是必须的,这使我们不能更改参数,同时const&可以绑定到右值。

    (2) 可以避免slicing(对象切割)问题。by value方式传递参数时,若派生类传递给基类时会发生切割,只传递了基类的部分。

    误解:有的人认为小的对象应该使用by value方式传递。

    理由:(1) 小类型复制构造函数代价不高比如一个指针,但是我们复制这种对象却要“复制那些指针所指的每样东西”,代价可能就高了。

    (2) 某些编译器拒绝将用户自定义类型放入缓存中,可能降低效率。而引用往往是用指针实现的,传递引用通常意味着真正传递的是指针。而指针肯定会被放入到缓存中。

    (3) 小型类型作为一个用户自定义的类型,其大小可能会变化,将来可能会变大。甚至在不同的编译器中大小可能都不同,如:string的不同实现在不同编译器中的大小可能不同。

    但是有些参数适合by value方式传递:包括内置类型(c语言中就是这么做的)、STL迭代器(一个智能指针)、函数对象(一个定义了operator()的类)。

    其他的往往都是传递引用比较好。

    4. 不是所有函数都可以返回引用,该返回对象时就应该这么做!

      引用指向并不存在的对象肯定会造成错误,例如指向函数内部的局部变量等等。

    对于有些函数我们妄想返回引用,肯定是错误的。无论是指向内部new的对象(谁来delete的问题)、一个static变量(函数的多次调用结果却是相同的)等等。

    这些函数往往的特征是需要满足参数满足交换率,例如+、*、==等等。

    这些函数往往都是类的non member函数(因为要满足交换律,左右参数都需要实现隐式类型转换)、但是却是friend(需要访问了的成员函数)的函数。

    它们只能返回对象不能返回引用,因为它们不是成员函数,没有this指针引用不能只想类内部的数据成员。

    往往作为成员函数的函数可以返回引用,例如:&operator[], opreator*等等。

    5. 类相关的函数何时成为non-member

    这些相关函数往往就是类的需要operator的函数,往往是在所有参数都需要类型转换时成为non member函数,典型的是operator+、operator*(乘号)、operaotr==等等。

    这些函数需要满足交换率,两边都需要隐式类型转换。而只有参数类表中的形参才会被执行隐式转换,this指针绝不会执行隐式类型转换。

    我们往往令这些函数为non member函数,不需要是友元,而且若要访问成员变量而可通过成员函数,而且他们的构造函数必须不能是explicit。

    同时也证明了:若不能成为member函数,应该作为non member函数,而不是成为friend函数。

    但是在template编程中,opreator+等重载函数设为friend,但是目的却不是为了访问其数据成员,而是模板实例化所必须的。

    6. 成员函数应该声明为private

    此时通过成员函数访问数据成员成为唯一的方式,可以满足语法的一致性。

    使用成员函数可以让你对成员变量的处理有更精确地控制。若成员变量为public这样就可能被无限多的函数访问它,我们就不能控制了。

    最最重要的是:封装:将成员变量隐藏在函数接口的背后,可以为所有可能的实现提供弹性。当我们更改成员函数的不同实现形式时,不必重新修改函数接口,可以从一个较好实现中受益。

    若果你对客户隐藏成员变量(就是封装它们),你可以确保class的约束条件总会获得维护,因为只有成员函数可以修改它们,确保了你日后变更实现的权利。

    同样的道理也适用于protected,包括语法一致性、细微划分之访问控制和封装。

    “成员的封装性”与“当其内容改变是可能造成的代码破坏量”成反比。

    对于public成员变量,取消时所有使用它的客户码都会被破坏,只是一个不可知的量。

    对于protected成员变量,所有使用它的derived类都会被破坏,往往也是一个不可知的量。

    所以protected成员变量也想public一样缺乏封装性。

    因此从封装的角度,只有两种权限:private(封装)和其他(不封装)。

    7. 用non-member、non-member替换member函数

    当可以用类的成员函数组合成一个功能函数时,或者说提供相同的功能时,是把这个函数作为non-member、non-friend更好,而不是member。

    作为成员函数,则多了一个成员函数访问数据成员,降低了类的封装性。

    数据的封装性可用:越多函数可访问它,封装性越低来粗略衡量。

    我们可以将non-member函数可以放入多了头文件但是隶属同一个命名空间中。命名空间可以跨越多个源文件,客户可以扩充这种函数。

    这也是STL的组织形式。

    8.不抛出异常的swap函数

    swap是异常安全性的脊柱、处理自我赋值的常用机制, 原本只是STL的一部分。

    8.1. 最典型的实现为:

    #include <utility>

    template<typename T>

    void swap(T &lhs, T &rhs)

    {

           T tmp(move(lhs));     //move语义,tmp值变成lhs的值,lhs变成默认构造下的值。一般对于内置类型不变,但是如string等会变为空“”。

            lhs = move(rhs);

            rhs = (tmp);
    }

    需要满足:copy构造函数、copy assignment操作符。

    对于所有STL容器类型,都会有一个成员函数的swap,并在std中完全特化一个swap调用STL成员的swap。

    例如:对于vector,内部会定义了一个成员函数

    template<typename T>

    void vector<T>::swap(vector<T> &rhs)

    {

    //仅仅交换内部指针

             start = rhs.start;

             finish = rhs.finish;

             end_of_storage = rhs.end_of_storage;
    }

    而在std中会定义一个完全特化版本:

    namespace std {

             template<typename T>

             void swap<vector>(vector<T> &lhs, vector<T> &rhs)

             {

                    //调用内部版本

                     lhs.swap(rhs);
            }
    };

    8.2. 普通类中实现swap

    对于那种以指针指向一个对象,内含真正数据的类型,也就是使用“pimpl”(pointer to implementation)使用指针去实现的方式最需要自己的swap。

    例如对于类:

    class testclass {

    public:

           testclass(const testclass &rhs);

           testclass& opreator=(const testclass &rhs);

    private:

           bigclass *pbc;      //指针所指对象复制需要花时间
    };

    类似于STL的做法:

    在类内定义成员swap(), 在std中定义特化版本。

    对于std命名空间我们不允许改变空间内的任何东西,但是我们可以为标准模板(如swap)制造特化版本。

    class testclass {

    public:

    void swap(testclass &rhs)

    {

          using std::swap;

           swap(pbc, rhs.pbc);        //置换对象我们只是置换指针
    }

    private:

           bigclass *pbc;      //指针所指对象复制需要花时间
    };

    namespace std {

            //完全特化版本

             template<typename T>

             void swap<testclass>(testclass &lhs, testclass &rhs)

             {

                    //调用内部版本

                     lhs.swap(rhs);
            }
    };

    8.3. 类模板的swap

    对于类模板

    template<typename T>
    testclass {          //内含swap
    }:

    由于函数模板不支持偏特化,只有类模板支持。所以不能定义下面的这种类型:

    namespace std {

            //偏特化版本:不支持,错误的。

             template<typename T>

             void swap<testclass<T> >(testclass<T> &lhs, testclass<T> &rhs)

             {

                     lhs.swap(rhs);
            }
    };

    正确的做法是定义一个swap的重载版本,但是不能放在std中,我们允许在std中添加东西,只能完全特化其中的模板。

    可以将swap放在我们自己的命名空间中。

    namespace mystd {

    template<typename T>    
    testclass {       //内含swap
    }:

    //一个重载版本

       template<typename T>

             void swap (testclass<T> &lhs, testclass<T> &rhs)

             {

                     lhs.swap(rhs);
            }
    };

    在swap(testclass1, testclass2);时,根据C++名称查找法则(name lookup rules)具体的是argument-dependent-lookup或Koenig-lookup法则会找到mystd中的testclass专属版本。

    使用的方式:

    template<typename T>

    void dosomething(T &obj1, T &obj2)

    {

             using std::swap;              //使可以使用STL中swap

             swap(obj1, obj2);            //根据实参相依查找:(1) 在全局作用于或者obj所在命名空间查找专用的swap(模板类需要重载版本);

                                                 //(2) 在std中查找swap的特化版本(对于普通类)。(3) 使用swap的一般化版本。
    }

    总结:

    (1) 当std::swap效率不高时(往往是class或template class使用了pimpl手法),考虑提供一个public成员swap成员函数,

    让它高效的置换你的类型和两个对象值,并确保这个函数不抛出异常。因为swap最好的应用是提供强烈的异常安全性,在这里不能抛出异常。

    (2)对于class或这template应该提供一个non-member swap来调用member swap。对于class还应该提供一个完全特化的std::swap。

    (3) 调用swap,使用using 声明式,以便使std::swap在你的函数内曝光课件,然后不加任何命名空间修饰符的使用swap。

    (4) 为“用户自定义类型”进行std namespace全特化是好的,但是不要在std内加入std而言全新的东西。

  • 相关阅读:
    Jquery揭秘系列:实现$.fn.extend 和$.extend函数
    小谈Jquery框架
    js实现可拖动Div
    WebApp 九宫格抽奖简易demo
    原生js实现autocomplete插件
    扩展RadioButtonListFor和CheckBoxListFor
    关于js的回调函数的一点看法
    原生js实现fadein 和 fadeout
    QlikView sheet权限
    asp.net MVC 文件流导出Excel
  • 原文地址:https://www.cnblogs.com/hancm/p/3973806.html
Copyright © 2020-2023  润新知