【C++篇】OOP中部分:继承和派生

news2024/11/15 7:11:23

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上


文章目录

  • 🚀一、继承和派生
    • ⛳(一)概述
      • 🎈1.派生和继承的实现
      • 🎈2.派生类(子类)对象的内存分布
    • ⛳(二)派生类和基类之间的特殊关系
      • 🎈1.访问权限:继承和派生的三种方式
      • 🎈2.指针指向
    • ⛳(三)子类
      • 🎈1.子类的构造函数
      • 🎈2.子类的析构函数
      • 🎈3.子类型
    • ⛳(四)多重继承
      • 🎈1.用法
      • 🎈2.多重继承的弊端:二义性
      • 🎈3.虚基类


🚀一、继承和派生

⛳(一)概述

父亲“派生”出儿子,儿子“继承”自父亲

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。派生和派生,本质是相同的,只是从不同的角度来描述。

在这里插入图片描述

🎈1.派生和继承的实现

在这里插入图片描述

Father.h

#pragma once
#include <string>

using namespace std;

class Father
{
public:
    Father(const char*name, int age);
    ~Father();
    	
    string getName();
    int getAge();
    string description();
private:
    int age;
    string name;
};

Father.cpp

#include "Father.h"
#include <sstream>
#include <iostream>


Father::Father(const char*name, int age)
{
    cout << __FUNCTION__ << endl;
    this->name = name;
    this->age = age;
}

Father::~Father()
{
}

string Father::getName() {
	return name;
}
int Father::getAge() {
	return age;
}

string Father::description() {
    stringstream ret;
    ret << "name:" << name << " age:" << age;
    return ret.str();
}

Son.h

#pragma once
#include "Father.h"
class Son : public Father {
public:
    Son(const char *name, int age, const char *game); //派生类需要自己的构造函数。
    ~Son();
    
    string getGame();
    string description();
private:
	string game;
};	

Son.cpp

#include "Son.h"
#include <iostream>
#include <sstream>

// 创建Son对象时, 会调用构造函数!
// 会先调用父类的构造函数, 用来初始化从父类继承的数据
// 再调用自己的构造函数, 用来初始化自己定义的数据
Son::Son(const char *name, int age, const char *game) : Father(name, age) {
    cout << __FUNCTION__ << endl;
    // 没有体现父类的构造函数, 那就会自动调用父类的默认构造函数!!!
    // 如果父类没有默认构造函数,如此例,那就需要使用初始化列表,用儿子的两个参数来显示调用父类构造函数进行初始化
    this->game = game;
}

Son::~Son() {
}

string Son::getGame() {
	return game;
}

string Son::description() {
    stringstream ret;
    // 子类的成员函数中, 不能访问从父类继承的private成员
    ret << "name:" << getName() << " age:" << getAge()
    << " game:" << game;
    return ret.str();
}

main.cpp

#include <iostream>
#include "Father.h"
#include "Son.h"

int main(void) {
    Father wjl("王健林", 68);
    Son wsc("王思聪", 32, "电竞");
    
    cout << wjl.description() << endl;
    
    // 子类对象调用方法时, 先在自己定义的方法中去寻找, 如果有, 就调用自己定义的方法
    // 如果找不到, 就到父类的方法中去找, 如果有, 就调用父类的这个同名方法
    // 如果还是找不到, 就是发生错误!
    cout << wsc.description() << endl;
    system("pause");
    return 0;
}
  • 除了“构造函数”和“析构函数”,父类的所有成员函数,以及数据成员,都会被子类继承!

  • 子类一般会添加自己的数据成员和成员函数, 或者, 重新定义从父类继承的方法!!! 子类对象就会调用自己重新定义的方法, 不会调用父类的同名方法,是一种静态的多态,即在派生类中重新定义基类的方法。还有一种动态的多态:虚方法,在后面会讲

    注意:

    1. 如果子类调用父类的同名方法,方法中涉及到的数据也是父类中的并不是子类中的

    2. 重新定义从父类继承的方法并不是重载,即如果重新定义继承的方法,应确保与原来的原型完全相同,只是函数体实现不同,如果原型不同,例如:

      class Father
      {
      public:
      	virtual void showperks(int a) const;
      ...
      };
      
      class Son : public Father {
      public:
      	virtual void showperks() const;
      ...
      };	
      

      新定义将showperks( )定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。

  • _FUNCTION_ 获取函数签名,假如它是一个成员函数,它的类名和const/volatile限定符也将是签名的一部分。

