从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

目录
相关文章
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
18 0
|
1月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
30 0
|
1月前
|
程序员 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
32 0
|
9天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1