vlambda博客
学习文章列表

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

前言

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

派生类扩展父类的功能

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

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

int main(int argc, char **argv)
{
    Son s;

    s.it_skill();

    return 0;
}

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

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

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类中继承而来,代码如下所示:

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类之间的关系,那么假设现在有如下所示的一句代码:

Student s;
Person &p = s; 

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

void test_func(Person &p)
{
    p.printInfo();
}

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

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;
}

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

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

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

多重继承

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

class Sofa {
public:
    void watchTV(void) cout<<"watch TV"<<endl; }
};

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

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

class Sofabed : public Sofa, public Bed {
};

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

int main(int argc, char **argv)
{
    Sofabed s;
    s.watchTV();
    s.sleep();
    return 0;
}

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

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

存在的问题

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

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;}
};

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

class Sofabed : public Sofa,public bed
{

};

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

int main(int argc,char **argv)
{
    Sofabed s;
    s.setWeight(100); /* error */
    return 0;
}

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

s.Sofa::setWeight(100);

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

虚拟继承

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

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

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

class Furniture
{

private:
    int weight;
public:
    void setWeight(int weight) this->weight = weight; }
    int getWeight(void) const return weight; }
};

然后是 Sofa的类的实现:

class Sofa : virtual public Furniture
{
private:
    int a;
public
    void watchTV(void) {cout << "watch TV" << endl;}
};

紧接着是 bed的实现代码:

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

紧接着是 Sofabed的代码实现:

class Sofabed : public Sofa, public Bed {
private:
    int c;
};

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

int main(int argc,char **argv)
{
    Sofabed s;
    s.setweight(100);
    return 0;
}

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

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

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

再论构造函数的调用顺序

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

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

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

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,那么代码实现如下所示:

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的代码如下所示:

class Sofabed : public Sofa, public Bed 
{
private:
    int c;
public:
    Sofabed() { cout <<"Sofabed()"<<endl; }
};

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

class LeftRightCom 
{

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

class Date 
{

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

class Type 
{

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

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

class LeftRightSofabed : public Sofabed, public LeftRightCom {
private:
    Date date;
    Type type;

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

};

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

int main(int argc, char **argv)
{
    LeftRightSofabed s;
    return 0;
}

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

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

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

  • 1、先调用虚拟基类构造函数:按照继承顺序,只执行一次

  • 2、然后调用非虚拟基类构造函数,按照继承顺序

  • 3、然后是类的对象成员(按照声明的顺序)

  • 4、最后是类自己的构造函数

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

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

wenzi嵌入式软件
记录一个嵌入式软件技术爱好者的成长之路,专注于嵌入式软件开发,分享学习过程中的心得,记录项目开发中遇到的“坑”,主要涉及STM32,ucos,RT_Thread,Linux的相关知识。
38篇原创内容
Official Account