🎈2.派生类(子类)对象的内存分布

vs使用技巧:

设置 vs 编译器,在命令行中添加选项(打印指定类的内存分布):

/d1 reportSingleClassLayoutFather /d1 reportSingleClassLayoutSon

在这里插入图片描述

重新生成:

在这里插入图片描述

测试:

int main(void) {
    Father wjl("王健林", 68);
    Son wsc("王思聪", 32, "电竞");


    cout << sizeof(wjl.age) << endl;
    cout << sizeof(wjl.name) << endl;

    cout << sizeof(wjl) << endl; // 48

    cout << endl;

    cout << sizeof(wsc.age) << endl;
    cout << sizeof(wsc.name) << endl;
    cout << sizeof(wsc.getGame()) << endl;

    cout << sizeof(wsc) << endl; // 88

    system("pause");
    return 0;
}

在这里插入图片描述

在这里插入图片描述

  • 注意涉及到内存对齐
  • 成员函数不占用对象的内存空间,但是也被子类继承了!!!

⛳(二)派生类和基类之间的特殊关系

🎈1.访问权限:继承和派生的三种方式

(1)public(公有)继承 [使用最频繁]

父类中定义的成员(数据成员和函数成员)被继承后,访问权限不变!

  • public --> public

  • protected --> protected

  • private --> private

(2)private(私有)继承

父类中定义的成员(数据成员和函数成员)被继承后,访问权限都变成 private

  • public --> private

  • protected --> private

  • private --> private

(3)protected(保护)继承

  • public --> protected

  • protected --> protected

  • private --> private

小结:public 继承全不变,private 继承全变私,protected 继承只把 public 降级为 protected

子类对父类成员的访问权限:

无论通过什么方式(public、protected、private)继承,在子类内部均可访问父类中的 public、protected 成员。

private 成员不可访问(如果想要子类能够访问,就定义为 protected),使用构造函数不能直接设置继承的成员,而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。

继承方式只影响外界通过子类对父类成员的访问权限。

  • public 继承,父类成员的访问权限全部保留至子类;

  • protected 继承,父类 public 成员的访问权限在子类中降至 protected;

  • private 继承,父类 public、protected 成员的访问权限在子类中均降至 private。

在这里插入图片描述

🎈2.指针指向

基类指针可以在不进行显式类型转换的情况下指向派生类对象,那么同样,基类引用可以在不进行显式类型转换的情况下引用派生类对象:

Father father("陈七",20);
Son son("陈启",18,"王者");

Father * man = &son;
Father & man = son;
  • 注意:基类指针或引用只能用于调用基类方法,这涉及到之后要讲的多态

  • 通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针

    上述规则是有道理的。例如,如果允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法。因为派生类继承了基类的方法,所以这样做不会出现问题。如果可以将基类对象赋给派生类引用,将发生什么情况呢?派生类引用能够为基对象调用派生类方法,这样做将出现问题。例如,将方法Son::getGame() 用于Father对象是没有意义的,因为Father对象没有rating成员。

  • 在函数参数中使用派生类引用或指针,在传参时,可以传递基类对象,也可以传递派生类对象

  • 能够将基类对象初始化为派生类对象:

    Son son("陈七",18,"王者");
    Father father(son);
    

    该构造函数原型:

    Father(const Son &);
    

    类定义中没有这样的构造函数,但存在隐式复制构造函数:

    Father(const Father &);
    

    形参是基类引用,因此它可以引用派生类。这样,将father初始化为son时,将要使用该构造函数,它复制age和name成员。换句话来说,它将father初始化为嵌套在son中的Father对象。即用派生类对象初始化基类对象时,只有该派生类对象的基类部分会被拷贝,派生类部分将会被忽略掉。

    同样,也可以将派生对象赋给基类对象:

    Son son("陈七",18,"王者");
    Father father;
    father = son;
    

    在这种情况下,程序将使用隐式重载赋值运算符:

    Father & operatpr=(const Father&) const;
    
  • 在使用基类指针时,此指针调用的方法是基类的,即使指针指向子类,要使用子类的就涉及到多态问题

