自学编程,从此开始

上第2学堂,听有趣的编程课

课文: 《你好,对象!派生版》 (点击查看完整内容:视频+评测+讨论+……)

作者:null

继承、变异,世界因此丰富多样。

课文题图

 

你好,对象!派生版

1. 从流程分支到类型分支

“派生”,全称“类型派生“,本质是一种分支,类似于”if/eles“这样的分支,只不过派生作用在类型上,“if/else”这样的条件分支,作用在流程上。二者立刻有了高下之分。

为什么上说派生是一种(类型)分支?正如视频里所说:“美人是人,不美的人也是人”。意思就是,原来只有一条个类型:“人/Person”,现在根据需要,在其下再分出一个“美女/Beauty”。在逻辑上,这不就是一个“if”分支吗?如果再有“丑人”、“怪人”、“懒人”等 子类型,那就是多级if/else的流程结构了。

问题来了,既然都是分支,为什么认定写基于流程的“if/else”分支就是“低”、写成派生就是“高”呢?回答之前先说一句:这里的高和低指的是逻辑抽象层面的高低之分,而不是本事的高低。我们并不能,也不应该 消灭各种流程分支,只是提醒所有新入门的程序员:不要以为只有流程分支可以控制逻辑。流程分支控制逻辑的特点是:它将所有逻辑揉在一起,比如视频中示例的:

//流程上的分支,所有逻辑在一个流程结构中出现:
void Person::Hello()
{
    if (name == XXX)
    {
        /* 这里是美女的逻辑 */
    }
    else
    {
        /* 这里是普通人的逻辑 */
    }

}

把所有逻辑揉在一起,中性一点说,叫集中在一起,并非完全坏事,比如:至少它能让逻辑表达更加清楚:因为都 写在一个地方了,你只要耐心从头看一遍,就能知道它在干什么。但是,如果分支很多,每个分支的内部实现又一大坨,那么把所有代码揉在一起,就是一个塞了太多东西的抽屉了,非常容易变得乱七八糟。人类天生是不喜欢把所有事混在一起处理。派生在类型上作分支,对代码的组织结构最基本的提升就是:把各自的逻辑分开单独实现:

void Person::Hello()
{
        /* 这里是普通人的逻辑 */   
}

void Beauty::Hello()
{
        /* 这里是美女的逻辑 */    
}

干净、清晰。别小看这个,在编程语言上,对逻辑剥离的追求真的是无止境。于是连派生本身,也仍然会在很多时候被嫌弃在逻辑剥离上,藕断丝连:剥离了代码结构,却留下了类型之间的牵连。于是很多语言(包括C++自身)都提出更加激进的方法(Google家的Go语言了解下)。将来我们会学习到,但本课一定要先从根子上理解派生在这方面做出的努力了(派生说:我尽力了)。

2. 继承与变异

剥离逻辑只是派生的最基本的本事 。同样如视频里所说,派生最主要的是反应了类型之间关系:美人也是人,Beauty应该在继承Person的基础来实现特定功能(Hello函数)的差异 。所以美人也有名字。以下是我们在上机训练时,所写代码的某一个瞬间:

struct Person
{
    std::string name;
    void Hello();
};

struct Beauty : Person
{
};

此时,看起来Beauty结构中空空如也,但其实它有name成员数据和Hello()成员函数。以下代码完全合法:

Beauty b;
b.name = "阮玲玉";
b.Hello();

合法的代码不一定有什么用处。我们定义Beauty的初衷是为了定制美女的问候行为。所以这才提到了“覆盖 / override”这个专业术语。不过更严格“覆盖”定义,要涉及到“虚函数”——这是我们下节课的内容——最容易的理解的是表达就是:我们在派生类重写了基类的某个同名函数,实质就是在派生类的范围内,重新定义了这个成员函数的行为。

为了让“继承”与“变异”都一样的明显,我们让Person多一个唱歌的函数,然后我们认为,美女在唱歌方面和普通人并没有多大区别——总不能说人美就一定歌甜嘛——于是在派生类,我们不重写这个成员:

struct Person
{
    std::string name;
    void Hello();
    void Sing();
};

struct Beauty : Person
{
    void Hello();
};
}

请大家给出一个版本的Sing的实现和基类及派生类两个版本实现,再加以测试 。对我来说,我写的Sing函数的实现如下:

void Person::Sing()
{
    cout << "@@#¥@#…………*&……%&¥#%" << endl;   
}

3. 派生类的生死过程

重点!所以把视频中的结论再说一次:

构造:派生类对象构造时,会先调用基类的构造函数,再调用自身的构造函数。

析构:派生类对象析构时,会先调用自身的析构函数,再调用基类的析构函数。

派生关系是可以多级的(爷类、父类、孙类……),以上过程也自然可以多级的。我们来写一个有趣的例子。阅读以下代码时,请注意对汉语拼音的复习:

#include <iostream>

using namespace std;

struct  ShaFaTie
{
     ShaFaTie()
     {
         cout << "哈哈,抢到沙发,笑抚二楼头。" << std::endl;
     }

     ~ShaFaTie()
     {
         cout << "我是一楼,结贴。" << std::endl;
     }
};


struct BanDengTie : ShaFaTie
{
BanDengTie()
    {
    cout << "\t" << "[回复]抢到板凳。羡慕一楼好舒服!笑看三楼坐地。" << std::endl;
    }

    ~BanDengTie()
    {
cout << "\t" << "我是二楼,结贴。" << std::endl;
    }    
};

struct DiBanTie : BanDengTie
{
DiBanTie()
    {
    cout << "\t\t" << "[回复]三楼怎么啦?席地而坐,天圆地方。" << std::endl;
    }

    ~DiBanTie()
    {
    cout << "\t\t" << "我是三楼,结贴。" << std::endl;
    }
};

int main()
{
    ShaFaTie L1;
    cout << "======================================" << std::endl;

    BanDengTie L2;
    cout << "======================================" << std::endl;

    DiBanTie L3;    
}

让二楼贴子继承一楼贴子,当然毫无道理:因为二楼贴子在类型上完全和一楼帖子无关,二楼就是二楼,二楼不是一种特殊的一楼。这个例子只是为了显示对象生死在类系中的对构造或析构的调用次序。这是本课作业,请完成并分析输出结果。