从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(中)

简介: 从C语言到C++_22(继承)多继承与菱形继承+笔试选择题

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(上):https://developer.aliyun.com/article/1521903


2.2 子类的拷贝构造函数

子类的拷贝构造函数必须调用父类的拷贝构造完成拷贝初始化:

#include <iostream>
using namespace std;
 
class Person 
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }
 
    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }
protected:
    string _name;
};
 
class Student : public Person 
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }
 
    Student(const Student& s)
        : Person(s)     // 子类的拷贝构造函数必须调用父类的拷贝构造完成拷贝初始化。
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }
protected:
    int _num;   // 学号
};
 
int main() 
{
    Student s1("GR", 19);
    Student s2(s1);
 
    return 0;
}


2.3 子类的赋值重载

子类的 operator= 必须要调用父类的 operator= 完成父类的复制:

#include <iostream>
using namespace std;
 
class Person 
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }
 
    Person& operator=(const Person& p) 
    {
        cout << "Person& operator=(const Person& p)" << endl;
        if (this != &p) {
            _name = p._name;
        }
        return *this;
    }
 
protected:
    string _name;
};
 
class Student : public Person 
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }
    Student& operator=(const Student& s) 
    {
        cout << "Student& operator=(const Student& s)" << endl;
        if (this != &s)
        {
            // 子类的 operator= 必须要调用父类的 operator= 完成父类的复制
            Person::operator=(s);
            _num = s._num;
        }
        return *this;
    }
 
protected:
    int _num;   // 学号
};
 
int main() 
{
    Student s1("GR", 19);
    Student s2("RL", 17);
    s1 = s2;
    return 0;
}


2.4 子类的析构函数

为了保证子类对象先清理子类成员再清理父类成员的顺序,先子后父。

子类析构先子后父,子类对象的析构清理是先调用子类析构再调父类析构。

子类析构函数完成后会自动调用父亲的析构函数,所以不需要我们显式调用。

且子类的析构的函数跟父类析构函数构成隐藏。

由于后面多态的需要,析构函数名字会统一处理成destructor()

(因为构成隐藏,所以非要显示调用的时候要加域作用符)因为要先析构子类,

所以不需要显示调用,编译器自动调用父类析构函数,这样才能保证先析构子类。

#include <iostream>
using namespace std;
 
class Person
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }
    ~Person() 
    {
        cout << "~Person()" << endl;
    }
 
protected:
    string _name;
};
 
class Student : public Person 
{
public:
    Student(const char* name, int num)
        : Person(name)  // 父类成员,调用自己的构造完成初始化
        , _num(num)
    {
        cout << "Student()" << endl;
    }
 
    ~Student() 
    {
        cout << "~Student()" << endl;
 
        //Person::~Person();  
        // 因为要先析构子类,所以不需要显示调用,编译器自动调用父类析构函数
    }
 
protected:
    int _num;   // 学号
};
 
int main() 
{
    Student s1("GR", 19);
 
    return 0;
}


2.5 小总结

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类

中,这几个成员函数是如何生成的呢?

  • 1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 3. 派生类的operator = 必须要调用基类的operator = 完成基类的复制。
  • 4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 5. 派生类对象初始化先调用基类构造再调派生类构造。
  • 6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  • 7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。


3. 继承与友元

友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。

#include<iostream>
using namespace std;
 
class Student;
class Person 
{
public:
  friend void Display(const Person& p, const Student& s);
protected:
  string _name; // 姓名
};
 
class Student : public Person 
{
protected:
  int _stuNum; // 学号
};
 
void Display(const Person& p, const Student& s)
{
  cout << p._name << endl;
  cout << s._stuNum << endl; // 错误  C2248 “Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)
}
 
void main()
{
  Person p;
  Student s;
  Display(p, s);
}

4. 继承与静态成员

父类定义了 static 静态成员,则整个继承体系里面中有一个这样的成员。

可以理解为共享,父类的静态成员可以在子类共享,父类和子类都能去访问它。

无论派生出多少个子类,都只有一个 static 成员实例:

和构造函数结合,我们就可以有以下应用:用 static 静态成员统计创建了多少个对象