⛳(三)子类

🎈1.子类的构造函数

调用父类的哪个构造函数

class Son : public Father {

public:

    // 在子类的构造函数中,显式调用父类的构造函数
    Son(const char *name, int age, const char *game):Father(name, age) {
        this->game = game;
    }

    // 没有显式的调用父类的构造函数,那么会自动调用父类的默认构造函数
    Son(const char *name, const char *game){
        this->game = game;
    }

    ......
};
  • 创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如:

    此例中,需要提供两个相同的参数const char *name, int age来使用父类的构造函数,即Father(name, age) 是成员初始化列表,调用Father构造函数,Son构造函数将实参"王思聪"和2赋给形参name,age,然后将这些参数作为实参传递给Father构造函数,后者将创建一个嵌套Father对象,并将数据"王思聪"和2存储在该对象中。然后,程序进入Son构造函数体,完成Son对象的创建,并将参数game的值赋给game成员

  • 如果省略成员初始化列表,必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,除非要使用默认构造函数,否则应显式调用正确的基类构造函数。

  • 也可以使用复制构造函数来作为初始化列表:

    RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp):TableTennisPlayer (tp)
    {
    	rating = r;
    }
    

    这里将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数:

    TableTennisPlayer (tp)
    

    首先,由于tp的类型为TableTennisPlayer &,因此将调用基类的复制构造函数。基类没有定义复制构造函数,但编译器将自动生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配(string成员确实使用了动态内存分配,但本书前面说过,成员复制将使用string类的复制构造函数来复制string成员)。

1.总之呢,构建子类必须要有个父类,要继承父类初始化好的数据,根据参数列表中使用哪种父类的构造函数,就需要在子类构造函数中提供对应的参数,

  • 比如 Father(name, age)就需要提供const char *name, int age,子类构造函数体中管自己的game就行了,
  • 或者TableTennisPlayer (tp)使用拷贝构造函数,就需要提供const TableTennisPlayer & tp参数,到时候传递个已经构造好的父类进来就行,
  • 再或者不用参数化列表,会调用默认构造函数,参数只有一个自己的game就行

如果要想子类继承和我们定义好的父类实例一样的数据,可以都使用默认构造函数,或者子类的参数列表使用拷贝构造函数

2.实际上的初始化列表:

子类构造函数(数据类型 数值1,数据类型 数值2):父类构造函数(),变量名1(数值1),变量名2(数值2){}

  • 在构造函数执行时,先执行初始化列表,实现变量的初始化,然后再执行函数内部的语句
  • 成员初始化的顺序只与声明的顺序有关,而跟初始化列表的顺序无关。
  • 当类成员中有引用和const常量时就一定得初始化,否则会报错,

3.注意:

先使用父类构造函数初始化父类数据成员,以便于子类继承,但只是初始化数据,并未创建一个父类实例对象(此问题暂定)

4.当使用已有子类对象来初始化子类对象时:

前面讲的初始化方式都是普通的创建子类,所以都是涉及到关于构造函数以及它的初始化列表使用哪种父类构造函数先初始化父类,但是这里就涉及初始化方式采用已有子类对象初始化,涉及到的是子类的拷贝构造函数,所以我们应在子类的拷贝构造函数中使用初始化列表方式,先决定使用哪种构造函数方式初始化父类

  • 同样,如果不指定,会先调用父类的默认构造函数。再调用子类的拷贝构造函数

  • 显示调用父类拷贝构造函数,则是先调用父类拷贝构造函数, 再调用子类的拷贝构造函数:

    Son::Son(const Son& z):Father(z)
    {
        cout << "子类拷贝构造函数被调用";
        game = z.game;
    }
    

