第十二课

大部分的 Haiku 程序都是用 C++ 来编写的。作为 C 语言的一种延伸它是由贝尔实验室的 Dennis Ritchie 在 1970 完成的。我们到目前所学的语言基础对两者都适用。不过从今天开始,我们就要开始学一些 C++ 的特殊部分了。就是这些特殊部分让 C++ 变的这么无敌。不过为了真正能领悟到它的强大,我们必须先变换我们编程的思路。

面向对象编程

我们到目前为止所学习编写的程序其实是所谓的过程型编程(procedural programming)的一部分范例。它一种以单独函数调用为中心的编程思想。对于很多目的的达到,这个也不错,不过为了 Haiku 图形化用户界面的编程我们就得更进一步到面向对象编程的领域。

面向对象编程的想法就是一个应用程序是通过对象之间的互动来完成任务的。每一个类都是一个”黑盒子”,它包含一些属性和一些和这些属性互动的方法,而同时这些互动究竟是如何完成的却被隐藏起来。我们对一个对象认识只是知道如何和它交流,互动。现实中的例子就好像说一个电视机,按按钮可以换频道。更改内置菜单的一些设置可以改变音量或者开关电视。我们还知道它的大小,颜色和重量。不过除非你是个修电视的,不然你就不会知道电视机到底怎么工作或者去查看它真正的内部构造。

一个 C++ 对象就好像是一台电视机。它有函数,叫做方法 methods,方法就提供了和对象交流的方法。对象还保存了一些储存数据的变量叫做属性 properties。不是所有的方法和属性都有必要让它可以从这个对象之外被访问到的。

类 C

C++ 里的类型定义叫类(class),类的定义很像我们上节课所提到的结构体。这里定义了一个电视机的类作为例子:

class Television
{
public:    // 这里的所有函数都能被任何人访问。没有 public 关键字,
           // 所有的这些都会被当做内部方法
           // 默认的访问模式是 private


              // 这是一个构造函数,等会我们会讲到他
                Television(void);

              // 这是一个析构函数,等会也会讲到他
                ~Television(void);

    void        SetChannel(uint16 channel);
    uint16      GetChannel(void);
    void        SetVolume(uint8 volume);
    uint8       GetVolume(void);
    void        TogglePower(void);
    bool        IsTurnedOn(void);

private: // 下面的这些东西我们从外部是无法访问的

    int8        DetectActiveInput(void);
    void        ConnectToSignal(void);
    uint16      fChannel;
    uint8       fVolume;
    bool        fPowerState;
};

上面的代码酷似结构体的定义。它有比属性多很多的方法,还有两个怪词 public 和 private,和电视机工作息息相关的一些方法比如 ConnectToSignal 被放在这个类的 private 区域里,它们只能被 Television 类内部的其他方法调用。

这个类里有 6 个方法来设定属性或者获取属性的值似乎有点怪,如果我们单纯的把 fChannel,fVolume 和 fPowerState 都设置为公共的是否可以减少很多工作呢?是的!不过这会让和这个类一起工作的代码开上去很凌乱。我们的对象应该只有在绝对必要的时候才公开他的内部工作。这么做的好处是,我们可以在不妨碍到某个类和外部世界的交流的情况下修改它内部的工作方式。比如,如果我们想要把 fPowerState 改名为 fIsTurnOn ,那我们只需要修改这个类内部用到这个变量的地方,而外部不用改变任何东西。但如果我们把 fPowerState 改成公共的,那我们就必须在整个项目里把对它的所有引用的名字都修改一遍。如果我们的项目很大,那这么做的工作量也会很大。这种利用方法来隐藏属性的方式被叫做 数据抽象(Data Abstraction) ,这个词在 C++ 中你会经常听到。

调用一个对象的方法需要你能访问对象。结构体里我们用来访问变量的点和箭头在类里被用来访问类的方法和属性:

在类定义里另一个怪怪的地方是 Television() 和 ~Television 函数。他们没有任何返回值–连 void 都没有。是因为他们是很特殊的函数。Television() 被称为这个类的 构造函数(constructor) 。当我们创建一个新的 Television 对象时它就会被调用。~Television() 是这个类的 析构函数(destructor) 。当我们试图销毁一个该类对象的时候他就会被调用。他们不是必须品,但几乎所有类都会有一个构造函数,很多类会有析构函数。他们也在分配和释放内存空间时扮演重要角色。说道这个,我们来看看 C++ 里是如何使用堆空间的。我们马上就要用到它啦。

C++内存分配 new 和 delete

为了创建一个 Television 的对象,我们不用 malloc 函数。事实上,我们不太会使用它,除非我们在写一个 C 程序。我们会使用 new 和 delete,他们是 C++ 版的 malloc 和 free。

int
main(void)
{
    // 新建一个指向我们电视机对象的指针
    // 通过new分配足够的内存来存储这个对象
    // 然后调用它的构造函数
    Television *tv = new Television();

    // 像上一次一样,先得开电视啊
    if (!tv->IsTurnedOn())
        tv->TogglePower();

    // 新闻联播还是别看了,看看琼瑶剧吧,HOHO
    tv->SetChannel(172);

    // 释放电视机对象,就在这个对象的内存被释放之前
    // 它的析构函数会被调用
    delete tv;

    return 0;
}

使用 new 和 delete 来分配堆内存简单多了。事实上,对于对象来说它们是必须的。类的其他方法会导致对象的构造和/或析构函数永远不会被执行而引发各种各样的混乱。new 和 delete 也可以被使用在对象数组上,就好像下面这个例子:

int
main(void)
{
    // 创建一个电视机数组,我猜我们是不是要开个电器商店啊
    // 我们从100台开始吧,我们是不是需要个地方来卖啊 ;-)
    Television *tvArray = new Television[100];

    // 和以前一样,我们得先把电视打开,不过我们这次只开一台来看看它们都是一样的;-)
    if (!tvArray[0].IsTurnedOn())
        tvArray[0].TogglePower();

    // 如果听不到我们就不用管我们看的是啥频道了
    tvArray[0].SetVolume(0);

    // 释放我们的电视。当我们要释放我们从new中获取的数组的时候就要用中括号[]
    // 如果忘记写[],那就会造成内存泄露,因为那样只释放了其中一台电视的内存
    delete [] tvArray;

    return 0;
}

构造和析构

我们在我们的 Television 类里所看到的这两个怪函数是 C++ 语言本身的一部分。构造函数的职责就是完成所有有关对象初始化的任务。当我们分配空间给一个结构体时,结构体内部的变量的值是随机的。这对于对象也是一样的。不过构造函数可以帮助我们完成初始化。

如果一个类内部没有定义构造函数则会自动生成一个默认的构造函数。默认的构造函数没有参数也不做任何事情。我们的 Television 类定义了默认的构造函数。但这不是类的要求。当然最好是让构造函数做一点事情。类的构造函数还可以带参数。比如下面这样的或者其他的:

Television(const char *name);
Television(bool isHD);

类的析构函数看上去是一样的:一个 “~” 符号,类名,没有参数。和构造函数一样,如果一个类没有定义析构函数,也会自动生成一个。同样默认的也不会做任何事。析构函数的职责就是在对象被完全销毁之前完成清理善后的工作。大部分情况下,这意味着释放类内方法申请的内存空间。

作业

通过编写下列这些例子的类来练习面向对象编程的思路。包括方法和属性

  • 闹钟
  • 汽车
  • 炉子
  • 洗衣机