[C++随想录] 继承(上)

简介: [C++随想录] 继承(上)

继承的引言

概念

继承(inheritance)机制是面向对象程序设计使代码可以 复用的最重要的手段,它允许程序员在保

持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象

程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。

定义

class People
{
public:
  People(string name = "John", int age = 18)
  {
    _name = name;
    _age = age;
  }
  void print() 
  {
    cout << "姓名->" << _name << endl;
    cout << "年龄->" << _age << endl;
  }
protected:
  int _age ;
  string _name;
};
class Student : public People
{
public:
  Student(string name = "muyu", int age = 20, string id = "9210401227")
    :People(name, age)
  {
    _id = id;
  }
protected:
  string _id;
};
class Teacher : public People
{
public:
  Teacher()
  {
    People::_name = "mutong";
    People::_age = 22;
  }
protected:
  int _JobNumber;
};
int main()
{
  People p;
  p.print();
  cout << endl;
  Student s;
  s.print();
  cout << endl;
  Teacher t;
  t.print();
  return 0;
}

运行结果:

姓名->John
年龄->18
姓名->muyu
年龄->20
姓名->mutong
年龄->22

解释 class Student : public People

People类是 父类/ 基类, Student类是 子类/ 派生类

Student类继承People类的本质就是 复用 ⇒ Student 对象可以使用 People类里面的成员

成员包括 成员变量 和 成员函数.

成员变量是在对象空间内的, 而成员函数是不在对象空间内的, 属于整个类.

成员的 访问限定符 有三种 public, protected, private

继承方式不同 && 基类成员的访问限定符不同 ⇒ 决定了基类的成员在派生类中的存在情况


继承的方式

继承方式有三种: public, protected, private

成员的 限定符 有三种 public, protected, private

所以, 一共有 九种继承方式 👇👇👇

基类中的private成员, 在派生类中都是 不可见的

不可见 和 private成员是不一样的, private成员是 类里面可以访问, 类外面不可访问, 不可见是 类里面看不见/ 不可访问, 类外面不可访问

其余继承方式, 派生类中的情况是 继承方式 和 类成员访问限定符中 权限小的那一个

权限的大小: public > protected > private

父类如果是 class, 默认继承是 私有继承, 父类如果是 struct, 默认继承是 公有继承. 不过建议显示继承方式

常用的继承方式为 图中绿色的区域 ⇐ 继承的本质是 复用, 私有继承 和 基类中的私有成员在继承中是没有复用的意义的.

为什么 派生类没有 print函数 , 但能调用 print函数?

我们可以认为 子类对象里面包含两个部分: 父类对象成员变量 + 子类本身成员变量



子类对象中的 成员变量 = 自己本身的成员变量 + 父类的成员变量 (受访问限定符 和 继承方式共同限制)

子类对象中的 成员函数 = 自己本身的成员函数 + 父类的成员函数 (受访问限定符 和 继承方式共同限制)

print函数 是公有继承 && 访问限定符是公有 ⇒ 子类对象可以调用


为什么在 Teacher类中 可以People::_name = "mutong";

我们已经知道了 派生类对象的基本结构了.

那么派生类对象在 初始化阶段, 即调用默认构造 是先父类还是先子类呢?

通过调试, 我们发现: 子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数

Person类中有默认构造函数, 但是我们想改变一下 Teacher类对象中的 关于父类对象的那一部分, 那我们该怎么做呢?

首先, 我们不能直接写

_name = "mutong";
_age = 22;

因为受 域 的影响, 域是编译器在编译阶段查找变量的规则.

虽然, 我们可以认为子类对象中有 父类对象成员 + 子类对象成员, 但彼此是 独立的.

调用默认构造函数还是去 Person类中去调用

编译器在 编译阶段默认查找的顺序是 局部域 , 子类域, 父类域, 全局域

我们在子类中去给父类对象成员赋值 ⇒ 我们应该告诉编译器, 这个变量直接去父类中去查找就OK

即, 这个时候我们要用 Person(父类)::


为什么在 Student类中 可以 :People(name, age)

子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数.

那么如果 父类对象没有默认构造函数呢?

我们就需要 在子类的初始化列表处 显示调用父类的构造

基类和子类的赋值转换

int main()
{
  People p;
  Student st;
  st = p; // error
  p = st; // 可以进行转换
  return 0;
}

父类对象 不能 赋值给子类对象, 而子类对象 可以 赋值给父类对象

可以这样想: 子类对象的成员 > 父类对象的成员 ⇒ 可以 变小一点, 但不能变大一点

父类对象 = 子类对象, 属于不同类型之间的赋值 ⇒ 一般都会发生 类型转换 ⇒ 类型转换, 那就意味着要产生 临时常量拷贝. 但结果真的如我们想的这般吗?


验证 父类对象 = 子类对象 是否有临时常量拷贝

拷贝是 常量的 ⇒ 要进行区分, 我们可以使用 引用 &

如果生成了临时拷贝, 我们用普通引用 就会导致 权限的放大 , 就会报错

如果没有生成临时拷贝, 我们用普通引用, 就是 权限的平移, 就不会报错