注意:我们使用子类构造函数时候,一般只会设置自己类的值,而不管从父类继承下来的值,这可能会导致我们拷贝时,最后的子类当中从父类继承下来的值不符合我们的预期,如下图,我们拷贝后的wsc1的age和name与wsc并不一样

因为我们并没有在子类拷贝构造函数中设置这两个值,用的是父类构造函数继承下来的两个值

Father::Father()
{
    cout << "父类默认构造函数被调用" << endl;
    this->name = "陈三";
    this->age = 18;
}

Son::Son(const Son& z)
{
    cout << "子类拷贝构造函数被调用";
    game = z.game;
    //name = z.name;  这样过后就不会有问题了
    //age = z.age;
}

int main(void) {
    Son wsc("王思聪", 32, "电竞");

    Son wsc1 = wsc;
    cout << wsc1.age << endl << wsc1.name << endl << wsc1.getGame() << endl;

    system("pause");
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J40pLML6-1687363674600)(E:\create\图片\C++\1\42.png)]

子类和父类的构造函数的调用顺序

当创建子类对象时, 构造函数的调用顺序:

静态数据成员的构造函数 -> 父类的构造函数 -> 非静态的数据成员的构造函数 -> 自己的构造函数

注意:

无论创建几个对象, 该类的静态成员只构建一次, 所以静态成员的构造函数只调用 1 次!!!

#include <iostream>
#include <Windows.h>

using namespace std;

class M {
public:
    M() {
    	cout << __FUNCTION__ << endl;
    }
};

class N {
public:
    N() {
    	cout << __FUNCTION__ << endl;
    }
};

class A {
public:
    A() {
    	cout << __FUNCTION__ << endl;
    }
};
	

class B : public A {
public:
    B() {
    	cout << __FUNCTION__ << endl;
    }
private:
    M m1;
    M m2;
    static N ms;
};

N B::ms; //静态成员


int main(void) {
    B b;
    system("pause");
}

执行:

  • N::N 静态数据成员的构造函数
  • A::A 父类的构造函数
  • M::M 非静态数据成员的构造函数
  • M::M 非静态数据成员的构造函数
  • B::B 自己的构造函数

🎈2.子类的析构函数

子类的析构函数的调用顺序,和子类的构造函数的调用顺序相反!!!记住,相反即可

#include <iostream>
#include <Windows.h>

using namespace std;

class M {
public:
    M() {
    	cout << __FUNCTION__ << endl;
    }
    
    ~M() {
    	cout << __FUNCTION__ << endl;
    }
};
		
class N {
public:
    N() {
    	cout << __FUNCTION__ << endl;
    }
    
    ~N() {
    	cout << __FUNCTION__ << endl;
    }
};

class A {
public:
    A() {
    	cout << __FUNCTION__ << endl;
    }
    
    ~A() {
    	cout << __FUNCTION__ << endl;
    }
};

class B : public A {
public:
    B() {
    	cout << __FUNCTION__ << endl;
    }
    ~B() {
    cout << __FUNCTION__ << endl;
}
private:
    M m1;
    M m2;
    static N ms;
};

N B::ms; //静态成员

int main(void) {
    {
    B b;
    cout << endl;
    }
    
	system("pause");
}

执行:

N::N
A::A
M::M
M::M
B::B

B::~B
M::~M
M::~M
A::~A

🎈3.子类型

公有继承时,派生类的对象可以作为基类的对象处理,派生类是基类的子类型。

在这里插入图片描述

#include <iostream>

using namespace std;
class A {
public:
    A() {}
    ~A() {}
    void kill() { cout << "A kill." << endl; }
};

class B : public A {
public:
    B(){}
    ~B(){}
    void kill() { cout << "B kill." << endl; }
};

void test(A a) {
	a.kill(); //调用的是A类对象的kill方法
}

