前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >适合具备 C 语言基础的 C++ 教程(六)

适合具备 C 语言基础的 C++ 教程(六)

作者头像
wenzid
发布2021-03-04 14:29:51
2660
发布2021-03-04 14:29:51
举报
文章被收录于专栏:wenzi嵌入式软件wenzi嵌入式软件

前言

在上一则教程中,着重讲述了派生类继承于父类之后的一些访问控制,在本次教程中,将介绍如下几个点:派生类扩展父类功能派生类的空间分布,以及多重继承的相关概念。

派生类扩展父类的功能

在前文所述的 Father类我们通常也称之为父类或者说称之为基类,而 Son类我们则称之为子类或者是派生类,我们知道通过public继承的方式Son类可以继承到父类的 it_skill,那么我们可不可以将这个继承得到的 it_skill发扬光大呢?实际上是可以的,用更加专业的话来讲就是覆写,也就是 override,代码如下所示:

代码语言:javascript
复制
class Son : public Father
{
private:
    int toy;
public:
    void paly_game(void)
    {
        cout << "son play game" << endl;
    }

    void it_skill(void)
    {
        cout << "son's it skill" << endl;
    }
};

注意上述的it_skillFather类的 it_skill是相同的一个函数,只是在 Son类里对这个函数进行了覆写,这个时候,如果向如下方式调用 it_skill,那么就会调用的是 Son类里面定义的 it_skill

代码语言:javascript
复制
int main(int argc, char **argv)
{
    Son s;

    s.it_skill();

    return 0;
}

派生类的空间分布(内存分布)

在讲述派生类的空间分布的时候,我们采用 Person类和 Student类进行阐述,首先 Person类具有如下的属性:

代码语言:javascript
复制
class Person
{
private:
    char *name;
    int age;
public:
    int address;

    void setName(char *name)
    {/*省略*/}
    void setAge(int age)
    {/*省略*/}

    Person(char *name, int age)
    {/*省略*/}

    ~Person()
    {/*省略*/}
};

然后,Student类以 public的方式从 Person类中继承而来,代码如下所示:

代码语言:javascript
复制
class Student : public Person
{
private:
    int grade;
public:
    void setGrade(int grade)
    {
        this->grade = grade;
    }

    int getGrade(void)
    {
        return this->grade;
    }

    /*override*/
    void printInfo(void)
    {
        cout << "Student";
        person::printfInfo();
    }
};

上述就是Student类以 public方式继承自 Person类的一个例子,因为 Student类中也存在其自身的私有数据成员,所以,总的来说,Person类和Student类之间的关系如下所示:

image-20210210215953484

通过上述的示意图可以清楚地知晓Student类和 Person类之间的关系,那么假设现在有如下所示的一句代码:

代码语言:javascript
复制
Student s;
Person &p = s; 

那么对于p来说,它引用的是 s里面继承于 Person里的那部分,为了更清楚地说明这个问题,我们编写如下所示的一个函数:

代码语言:javascript
复制
void test_func(Person &p)
{
    p.printInfo();
}

基于上述代码的基础上,我们继续来编写主函数代码:

代码语言:javascript
复制
int main(int argc, char **argv)
{
    Person p("lisi", 16);

    Student s;
    s.setName("zhangsan");
    s.setAge(16);

    test_func(p);
    test_func(s); 

    s.printInfo();

    return 0;
}

上述代码运行的结果如下图所示:

image-20210210220743410

通过上述地输出信息,也可以知道,在第十行代码中,test_fun(s) 实参传入地是 Student地实例化对象,但是在执行代码地时候,它执行的是Person类的 printInfo函数,也就是说,虽然传进去的是 Student的实例化对象,但是真正起作用的是实例化对象中继承自 Person类的那部分。

多重继承

多重继承也就如字面意思一样,就是说派生类继承自多个父类,这就称之为是多重继承,简单来说,就是一个派生类可以有多个基类。基于上面的叙述,我们用一个例子来说明,比如我们现在有如下两个基类:

代码语言:javascript
复制
class Sofa {
public:
    void watchTV(void) { cout<<"watch TV"<<endl; }
};

