C++中的继承/虚继承原理

简介: C++中的继承/虚继承原理

C++中的继承

1.继承的概念和定义

继承是一种提高代码复用率的重要方式,它允许程序员保持原有类的特性的基础上去增加其他特性、功能,这样的类叫做派生类继承是类设计层次的复用

class Person
{
public:
  void Print()
  {
    cout << "name: " << _name << endl;
    cout << "age: " << _age << endl;
  }
protected:
  string _name = "牡丹";
  int _age = 18;
};
class Student :public Person
{
protected:
  int _stuid;
};
class Teacher :public Person
{
protected:
  int _jobid;
};
int main(void)
{
  Student s;
  Teacher t;
  s.Print();
  t.Print();
  return 0;
}

运行结果为:

明明Student和Teacher当中都没有Print()函数啊,为什么还是正常运行呢?因为它们都继承了Person类。

1.1 继承定义

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

1.12 继承关系和访问限定符

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见(隐藏) 在派生类中不可见(隐藏) 在派生类中不可见(隐藏)

总结

1.基类的private成员在派生类当中无论是什么继承方式,都是不可见的,是隐藏的,在类外,在派生类当中都是无法调用到private的成员,都是隐藏的。

2.private成员在派生类中是不能被访问的,但是如果想要在类外不能被访问,但是在派生类当中被访问到就可以使用protected

3.public > protected > private,基类的其他成员在子类的访问方式 == 权限小的那个

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的

写出继承方式

5.在实际运用中一般使用都是publicb继承,几乎很少使用protetced/private继承,也不提倡使用

protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中

扩展维护性不强。

2.基类和派生类对象的复制转换

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。有一种很形象的说法叫做切片或者切割。就是把派生类当中父类的那部分切来赋值过去。
  • 基类的对象不能赋值给派生类
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才
    是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来
    进行识别后进行安全转换。

class Person
{
protected:
  string _name; // 姓名
  string _sex; // 性别
  int _age; // 年龄
};
class Student : public Person
{
public:
  int _No; // 学号
};
void Test()
{
  Student sobj;
  // 1.子类对象可以赋值给父类对象/指针/引用
  Person pobj = sobj;//Person = Student
  Person* pp = &sobj;//Person = Student
  Person& rp = sobj;//Person = Student
  //2.基类对象不能赋值给派生类对象
  sobj = pobj;//Student = Person
  // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
  pp = &sobj;
  Student * ps1 = (Student*)pp; // 这种情况转换时可以的。Student* = (Student*)Person*
  ps1->_No = 10;
  pp = &pobj;//Person =Person
  Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
  ps2->_No = 10;
}

3.继承中的作用域

1.在继承体系中基类派生类都有独立的作用域

2.子类和父类中有同名的成员,子类成员和父类成员的关系叫做隐藏,如果非要访问父类的成员,可以添加访问限定符显示访问

3.函数只需要函数名相同就能构成隐藏,参数什么的无关。

4.最好不要定义同名的成员

class Person
{
public:
protected:
  string _name="牡丹";
  int _num=18;
};
class Student :public Person
{
public:
  void Print()
  {
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
    cout << "学号:" << Person::_num << endl;
  }
protected:
  int _num = 520;
};
int main(void)
{
  Student s1;
  s1.Print();
  return 0;
}

运行结果:

class Person
{
public:
  void Print(int a)
  {
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
  }
protected:
  string _name="牡丹";
  int _num=18;
};
class Student :public Person
{
public:
  void Print()
  {
    Person::Print(1);
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
    cout << "学号:" << Person::_num << endl;
  }
protected:
  int _num = 520;
};
int main(void)
{
  Student s1;
  s1.Print();
  return 0;
}

这里构成的是函数的隐藏而不是重载,因为不在同一个作用域

4.派生类的默认成员函数

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

成员函数是如何生成的呢?

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

2.派生类的拷贝构造必须调用基类的拷贝构造完成基类的初始化。

3.派生类的operator=必须要调用基类的operator=完成基类的复制。

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类

对象先清理派生类成员再清理基类成员的顺序。

5.派生类对象初始化先调用基类构造再调派生类构造。

6.派生类对象析构清理先调用派生类析构再调基类的析构。

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;
  }
  Person& operator=(const Person& p)
  {
    cout << "Person& operator=(const Person& p)" << endl;
    if (this != &p)
    {
      _name = p._name;
    }
    return *this;
  }
  ~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(const Student& s)
    : Person(s)
    , _num(s._num)
  {
    cout << "Student(const Student& s)" << endl;
  }
  Student& operator = (const Student& s)
  {
    cout << "Student& operator= (const Student& s)" << endl;
    if (this != &s)
    {
      Person::operator =(s);
      _num = s._num;
    }
    return *this;
  }
  ~Student()
  {
    cout << "~Student()" << endl;
  }
protected:
  int _num; //学号
};
void Test()
{
  Student s1("jack", 18);
  Student s2(s1);
  Student s3("rose", 17);
  s1 = s3;
}
int main(void)
{
  Test();
  return 0;
}

这段代码:

实例化对象s1的时候会先去调用Student的构造函数,然后再Student的构造函数当中显示的调用了父类的构造函数。
实例化对象s2的时候先调用Student的拷贝构造,在拷贝构造的初始化列表当中显示的调用父类的拷贝构造
实例化s3的时候和s1一样
s1赋值给s3的时候先去调用子类的operator=,但是在子类当中会调用父类的operator=

继承与友元

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

Display是基类Person的友元,假如Student把Person当中的友元关系也给继承了,那么这里的s._stuNum是可以访问的,但是结果的不可访问,所以友元关系是不能被继承的

6.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

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 ; // 研究科目
};
void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl;
}

复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

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

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; // 主修课程
};
void Test()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  a._name = "peter";
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "xxx";
  a.Teacher::_name = "yyy";
}

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

class Person
{
public:
  string _name; // 姓名
};
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; // 主修课程
};
void Test()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  a._name = "peter";
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "xxx";
  a.Teacher::_name = "yyy";
}
int main(void)
{
  Test();
  return 0;
}

这样,指向的_name就都是同一个_name了。

7.虚继承解决数据冗余和二义性的原理

可以看到,虚继承的成员都放在公共区了,但是这里又多了两个地址,看起来像是两个指针

可以看到,这里有一个140c,14的16进制就是20,c就是12,刚好记录了当前类到公共区域的距离。

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