int main(void) {
    A a;
    B b;
    test(a);  //A Kill
    test(b);  //A Kill
    
    system("pause");
    return 0;
}
  • 子类型关系具有单向传递性:C 类是 B 类的子类型而 B 类是 A 类的子类型,C 类也是 A 类的子类型

  • 如果把继承方式改为 protected 继承,或 private 继承,就会导致编译失败!

  • 在需要父类对象的任何地方, 可以使用”公有派生”的子类的对象来替代,从而可以使用相同的函数统一处理基类对象和公有派生类对象

    即:形参为基类对象时,实参可以是派生类对象(但反过来是不行的):

    #include <iostream>
    #include <sstream>
    
    using namespace std;
    
    class Father {
    public:
        void play() {
        cout << "KTV唱歌!" << endl;
    	}
    };
    
    class Son : public Father {
    public:
    	void play() {
    	cout << "今晚吃鸡!" << endl;
    	}
    };
    
    void party(Father *f1, Father *f2) {
        f1->play();
        f2->play();
    }
    
    int main(void) {
        Father yangKang;
        Son yangGuo;
        
        party(&yangKang, &yangGuo);  //都将执行KTV唱歌!
        
        system("pause");
        return 0;
    }
    
  • 子类型的应用:

    //1.基类(父类)的指针,可以指向这个类的公有派生类(子类型)对象:
    Son yangGuo;
    Father * f = &yangGuo;
    
    //2. 公有派生类(子类型)的对象可以初始化基类的引用
    Son yangGuo;
    Father &f2 = yangGuo;
    
    //3. 公有派生类的对象可以赋值给基类的对象
    Son yangGuo;
    Father f1 = yangGuo;
    

    如上所有的都是以父类为基础,都只能使用父类的相关方法,数据成员也只会将子类中包含的那个父类的所有数据给复制过来

⛳(四)多重继承

多继承/多重继承:

一个派生类可以有两个或多个基类(父类)。多重继承在中小型项目中较少使用,在 Java、C#等语言中直接取消多继承, 以避免复杂性.

🎈1.用法

将多个基类用逗号隔开.

实例:

例如已声明了类 A、类 B 和类 C,那么可以这样来声明派生类 D:

class D: public A, private B, protected C{
	//类 D 自己新增加的成员
};

D 是多继承形式的派生类,有 3 个父类(基类)它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。根据不同的继承方式获取 A、B、C 中的成员.

多继承的构造函数:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
	//其他操作
}

Father.h

#pragma once
#include <string>

class Father
{
public:
    Father(const char *lastName="无姓", const char *firstName="无名");
    ~Father();

	void playBasketball(); //打篮球
protected:
    std::string lastName; //姓
    std::string firstName; //名
};

Father.cpp

#include "Father.h"
#include <iostream>

Father::Father(const char *lastName, const char *firstName)
{
    this->lastName = lastName;
    this->firstName = firstName;
}

Father::~Father()
{
}

void Father::playBasketball() {
	std::cout << "呦呦, 我要三步上篮了!" << std::endl;
}

Mother.h

#pragma once
#include <string>

class Mother
{
public:
    Mother(const char * food,const char *lastName = "无姓", const char *firstName = "无名");
    ~Mother();
    
    void dance();
private:
    std::string lastName; //姓
    std::string firstName; //名
    std::string food; //喜欢的食物
};

Mother.cpp

#include "Mother.h"
#include <iostream>

Mother::Mother(const char *food,const char *lastName, const char *firstName)
{
    this->food = food;
    this->lastName = lastName;
    this->firstName = firstName;
}

Mother::~Mother()
{
}

void Mother::dance()
{
	std::cout << "一起跳舞吧, 一二三四, 二二三四..." << std::endl;
}

Son.h

#pragma once
#include "Father.h"
#include "Mother.h"

class Son : public Father, public Mother {
public:
    Son(const char *lastName, const char *firstName,const char *food,const char *game);
	~Son();
	void playGame();
private:
	std::string game;
};

Son.cpp

#include "Son.h"
#include <iostream>

Son::Son(
    const char *lastName, const char *firstName,
    const char *food,
    const char *game)
	:Father(lastName, firstName), Mother(food)
{
	this->game = game;
}

Son::~Son()
{
}