#include <iostream>
using namespace std;
 
class Person
{
public:
  Person() 
  {
    ++_count;
  }
protected:
  string _name; // 姓名
public:
  static int _count; // 统计人的个数
};
 
int Person::_count = 0;
class Student : public Person
{
protected:
  int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
  string _seminarCourse;
};
 
int main()
{
  Student s1;
  Student s2;
  Student s3;
  Graduate s4;
  Person s5;
 
  cout << "大家都可以访问:(创建了多少个对象)" << endl;
  cout << "人数:" << Person::_count << endl;
  cout << "人数:" << Student::_count << endl;
  cout << "人数:" << s4._count << endl;
 
  cout << "顺便一个类把静态成员改变:" << endl;
  s3._count = 0;
  cout << "人数:" << Person::_count << endl;
 
  cout << "并且他们的地址也都是一样的,因为所有继承体系中只有一个" << endl;
  cout << "人数:" << &Person::_count << endl;
  cout << "人数:" << &Student::_count << endl;
  cout << "人数:" << &s4._count << endl;
 
  return 0;
}


5. 多继承与菱形继承

先说说单继承,刚才我们讲的其实就是单继承。

单继承:一个子类只有一个直接父类,我们称这种继承关系为单继承。

多继承:一个子类有两个或以上直接父类,我们称这种继承关系为多继承。

大佬早期设计的时候认为多继承挺好的,这也没有出现什么大的毛病。

因为一个子类继承多个父类的情况也挺合理的,比如有的角色,既是学生也是老师;

房车,既是房子也是车;但实际慢慢用起来后问题就慢慢显现出来了,

有多继承就会产生 "菱形继承"。


5.1 菱形继承的概念

菱形继承,又称钻石继承,是多继承的一种特殊情况。

举个例子:研究生助教继承了学生和老师,学生和老师又都继承了人

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

在Assistant的对象中Person成员会有两份。

#include <iostream>
using namespace std;
 
class Person 
{
public:
  string _name; // 姓名
};
 
class Student : public Person 
{
protected:
  int _num; //学号
};
 
class Teacher : public Person 
{
protected:
  int _id; // 职工编号
};
 
class Assistant : public Student, public Teacher
{
protected:
  string _majorCourse; // 主修课程
};
 
int main()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  //a._name = "peter"; // 错误  C2385 对“_name”的访问不明确
 
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "GR";
  a.Teacher::_name = "RL";
 
  return 0;
}

二义性通过指定作用域是可以解决的,告诉编译器是从学生那继承的还是从老师那继承的。

但是数据冗余没有解决,数据冗余带来的最大问题是空间的浪费:

class Person 
{
public:
  string _name;
  int _arr[1000];   // 数据越大,浪费越大
};


5.2 虚拟继承解决菱形继承问题

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。

如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

需要注意的是,虚拟继承不要在其他地方去使用。

代码:在类的腰部位置加一个 virtual 关键字:

class Person 
{
public:
  string _name;
  int _arr[1000];
};
 
// 虚继承
class Student : virtual public Person 
{
protected:
  int _num;
};
 
// 虚继承
class Teacher : virtual public Person 
{
protected:
  int _id;
};
 
class Assistant : public Student, public Teacher 
{
protected:
  string _majorCourse;
};

现在解决数据冗余的问题,那么它是具体怎么来做的,

这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,

这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到了A。


这里的B  C就像两个人分别在两个不同的地方,但是都要去A这个地方,保存了A的地址,那么就可以用地图去导航,但是因为两个人所在的地方不一样,所以到A的距离肯定也会不一样,也就是A的偏移量肯定会不一样(这样说会比较好理解一点)。


很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有 菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度上就有问题并且菱形继承虚继承也带来了性能上的损耗(因为多开了地址来存放偏移量)。多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java。

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(下):https://developer.aliyun.com/article/1521908?spm=a2c6h.13148508.setting.17.712b4f0eDngT44

目录
打赏
0
0
0
0
47
分享
相关文章
|
1月前
|
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
58 16
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
52 5
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
42 1
【C++】继承
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
119 0
|
4月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
73 1
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
34 0
|
1月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等