int main()
{
  // 类型转换
  int i = 0;
  double d = i;
  // double& dd = i // error
  const double& dd = i;
  // 赋值兼容转换
  Student st;
  People ps = st;
  People& p = st;
  return 0;
}

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫 切片 或者切割 . 寓意把派生类中父类那部分切来赋值过去

🗨️那么这个切片是怎样完成的呢?


继承中的作用域

🗨️在继承过程中, 可能会出现 父类中的成员名 和 子类中的成员名相同的情况, 那么派生类对象调用该成员会是怎样的情况呢?

  • 先看下面的代码:
class People
{
public:
  People(string name = "John", int age = 18)
  {
    _name = name;
    _age = age;
  }
  void print() 
  {
    cout << "class People" << endl;
  }
protected:
  int _age ;
  string _name;
};
class Student : public People
{
public:
  Student(string name = "muyu", int age = 20, string id = "9210401227")
    :People(name, age)
  {
    _id = id;
  }
  void print()
  {
    cout << "class Student : public People" << endl;
  }
protected:
  string _id;
};
void test1()
{
  Student st;
  st.print();
}
int main()
{
  test1();
  return 0;
}

运行结果:

class Student : public People

父类和子类中都有 print函数, 通过结果显示 派生类内部的print函数

这是因为 , 跟上面的People::_name = "muyu";是一样的道理

那么, 如果我们非要通过派生类对象 调用基类中的print函数呢?👇👇👇

void test1()
{
  Student st;
  st.People::print();
}

总结:


子类和父类中的成员尽量不同名!

上面的例子, 子类和父类有同名的成员, 子类隐藏父类的成员, 这种关系叫做 隐藏/ 重定义

注意: 隐藏 != 重载

重载的前提条件是 同一作用域, 而隐藏是 父类和子类成员同名

隐藏 != 重写

隐藏是 子类中同名成员隐藏父类中同名成员, 而重写是 子类中重写父类有关函数的实现

派生类中的默认成员函数

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

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


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

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

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

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

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

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

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

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲

解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加

virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

class Person
{
public:
  Person(string name = "muyu", int age = 20)
    :_name(name)
    ,_age(age)
  {
    cout << "Person()" << endl;
  }
  Person(const Person& tem)
  {
    _name = tem._name;
    _age = tem._age;
    cout << "Person(const Person& tem)" << endl;
  }
  Person& operator=(const Person& tem)
  {
    _name = tem._name;
    _age = tem._age;
    return *this;
    cout << "Person& operator=(Person& tem)" << endl;
  }
  ~Person()
  {
    cout << "~Person()" << endl;
  }
protected:
  string _name;
  int _age;
};
class Student : public Person
{
public:
  Student(const string name,const int age, const int num)
    : Person(name,age)
    , _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 test2()
{
  Student st1("牧童", 20, 20230101);
  Student st2(st1);
  Student st3("沐雨", 18, 20230102);
  st3 = st1;
}
int main()
{
  // test1();
  test2();
  return 0;
}

运行结果:

Person()
Student()
Person(const Person& tem)
Student(const Student& s)
Person()
Student()
Student& operator= (const Student& s)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()

🗨️其他函数都是 先父类, 后子类, 唯独 析构函数 先子类后父类?


首先, 构造函数是 先父类, 后子类

栈, 先进后出 ⇒ 析构的时候, 先子类, 后父类.

其次, 父类可以调用子类的成员, 而子类不能调用父类的成员

如果先析构父类, 如果子类对象还想调用父类的成员,那就完蛋了!

🗨️在子类的析构函数中, 调用父类的析构函数


首先,

  ~Student()
  {
    ~Person(); // 提示有一个重载
    cout << "~Student()" << endl;
  }

纳闷? 这个还能有重载?

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲

解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor().

那么子类和父类中的 析构函数名 都是 destruction ⇒ 那么就构成了隐藏关系

那么我们在子类中调用父类的析构函数应该如下:

  ~Student()
  {
    Person::~Person();
    cout << "~Student()" << endl;
  }

结果如下:

编译器默认帮我们 先调用了父类的析构函数不信任我们用户, 由编译器自己完成

相关文章
|
2月前
|
安全 Java 编译器
C++进阶(1)——继承
本文系统讲解C++继承机制,涵盖继承定义、访问限定符、派生类默认成员函数、菱形虚拟继承原理及组合与继承对比,深入剖析其在代码复用与面向对象设计中的应用。
|
6月前
|
存储 安全 Java
c++--继承
c++作为面向对象的语言三大特点其中之一就是继承,那么继承到底有何奥妙呢?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用,继承就是类方法的复用。
155 0
|
9月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
496 6
|
11月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
170 16
|
11月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
367 5
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
245 1
【C++】继承
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
204 11
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
165 1
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
156 1
C++(二十)继承
本文介绍了C++中的继承特性,包括公有、保护和私有继承,并解释了虚继承的作用。通过示例展示了派生类如何从基类继承属性和方法,并保持自身的独特性。此外,还详细说明了派生类构造函数的语法格式及构造顺序,提供了具体的代码示例帮助理解。