void Son::playGame()
{
	std::cout << "一起玩" << game << "吧..." << std::endl;
}

main.cpp

#include <Windows.h>
#include "Son.h"

int main(void) {
    Son wsc("川菜", "王", "思聪", "电竞");
    wsc.playBasketball();
    wsc.dance();
    wsc.playGame();
    
    system("pause");
	return 0;
}

多继承的构造函数的调用顺序:

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同:

//先调用Father的构造函数,再调用Mother的
class Son : public Father, public Mother 
{
}

🎈2.多重继承的弊端:二义性

如果我们在父亲类中增加一个跳舞方法:

Father.h

#pragma once
#include <string>

class Father
{
public:
    Father(const char *lastName="无姓", const char *firstName="无名");
    ~Father();

	void playBasketball(); //打篮球
    void dance();  //跳舞
protected:
    std::string lastName; //姓
    std::string firstName; //名
};

Father.cpp

#include "Father.h"
#include <iostream>

Father::Father(const char *lastName, const char *firstName)
{
    this->lastName = lastName;
    this->firstName = firstName;
}

Father::~Father()
{
}

void Father::playBasketball() {
	std::cout << "呦呦, 我要三步上篮了!" << std::endl;
}

void Father::dance() {
	std::cout << "嘿嘿, 我要跳霹雳舞!" << std::endl;
}

Son.h

#pragma once
#include "Father.h"
#include "Mother.h"

class Son : public Father, public Mother {
public:
    Son(const char *lastName, const char *firstName,const char *food,const char *game);
	~Son();
	
	void playGame();
	void dance();  //增加个跳舞方法
private:
	std::string game;
};

Son.cpp

#include "Son.h"
#include <iostream>

Son::Son(
    const char *lastName, const char *firstName,
    const char *food,
    const char *game)
	:Father(lastName, firstName), Mother(food)
{
	this->game = game;
}

Son::~Son()
{
}

void Son::playGame()
{
	std::cout << "一起玩" << game << "吧..." << std::endl;
}

void Son::dance() {
    Father::dance();
    Mother::dance();  //这里同时调用父母的dance()方法
    std::cout << "霍霍, 我们来跳街舞吧! " << std::endl;
}	

main.cpp

#include <Windows.h>
#include "Son.h"

int main(void) {
    Son wsc("川菜", "王", "思聪", "电竞");
    wsc.playBasketball();
    
    // 解决多重继承的二义性的方法1:
    // 使用 "类名::" 进行指定, 指定调用从哪个基类继承的方法!
    wsc.Father::dance();
    wsc.Mother::dance();

    // 解决多重继承的二义性的方法2:
    // 在子类中重新实现这个同名方法, 并在这个方法内部, 使用基类名进行限定,
    // 来调用对应的基类方法
    wsc.dance();

    wsc.playGame();
    
    system("pause");
	return 0;
}

🎈3.虚基类

多重继承除了在同名方法上存在二义性之外,在同名数据成员上也存在二义性问题

在这里插入图片描述

#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;

// 电话类
class Tel {
public:
    Tel() {
    	this->number = "未知";
    }
protected:
	string number; //电话号码;
};

// 座机类
class FixedLine : public Tel {
};

// 手机类
class MobilePhone :public Tel {
};

// 无线座机
class WirelessTel :public FixedLine, public MobilePhone {
public:
    void setNumber(const char *number) {
        //this->number = number; //错误, 指定不明确
        this->FixedLine::number = number; //this可以省略
    }
    
    string getNumber() {
        //return MobilePhone::number;
        return MobilePhone::number;
    }
};

int main(void) {
    WirelessTel phone;
    phone.setNumber("13243879166");    //这里设置的是继承自FixedLine的number
    cout << phone.getNumber() << endl; //要获取继承自MobilePhone的number,打印未知
    system("pause");
    return 0;
}

座机和手机都继承自电话类,都继承了一个number数据成员,而无线座机有多重继承自座机和手机,将它们各自的number都继承了下来,以至于无线座机类有两个number数据成员,造成二义性

解决方法:

(1)使用基类名进行限定