class Bed {
public:
    void sleep(void) { cout<<"sleep"<<endl; }
};

那我们现在有一个派生类从这两个基类继承而来,代码如下所示:

代码语言:javascript
复制
class Sofabed : public Sofa, public Bed {
};

既然是从两个基类中继承而来,自然也就满足在之前的教程里所述的访问控制的原则,并为继承所得到的派生类也满足基类所具备的一些特性:

代码语言:javascript
复制
int main(int argc, char **argv)
{
    Sofabed s;
    s.watchTV();
    s.sleep();
    return 0;
}

上述代码的执行结果也显而易见,执行结果如下所示:

image-20210219160149329

存在的问题

在上述的例子中,我们使用多重继承的时候,那么这样会带来什么问题呢?我们将上述的两个基类的数据成员和成员函数进行补充:

代码语言:javascript
复制
class Sofa
{
private:
    int weight;
public:
    void WatchTV(void) { cout << "watch TV" << endl;}
    void setWeight(int weight) {this->weight = weight;}
    void getWeight(void) {return this->weight;}
};

class bed
{
private:
    int weight;
public:
    void sleep(void) {cout << "sleep" << endl;}
    void setWeight(int weight) {this->weight = weight;}
    void getWeight(void) {return this->weight;}
};

上述代码我们又增添了两个基类的数据成员以及成员函数,这样也会出现一个问题。如果我们仍然像之前的那样继承的方式继承两个基类:

代码语言:javascript
复制
class Sofabed : public Sofa,public bed
{

};

这样子就会存在一个问题,如果我们在主函数这样子定义了一个实例化对象,并调用它的成员函数:

代码语言:javascript
复制
int main(int argc,char **argv)
{
    Sofabed s;
    s.setWeight(100); /* error */
    return 0;
}

这个时候sSofabed两个类中继承而来,那么自然也就具备了Sofabed的属性,但是这个时候实例化的 s调用 setWeight的时候,并不知道操作的是从哪里继承而来的weight,因此也就造成了错误(注释表明了error)。如果在不更改继承方式的前提下,也可以这样书写避免错误:

代码语言:javascript
复制
s.Sofa::setWeight(100);

但是这样书写一方面看着是存在冗余,一方面是实际上就多出来了一个无用的 weight。因此,为了解决上述的问题,就引入了虚拟继承的概念。

虚拟继承

为了改进上述的代码,引入虚拟继承的概念,在这里,我们多引入一个类,Furniture类,然后,Sofabed都虚拟继承自FurnitureSofabedSofabed中继承而来,下图是这几个类之间的一个关系图:

image-20210219162331887

从上图中我们看到Sofabed都是虚拟继承自Furniture,下面是各个类的代码实现:

代码语言:javascript
复制
class Furniture
{
private:
    int weight;
public:
    void setWeight(int weight) { this->weight = weight; }
    int getWeight(void) const { return weight; }
};

然后是 Sofa的类的实现:

代码语言:javascript
复制
class Sofa : virtual public Furniture
{
private:
    int a;
public:
    void watchTV(void) {cout << "watch TV" << endl;}
};

紧接着是 bed的实现代码:

代码语言:javascript
复制
class bed : virtual public Furniture
{
private:
    int b;
public:
    void sleep(void) {cout << "sleep" << endl;}
};

紧接着是 Sofabed的代码实现:

代码语言:javascript
复制
class Sofabed : public Sofa, public Bed {
private:
    int c;
};

然后,我们紧接着我们看主函数的代码:

代码语言:javascript
复制
int main(int argc,char **argv)
{
    Sofabed s;
    s.setweight(100);
    return 0;
}

上述代码就没有出错,为什么这样就没有出错呢,我们来看每个每个类以及实例化对象的空间分布:

image-20210219163214150

我们可以看到通过 virtual(虚拟)继承的方式,SofaBedweight公用的是一块内存空间,那么这个时候操作 s的时候也就不存在二义性了。

再论构造函数的调用顺序

