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,刚好记录了当前类到公共区域的距离。

目录
打赏
0
0
0
0
3
分享
相关文章
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
|
2月前
|
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
61 16
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
58 5
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
46 1
【C++】继承
|
5月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
79 1
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
37 0
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
AI助理

你好,我是AI助理

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