(2)使用虚基类和虚继承:

#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;

// 电话类
class Tel {
public:
    Tel() {
    	this->number = "未知";
    }
protected:
	string number; //电话号码;
};

// 座机类
class FixedLine : virtual public Tel {
};


// 手机类
class MobilePhone :virtual public Tel {
};

// 无线座机
class WirelessTel :public FixedLine, public MobilePhone {
public:
    void setNumber(const char *number) {
        this->number = number; //直接访问number
    }
    
    string getNumber() {
		return this->number;  //直接访问number
    }
};

int main(void) {
    WirelessTel phone;
    phone.setNumber("13243879166");   
    cout << phone.getNumber() << endl; 
    system("pause");
    return 0;
}
  • 座机和手机都虚继承自电话类,此时他们共同的基类,就成为虚基类
  • 而如果改成虚继承,由这两个派生出的子类就会只有一个number成员,从本质上说,两个子类对象共享一个number成员,

小结:尽量不要使用多重继承(多继承),部分高级语言已停止使用多重继承


行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/673885.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年05月份青少年软件编程Python等级考试试卷六级真题(含答案)

2023-05 Python六级真题 分数&#xff1a;100 题数&#xff1a;38 测试时长&#xff1a;60min 一、单选题(共25题&#xff0c;共50分) 1. 明明每天坚持背英语单词&#xff0c;他建立了英语单词错题本文件“mistakes.txt”&#xff0c;将每天记错的单词增加到该文件中&#x…

ROS:launch文件演示

目录 前言一、添加launch文件夹二、新建launch文件三、编辑launch内容四、 执行文件 前言 一个程序中可能需要启动多个节点&#xff0c;比如:ROS 内置的小乌龟案例&#xff0c;如果要控制乌龟运动&#xff0c;要启动多个窗口&#xff0c;分别启动 roscore、乌龟界面节点、键盘…