在前面的教程中,已经多次提及了构造函数的执行顺序,接下来也有必要就此问题继续谈一下当存在多重继承时,以及存在虚拟继承时,这个时候构造函数的调用顺序又是怎么样的?同样的,我们采用打印消息的方式来了解这个执行过程,为了更好地说明这个问题,我们引入如下几个类:Furniture类,Vertification3c类,Sofa类、Bed类、SofaBed类、LeftRightCom类以及LeftRightSoftbed类,这几个类之间的关系如下所示:

image-20210219211646808

下面是这几个类的代码实现,首先是Furniture类和Vertifivation类的代码实现:

代码语言:javascript
复制
class Furniture 
{
private:
    int weight;
public:
    void setWeight(int weight) { this->weight = weight; }
    int getWeight(void) const { return weight; }
public:
    Furniture() { cout <<"Furniture()"<<endl; }
};

class Vertification3C 
{
public:
    Vertification3C() { cout <<"Vertification3C()"<<endl; }
};

由上述框图可以知道,SofaBed都是虚拟继承自FurnitureVertication3C,那么代码实现如下所示:

代码语言:javascript
复制
class Sofa : virtual public Furniture , virtual public Vertification3C
{
private:
    int a;
public:
    void watchTV(void) { cout<<"watch TV"<<endl; }
public:
    Sofa() { cout <<"Sofa()"<<endl; }
};

class Bed : virtual public Furniture, virtual public Vertification3C 
{
private:
    int b;
public:
    void sleep(void) { cout<<"sleep"<<endl; }
public:
    Bed() { cout <<"Bed()"<<endl; }
};

然后,我们又知道 Sofabed是继承自SofaBed,那么 Sofabed的代码如下所示:

代码语言:javascript
复制
class Sofabed : public Sofa, public Bed 
{
private:
    int c;
public:
    Sofabed() { cout <<"Sofabed()"<<endl; }
};

在上述基础上,我们继续来实现LeftRightCom类以及 Data类和Type类:

代码语言:javascript
复制
class LeftRightCom 
{
public:
    LeftRightCom() { cout <<"LeftRightCom()"<<endl; }
};

class Date 
{
public:
    Date() { cout <<"Date()"<<endl; }
};

class Type 
{
public:
    Type() { cout <<"Type()"<<endl; }
};

最后,我们来实现 LeftRightSofabed,代码实现如下所示:

代码语言:javascript
复制
class LeftRightSofabed : public Sofabed, public LeftRightCom {
private:
    Date date;
    Type type;

public:
    LeftRightSofabed() { cout <<"LeftRightSofabed()"<<endl; }

};

最后,为了弄清楚构造函数的调用顺序,我们在主函数里进行实例化对象:

代码语言:javascript
复制
int main(int argc, char **argv)
{
    LeftRightSofabed s;
    return 0;
}

上述代码的执行结果如下所示:

image-20210219212921346

在剖析构造函数的调用顺序之前,我们先明确如下几个原则:

  • 1、先调用虚拟基类构造函数:按照继承顺序,只执行一次
  • 2、然后调用非虚拟基类构造函数,按照继承顺序
  • 3、然后是类的对象成员(按照声明的顺序)
  • 4、最后是类自己的构造函数

按照上述这个分析原则,很容易就能够得出构造函数的执行顺序就是如运行结果所示,如果在 LeftRightSofabed类的定义中,我们将其更改为如下方式:

代码语言:javascript
复制
class LeftRightSofabed : public Sofabed, virtual public LeftRightCom {
private:
    Date date;
    Type type;

public:
    LeftRightSofabed() { cout <<"LeftRightSofabed()"<<endl; }

};

那么按照先调用虚拟基类构造函数的这个原则,构造函数的调用顺序如下所示:

image-20210219214038996

小结

本次分享的内容就是这些,本节所涉及到的代码可以到百度云链接中获取:

链接:https://pan.baidu.com/s/1ReW7F-2RAGGlVnHIycCI1w 提取码:lhru

如果您对我分享的内容感兴趣,欢迎关注我的个人公众号~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 wenzi嵌入式软件 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 派生类扩展父类的功能
  • 派生类的空间分布(内存分布)
  • 多重继承
  • 存在的问题
  • 虚拟继承
  • 再论构造函数的调用顺序
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档