`
qqsunkist
  • 浏览: 33015 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

有效创建一个类(三)

    博客分类:
  • C++
阅读更多
4. 类成员函数(改变第2种的)
设计类改变成员变量的成员函数,需要考虑的因素非常多,但是这些因素大致可以分为两类:一类是比较通用的,另一类呢就是有类体系的前提;

(1)是否真需要成为成员函数
(2)是否有必要返回对象?如果有必要返回对象,那么不要返回其引用
(3)函数参数宁以pass-by-reference-to-const传递替换pass-by-value
(4)是否需要提供一些适合类操作的运算符?如果是,那么提供哪些运算符重载是合理的?
(5)类型是否需要提供转换?
(6)类成员函数是否与类继承关系有关?如果有,什么样的继承关系?如何审慎应用继承关系

(1)~(3)应该可以被视为是第一类,通用性的考虑因素;后三者则该被视为是与类层级关系有关系的考虑因素;

(1)是否真的需要称为成员函数
代码摘自《Effective C++》条款23
class WebBrowser {
public:
  ...
  void clearCache();
  void clearHistory();
  void removeCookies();
  ...
  void clearEverything();
};

在上述的代码中表示了一个WebBrowser类,在这样一个类中提供了很多成员操作函数,如clearCache(), clearHistory(), removeCookies()等,其中许多用户想提供一个整体化执行这些上述三个操作的函数,因此又提供了一个clearEverything()的类成员函数;
当然,这个功能可以由另一个非成员函数调用适当的成员函数而实现:
void clearBrowser(WebBrowser& wb){
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

那么上面这两种实现,哪个更好呢?或许有些人会选择clearEverything()成员函数更好些,因为这样更符合面向对象设计原则(数据与数据操作绑定在一起)。实际上,这个选择并不是遵守想象中这条设计原则,因为这个选择带来了别clearBrowser更差的封装性。那么为什么这么说呢?

当一个类的提供了很多的成员函数可以用于操作或者改变private成员变量,那么意味着这个类没什么封装性可言。就此而言,clearBrowser()因为不是成员函数,并未给客户提供增加操作类私有成员变量的可能性,因此封装性比clearEverything()好。更重要的一点是non-member函数,或者non-friend函数可以为类设计提供更好包裹弹性(package flexibility)。

再考虑下面的代码
代码摘自《Effective C++》条款24
class Rational {
public:
  Rational(int numerator = 0, int denominator = 1);
  int numerator() const;
  int denominator() const;
  const Rational operator* (const Rational& rhs) const
private:
  ...
};

Rational oneEighth(1,8);
Rational oneHalf(1,2);
Rational result = oneHalf * oneEighth; //good
result = result * oneEighth; // good

result = oneHalf * 2; //good, but implicit type conversion occurs
result = 2 * oneHalf; //error! 

oneHalf是一个内含operator*函数的class的对象,所以编译器调用该函数。但是当整数2并没有相应的class,也就没有operator*成员函数。编译器也会尝试寻找可被调用的non-member operator*,可惜的是也没有;因此报错了。

如果上述的operator*成员函数以non-member函数的形式提供
const Rational operator*(const Rational& lhs, const Rational& rhs){
  return Rational(lhs.numerator() * rhs.numerator(), 
                  lhs.denomenator() * rhs.denominator());
}

这样上述result = 2 * oneHalf就通过编译了。

因此根据上述两个例子,考虑函数是否真的有必要称为成员函数;
《Effective C++》条款23, 24:
条款23:宁以non-member, non-friend替换member函数
条款24:若所有参数皆需类型转换,请为此采用non-member函数

(2)是否有必要返回对象?如果有必要返回对象,那么不要返回其引用
//代码摘自《Effective C++》条款21
const Rational& operator*(const Rational& lhs, 
                          const Rational& rhs){
  Rational result(lhs.n*rhs.n, lhs.d*rhs.d); //on-stack
  return result;
}

const Rational& operator(const Rational& lhs, 
                         const Rational& rhs){
  Rational *result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d); //on-heap
  return *result;
}

注意引用的语义只是另一个既有对象的别名;如果那个既有的对象已经被销毁了,或者其他的原因已经不存在了,再进行引用招致不必要的风险了。所以返回引用时,一定是在函数外已经创建,且函数可见的变量或者对象,而不是函数体内local的on-stack或者on-heap创建的函数局域变量或者对象;这是很危险的操作!

(3)函数参数宁以pass-by-reference-to-const传递替换pass-by-value
C++默认方式下,是通过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 validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);

每次以pass-by-value方式调用validateStudent的成本:
   调用Student的拷贝构造函数一次
   调用Person的拷贝构造函数一次
   Student对象内包含的两个string对象
   Person对象内包含的两个string对象
一旦使用完毕后,相应地还需要调用析构函数。因此总成本是6次构造函数,6次析构函数。

引用传递
bool validateStudent(const Student& s);

这种方式避免了不必要的6次构造函数和6次析构函数成本;但是注意到上述的引用传递中,更重要的是使用了const修饰了引用传递的参数;因为在值传递中,函数实际上是对参数的副本做操作,而引用传递中,函数直接操作参数本身,并非副本。为了避免函数对传进的参数有改动,使用const修饰,就不必担心validateStudent是否会改变传入的那个参数了。

另外以引用传递方式传递参数还可以避免对象切割问题。对于某些内置类型而言,pass-by-value实际上有可能比pass-by-reference效率更高些,为什么呢? 指针的调用的成本比起一些内置类型的拷贝成本更高,取决于机器instruction的位数,以及内置类型的设定长度。

(4)是否需要提供一些适合类操作的运算符?如果是,那么提供哪些运算符重载是合理的?
用户可以重载的运算符
+   -   *    /    %     ^      &
|   ~   !    =    <     >      +=
-=  *=  /=   %=   ^=    &=     |= 
<<  >>  >>=  <<=  ==    !=     <=
>=  &&  ||   ++   --    ->*    , 
->  []  ()   new  new[] delete delete[]


用户不可以重载的运算符
:: //scope resolution
.  //member selection
.* //member selection through pointer to member


二元运算符与一元运算符
× 一个二元运算符可以定义为取一个参数的非静态成员函数,也可以定义为取两个参数的非成员函数。
× 一个一元运算符可以定义为无参数的非静态成员函数,也可以定义为取一个参数的非成员函数;

那么根据上述运算符的定义,除了考虑需要提供哪些合理的类运算符之外,更需要考虑的就是这类运算符的提供应该以non-member的方式提供,还是以member的方式提供;(参考(1))

另外,那就是需要注意一些特殊意义的运算符以及相关的注意事项;
  I. operator=返回一个reference to *this; 需要注意处理"自我赋值"
II. operator(); // function call

I. operator= 返回一个reference to *this; 需要注意处理"自我赋值"
int x, y, z;
x = y = z = 15; 

为了实现上面的“连续赋值”,赋值运算符必须返回一个reference指向运算符左侧实参;
当然这个不仅适用于以上标准赋值形式,也适用于所有赋值相关运算。
class Widget { ... };
Widget w;
...
w = w;

或许上面的代码已然让我们感觉到惊讶!怎么会存有这样的赋值呢?当然会有,下面的这个就比较隐蔽了
a[i] = a[j]; //i==j 

如果运用对象管理资源,而且你可以确定所谓“资源管理对象”在copy发生时有正确的举措。这种情况下赋值运算符或许是“安全”的,不需要额外小心。但是如果在尝试自行管理资源时,很可能会掉进“在停止使用资源前意外释放了它”的陷阱。看下面的code
class Bitmap { ... };
class Widget {
  ...
private:
  Bitmap *pb;
};

Widget& Widget::operator=(const Widget& rhs){
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

如果上面的operator=函数内的*this和rhs是一个对象的话,那么delete不仅仅销毁了当前的pb,而且删除了rhs.pb;想象这样的后果会怎样?解决方法可以是identity test,也可以是copy and swap。
Widget& Widget::operator=(const Widget& rhs){
  if(this == &rhs) return *this; //identity test
  
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

/*
 * below is copy and swap
 */

class Widget {
  ...
  void swap(Widget& rhs);
  ...
};

Widget& Widget::operator=(const Widget& rhs){
  Widget temp(rhs);
  swap(temp);
  return *this;
}


II. operator() // 函数调用
相当于一个二元运算符@ : expression @ expression-list
重载函数调用运算符非常有用,提别是对定义那些只有一个运算的类型,和那些具有某个主导运算的类型。

函数调用运算符最明显或许也是最重要的应用是为了对象提供常规的函数调用语法形式,使它们具有像函数一样的行为方式。一个行为像函数的对象常被称为函数对象。
class Add {
  complex val;
public:
  Add(complex c):val(c){}
  Add(double r, double i){val=complex(r,i);}
  void operator()(complex& c) const { c += val; }
};

void h(vector<complex>& aa, list<complex>& ll, complex z){
  for_each(aa.begin(), aa.end(), Add(2,3));
  for_each(ll.begin(), ll.end(), Add(z));
}

(5)类型是否需要提供转换?
原则上我们并不希望转换,如果需要转换,那么尽量缩小转换的使用范围。其他原则同(4)。注意隐式类型转换。

(6)类成员函数是否与类继承关系有关?如果有,什么样的继承关系?如何审慎应用继承关系
继承关系在C++语言中,十分复杂。继承关系也有如下种类:

     I.公有继承【is a的关系,适用于base类的每件事也可以应用到其衍生类】
    II.保护继承
   III.私有继承【is implementated in terms of a 】
    IV.接口继承【纯虚函数】
     V.实现继承【非纯虚函数、已经实现的成员函数】
    VI.多重继承
   VII.虚继承  【钻石继承,虚基类】

在具体说到设计成员函数的考虑之前,有必要逐一说明一下以上的几种继承关系;

I. 公有继承
class Bird {
public:
  virtual void fly();
  ... 
};

class Penguin: public Bird {
  ...
};


class derived_class_name:public base_class_name

即表示公有继承;
切忌,在C++的语义中,公有继承表示着“is a”的逻辑关系。
如上述代码通过公有继承表示这样一个逻辑关系:“企鹅是一种鸟类“;虽然从动物学分类的角度看,这种逻辑关系是正确的,但是从C++语义上不仅仅单纯地表示了企鹅是一种鸟类,而且还表现出了更深一层的语义,那就是”企鹅会飞”这样一个虚假的事实,即使这是违背程序员意志的一种错误语义。

因此,在面向对象程序设计中涉及公有继承关系时,需要知道C++的语义不仅仅单纯地表示了“is a"这样一个逻辑关系,还表示了更深一层的语义:即基类可完成的动作,同样地可以应用到其子类中,因为每个衍生类对象也都是一个基类对象。

II. 保护继承
class drived_class_name:protected base_class_name

表示保护继承;
保护继承表现的逻辑关系不像公有继承和私有继承那样十分明确。从语义上说,这种继承关系允许子类知道它与基类的继承关系;

III. 私有继承
class drived_class_name:private base_class_name

表示私有继承;
私有继承表现的逻辑关系是”is implemented in terms of"。

在C#,JAVA语言中,默认的继承关系只有公有继承;(虽然通过compostion的方式可以表现出私有继承的逻辑关系)

IV. 接口继承与实现继承
C++中并没有abstract关键字表示该类是抽象类;但是如果类中含有纯虚函数,那么该类就是抽象类。
class Shape { //abstract class
public:
  virtual void rotate(int) = 0; //pure virtual function
  virtual void draw() = 0; // pure virtual function
  virtual bool is_closed() = 0; //pure virtual function
  // ...
};

抽象类不能实例化,因此必须通过继承,在子类覆盖纯虚函数后或者提供纯虚函数的实现,才可实例化子类对象。那么在抽象类中的纯虚函数就是接口继承,而非纯虚函数或者已经提供实现的函数就是实现继承。

JAVA,或C#语言中的interface关键字定义的接口类十分类似C++的虚基类(即只有纯虚函数声明的抽象类)。JAVA或C#的一个类可实现多接口,实际内部上就是多重继承。为什么这么说呢?原因在虚继承一节中具体说明。

VI.多重继承
多重继承,顾名思义,一个子类(衍生类)可以继承多个基类;例如:
class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

多重继承在一些特定的应用场景下,的确给我们带来了好处,但是总是有弊的一面,如果继承的基类中,有名称相冲突的成员函数,或者成员变量,都会导致子类在使用过程中的歧义。另外,像上面的代码中所表示的,IOFile中含有了两套File类的成员变量,这显然导致了不必要的空间浪费。因此,C++又引入了虚继承;

VI.虚继承  【钻石继承,虚基类】
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };

虚继承解决了因多重继承招致的基类成员变量重复的问题。但是虚继承也带来更复杂的问题与后果:
  • 虚继承的那些实例化的类对象通常体积要比那些非虚继承产生的类对象要大
  • 访问虚基类的成员变量时,也比访问非虚基类的成员变量速度慢
  • 虚基类的成本还包括其他方面。支配虚基类初始化的规则比起非虚基类的情况更复杂且不直观。

缺省情况下,虚基类的初始化责任是由继承体系中的最低层的衍生类负责,这暗示了若派生自虚基类而需要初始化,必须认知虚基类,不论那些虚基类距离多远。同时也暗示了当一个新的派生类加入到继承体系中,它有可能承担其虚基类的初始化责任。

注意,JAVA和C#的实现多接口的机制其实质就是多重虚继承;为了避免纯虚基类初始化责任,因此JAVA和C#不允许在接口类中含有任何数据。

通常前三种继承可以与后四种进行组合搭配起来。(哇!C++在这点上,为程序开发人员提供了很大的灵活性,但是作为程序开发人员来讲,必须为这种灵活性付出代价!增加了学习曲线和复杂度。不过这种复杂度一旦under control,那变庖丁解牛,游刃有余了!似乎这也是很多C++高手们骄傲的地方)。

书归正传,成员函数的设计一定要注意到所在类在类层级结构中扮演的角色,然后再考虑继承关系属于上述哪几种;最后,在特定继承关系下,考虑应该如何避免一些歧义的产生,如何避免高额的成本等等。以下请参考《Effective C++》提供一些考虑因素

  • 如果是基类的成员函数,那么是否需要用virtual声明?
  • 如果需要用virtual来声明能否可以用其他的选择替代virtual?(别忘记基类中应该提供virtual的析构函数)
  • 如果是衍生类中的成员函数,那么该成员函数是否是因为继承关系得来的?
  • 如果是,继承得来的是接口还是实现?如果是实现,则绝不要重新定义继承而来的函数实现,也绝不重新定义继承而来的缺省参数值。(参考《Effective C++》条款36,37)
  • 如果衍生类中成员函数与继承得来的成员函数名称相同,尽量避免遮掩继承来的名称;(参考《Effective C++》条款33)
0
0
分享到:
评论

相关推荐

    C++实验三.docx

    首先,我们需要定义一个名为`Time`的类,它包含三个私有成员变量`Hour`、`Minute`和`Second`,分别表示小时、分钟和秒。为了能够操作这些成员,我们需要提供公共的构造函数、析构函数以及用于设置、获取和显示时间...

    基于MFC的OpenGL三维图形类的创建

    3. **创建可重用的图形类**:为了方便地在不同的项目中重用OpenGL代码,可以创建一个或多个图形类。这些类应该具有足够的通用性,能够适应不同的应用场景。例如,可以创建一个`CGraphicObject`类,该类可以包含用于...

    实验三:Java类与对象

    其次,实验涉及到构造方法,这是类的一个特殊方法,用于初始化新创建的对象。`Monkey`类中有默认构造方法和带参数的构造方法,后者允许我们在创建对象时立即设置属性值。 接着,我们学习了如何创建和使用对象。在...

    一个简单的C#三层代码生成类

    这个"一个简单的C#三层代码生成类"就是为了解决在开发过程中重复编写这些层的代码而设计的工具。通过使用此类,开发者可以快速生成BLL(业务逻辑层)、DAL(数据访问层)和Model(模型层)的代码,大大提高了开发...

    java2 使用教程(第三版) 实验指导 上机实践3 类与对象

    在Java中,我们使用`class`关键字来创建类。例如,可以定义一个名为`Triangle`的类,表示三角形,包含边长和类型的属性,以及计算面积的方法。 2. **对象的创建**:通过类创建对象,使用`new`关键字和类的构造器。...

    后台添加三级产品菜单分类添加

    例如,当添加一个新的三级分类时,ASP脚本会向数据库发送SQL语句,创建新的记录,并将分类信息保存到相应的表格中。 实现三级菜单分类添加的功能,通常需要以下步骤: 1. 创建数据库表结构:设计数据库表,包含必要...

    java面向对象语言的实验报告

    - Java程序的源文件名必须与公共类名保持一致,并且一个Java源文件中只能有一个公共类。 2. Java程序的结构和开发过程 - Java应用程序的典型结构包括一个公共类和main方法,这是程序的入口点。 - 开发过程包括...

    26_多线程_第1天(Thread、线程创建、线程池)_讲义

    - **实现Runnable接口**:创建一个实现了Runnable接口的类,然后将其实例传递给Thread的构造器,同样可以启动新线程。这种方式更灵活,因为Java不支持多重继承,但可以与其他接口一起使用。 - **实现Callable接口...

    C++创建window服务

    文章强调了一个关键概念——使用一个特定的C++类来简化服务程序的开发过程。这种方法的核心优势在于,开发者只需要重写几个基本类中的虚拟函数就能实现自己的服务程序。此外,文中提供了三个示例源代码: - **NT...

    利用.NET Remoting技术创建的三层架构程序。

    在给定的"利用.NET Remoting技术创建的三层架构程序"案例中,我们将探讨如何通过.NET Remoting实现一个典型的三层架构,即表现层(UI)、业务逻辑层(BLL)和数据访问层(DAL)。 首先,我们要理解三层架构的基本...

    Eclipse中设置在创建新类时自动生成注释

    在模板编辑器中,可以看到一个名为`"${filecomment}"`的占位符,这是我们自定义文件级注释的关键位置。将光标置于该行后,可以插入自定义的注释模板。例如,参考提供的部分代码: ``` /** * @author E-mail: * @...

    有效处理JAVA异常三原则

    举例来说,JCheckbook类中的FileNotFoundException处理可以通过提示用户指定另一个文件名来明确地解决问题。相对的,不应该使用过于泛化的异常处理方式,如仅捕获Exception或更上层的类,并输出异常类名称或堆栈信息...

    定义一个学校的职工类

    在本Java上机实践中,我们主要探讨了面向对象编程中的类和继承概念,以及如何利用这些概念来构建一个学校职工类系统。这个系统包括教师、学生和后勤人员三个子类,它们都继承自一个抽象基类`People`,体现了公共特性...

    Delphi7创建及释放线程实例

    - 首先,创建一个新的单元,然后声明一个新的类,如`TMyThread`,继承自`TThread`。 - 在这个新类中,定义任何需要的数据成员,以存储线程所需的特定信息。 - 重写`Execute`方法,这是线程将执行的代码。在这个...

    jsp编程技巧 创建表示层之类

    例如,创建一个Servlet或Filter来处理请求,然后通过RequestDispatcher将控制权传递给JSP页面,以展示结果。 五、使用EL表达式和JSTL Expression Language (EL)和JavaServer Pages Standard Tag Library (JSTL)是...

    matlab 三维 数组 单元数组-创建分类数组 算法开发、数据可视化、数据分析以及数值计算 Matlab课程 教程 进阶 资源

    例如,`A = ones(3,3,3)`将创建一个3x3x3的全1数组。 2. 访问和修改元素:使用三个方括号`[]`来访问或修改三维数组的特定元素,如`A(:,:,2)`获取第二层的所有元素。 3. 三维数组操作:包括切片、转置、索引等,例如`...

    C#创建ini文件

    首先,你需要创建一个包含所需配置信息的字符串,然后将其写入文件。以下是一个简单的示例: ```csharp using System.IO; public void CreateIniFile(string filePath, string iniContent) { if (!File....

    创建jar并引入第三方包

    - **Extract required libraries into generated JAR**:这个选项会将所有依赖的第三方库解压并合并到新的JAR文件中,使其成为一个自包含的可执行文件。 - **Package required libraries into generated JAR**:这...

    判断三角形形状程序 判断三角形形状程序

    在给定的代码片段中,我们可以看到一个用Java编写的简单程序,其目的是判断由用户输入的三个边长能否构成一个三角形,以及如果能构成,那么它是什么类型的三角形。下面我们将深入解析这个程序的核心概念、逻辑流程...

    SQL实验:创建表并实施完整性,使用规则和缺省,更新表中数据.doc ) 您可以上传小于60MB的文件

    在SQL中,创建一个基本表的基本语法如下: ```sql CREATE TABLE 表名 ( 列名1 数据类型 约束, 列名2 数据类型 约束, ... ); ``` 其中,“表名”是您要创建的表的名称;“列名”表示表中的每一列的名称;“数据...

Global site tag (gtag.js) - Google Analytics