【数据分享】1929-2022年全球站点的逐日降雪深度数据(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 对于具体到监测站点的气象数据&#xff0c;之前我们分享过1929-2022年全球气象…

感应电动机起动动态计算(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

#家庭网络配置(华为路由与交换机)

#家庭网络配置 #三层交换机配置 #打开HDCP功能 #进入系统视图 <Huawei>system-view #关闭系统提示信息 [Huawei]undo info-center enable #打开DHCP 使能 [Huawei]dhcp enable #创建vlan 10 vlan 20 vlan 2 [Huawei]vlan batch 2 10 20 #进入 vlan 并配置ip 与 dhcp […

【机器学习】十大算法之一 “神经网络”

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,活动,python领域博主爱笑的男孩。擅长深度学习,活动,python,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typeblog个…

使用Postman模拟文件上传和下载

介绍 Postman是一款强大的HTTP请求模拟工具&#xff0c;它可以帮我们在没有前端界面的情况下模拟发送HTTP请求&#xff0c;非常适合API开发和测试。在本篇文章中&#xff0c;我将向你展示如何使用Postman进行文件的上传和下载。 文件上传 启动Postman&#xff0c;点击“新建请…

Linux的基础操作

0.前言 您好&#xff0c;这里是limou3434的一篇关于Linux基础操作的博文。感兴趣的话&#xff0c;可以看看我的其他博文系列。本次我给您带来的是Linux下的几个基础命令&#xff0c;学会这些命令您就可以开始在Linux管理和操作一些文件了。 注意&#xff1a;本次演示环境是在…

【HTML】常用标签

文章目录 1.标题字标签h1-h62.段落标签p3.换行标签br4.格式化标签5.图片标签6.超链接标签a7.表格标签单元格合并行合并列合并 8.无序列表9.有序列表10.自定义列表11.表单标签11.1 form标签11.2 表单控件11.2.1 input标签11.2.2 label标签11.2.3 select标签11.2.4 textarea标签 …

2023年为什么你需要学习GPU?

2023年为什么你需要学习GPU&#xff1f;掌握这一绝对重要的技能&#xff0c;开启你的未来之旅&#xff01; 亲爱的朋友们&#xff0c;时光飞逝&#xff0c;2023年已经悄然而至。在这个充满机遇和挑战的数字时代&#xff0c;学习GPU成为了你无法忽视的重要任务。为什么呢&#x…

《大话设计模式》笔记

1. 简单工厂模式 1.1 面向对象的好处 考虑通过封装、继承、多态把程序的耦合度降低&#xff0c;使用设计模式使得程序更加的灵活&#xff0c;容易修改&#xff0c;并且易于复用。 1.2 复制 Vs 复用 有人说初级程序员的工作就是 CtrlC 和 CtrlV&#xff0c;这其实是非常不好…

通过僵尸扫描判断目标主机端口开放状态

环境准备 使用scapy工具发包 攻击主机IP: 192.168.133.66 僵尸主机IP: 192.168.133.67 &#xff0c;僵尸主机不主动与任何机器通信&#xff0c;且满足IP数据包ID自增** 注&#xff1a;用nmap 判断主机是否满足僵尸主机条件&#xff0c;一般早期的windwos XP系统IP数据包ID…

MySQL优化--主从同步,分库分表

目录 MySQL主从同步原理 MySQL主从复制的核心就是二进制日志 面试回答 分库分表 分库分表的时机 分库分表的策略 垂直分库 垂直分表 水平分库 水平分表 分库分表的策略 分库之后的问题 面试回答 MySQL主从同步原理 如果项目上线了&#xff0c;通常情况下&#xf…

【Python开发】FastAPI 11:构建多文件应用

以往的文件都是将对外接口写在一个文件里边&#xff0c;而作为应用来说&#xff0c;接口是不可避免分散到多个文件中的&#xff0c;比如某文件负责注册登录模块&#xff0c;某文件负责内管模块&#xff0c;某文件负责业务模块等。FastAPI 也提供了APIRouter 这一工具来进行灵活…

kafka消息队列的初步探索

消息队列的作用就是提高运行速度&#xff0c;防止线程堵塞。 kafka的作用 异步 通过在消息队列发送消息的方式&#xff0c;将对应的业务作为监听者&#xff0c;此时我们只需要考虑发送消息的时间即可&#xff0c;大大提高了运行的速度。 解耦 如果使用原来的直接调用对应业务的…

【libdatachannel】pycharm运行streamer的信令服务及streamer与js客户端联调

启动py服务器 ssl必须额外指定 # Usage: ./server.py [[host:]port] [SSL certificate file]文档给出了服务的启动命令&#xff1a; python3 -m http.server --bind 127.0.0.1 8080 直接运行&#xff1a; python的信令服务 #!/usr/bin/env python # # Python signaling server…

需求分析引言:架构漫谈(二)非功能性需求

上一篇文章&#xff0c;简要介绍了架构的概念和架构设计流程&#xff0c;并简单介绍了需求分析的内容&#xff0c; 并在最后指出&#xff1a;需求分析的产出物&#xff0c;要包括非功能性需求&#xff0c;常见的非功能性需求如下&#xff1a; 完成任务的速度结果的精度操作的安…

MySQL实战解析底层---为什么表数据删掉一半,表文件大小不变

目录 前言 参数innodb_file_per_table 数据删除流程 重建表 Online 和 inplace 前言 数据库占用空间太大&#xff0c;我把一个最大的表删掉了一半的数据&#xff0c;怎么表文件的大小还是没变&#xff1f;这与数据库表的空间回收有关这里还是针对MySQL中应用最广泛的InnoD…

结构型设计模式07-享元模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 享元模式 1、享元模式介绍 享元模式是一种结构型设计模式&#xff0c;旨在**通过共享对象来减少内存使…

CSS弹性布局常用设置

目录 一、单位元素 二、弹性容器 三、常用属性 三、项目实战效果 一、单位元素 vm 1vm 为视口的1% vh 视口高的1% vmin 参照长边 vmax 参照长边 rem 等比缩放 需要设置最外层盒子html设置vw 根字号html的--- font-- 1vm 去适配 初始化 //初始化*{padding: 0;margin: 0}//…