首页 > 代码库 > C++我们必须要熟悉的事之具体做法(3)——类的设计与声明

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而言全新的东西。

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