【C++高阶】C++继承学习手册:全面解析继承的各个方面

简介: 【C++高阶】C++继承学习手册:全面解析继承的各个方面

📖1. 继承的概念及定义

⛰️继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

继承代码示例

class A 
{
public:
  void func()
  {
    cout << "A::func()" << endl;
  }
protected:
  int _a = 10;
};

// 继承后父类A的成员_a(成员函数+成员变量)都会变成子类的一部分
class B : public A
{
public:
  // ......
protected:
  int _b = 100;
};

int main()
{
  A a ;
  B b;
  a.func();
  b.func(); // b可以调用A中的成员函数
  return 0;
}

🌄继承定义

我们从刚刚的代码示例可以看到A是基类(父类),B是派生类(子类)

定义格式

注意:在定义继承的时候继承方式可以省略不写,如果不写则是根据基类的定义来决定默认继承方式,但是建议定义时带上继承方式

class定义的类默认private继承,struct定义的类默认public继承



继承关系和访问限定符


继承基类成员访问方式的变化

继承方式和访问限定符都有三种,虽然它们组合一共有9中能使用的方法,但是我们最常用的只有红色框里面的两种用法

这里我们有以下几点需要注意:

  • 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强



📙2. 基类和派生类对象赋值转换

关于赋值规则这里我们先提两点:

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


我们在讲C++入门知识的时候讲过,引用类型不同的变量时,会产生一个临时变量,临时变量具有常性,需要const修饰,但是在继承中就不需要const修饰

代码示例

int main()
{
  int c = 1;
  double d = 1.1;
  const int& r = d; // 中间产生了一个临时变量,临时变量具有常性,需要const修饰
  
  B b;
  A a = b; // 子类可以赋值给基类
  // b = a; // false, 基类不可以赋值给子类
  
  A& ra = b; // is-a 的关系中间不会产生临时对象,父子类的赋值兼容规则(切割/切片)
  return 0;
}

继承中的对象是is-a 的关系,它们中间并不会产生临时对象,这就是父子类的赋值兼容规则(切割/切片)


📕3. 继承中的作用域

关于作用域的注意事项:

  • 在继承体系中基类和派生类都有独立的作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员

🎩成员变量隐藏

当继承的基类与子类有同名的成员变量时,不指定的话,会调用子类的成员变量

代码示例

class A 
{
protected:
  int _a = 10;
};

class B : public A
{
public:
  void Print()
  {
    cout << "_a:" << _a << endl;
    // cout << "A: _a:" << A::_a << endl; // 要想成功打印A类的元素必须要指定
    cout << "_b:" << _b << endl;
  }
protected:
  int _a = 99;
  int _b = 100;
};

int main()
{
  B b;
  // 成员变量同名
  // A 和 B中的 _a 构成隐藏
  b.Print(); // // _a = 99 , _b = 100; 就近原则
  return 0;
}



🎈成员函数隐藏

在继承中,同名函数并不会构成函数重载,因为他们在不同的作用域,每个类都是独立的,成员函数满足函数名相同就构成隐藏

代码示例

class A 
{
public:
  void func()
  {
    cout << "func()" << endl;
  }
protected:
  int _a = 10;
};

class B : public A
{
public:
  // 
  void func(int b)
  {
    cout << "func(int b)" << endl;
  }
protected:
  int _b = 100;
};

int main()
{
  B b;
  // 成员函数同名
  // A 和 B中的 func() 构成隐藏
  b.func(); // 打印“func(int b)”
}

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

🧩默认成员函数

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


🧩派生类默认函数特征

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

综上所述:关于基类和子类的调用顺序,一般情况都是先父后子,但是析构必须先子后父,来避免析构完父类之后,子类出错

继承默认函数的实现代码示例

class A 
{
public:
  A()
  {}

  A(int a)
    :_a(a)
  {
    cout << "A()" << endl;
  }

  A(const A& a)
    :_a(a._a)
  {
    cout << "A(const A& a)" << endl;
  }

  A& operator=(const A& a)
  {
    cout << "A& operator=(const A& a)" << endl;
    if (&a != this)
    {
      _a = a._a;
    }
    return *this;
  }

  ~A()
  {
    cout << "~A()" << endl;
  }
protected:
  int _a = 10;
};

class B : public A
{
public:
  B()
  {}

  B(int a, int b)
    // :_a(a) // _a不是基类成员不能这样初始化
    :A(a)
    ,_b(b)
  {
    cout << "B()" << endl;
  }

  B(const B& b)
    // :_a(a) // _a不是基类成员不能这样初始化
    :A(b)
    , _b(b._b)
  {
    cout << "B(const A& a, const B& b)" << endl;
  }

  B& operator=(const B& b)
  {
    cout << "B& operator=(const B& b)" << endl;
    if (&b != this)
    {
      // 需要调用A类的 operator=
      A::operator=(b);
      _b = b._b;
    }
    return *this;
  }

  // 析构函数会先析构父类,而有时候先析构父类,子类会出事
  // 不需要显式调用父类析构
  ~B()
  {
    cout << "~B()" << endl;
  }
protected:
  int _b = 100;
};
int main()
{
  B b1(1, 100);
  B b2(b1);

  B b3(2, 200);
  b1 = b3;
  return 0;
}

📒5. 友元与静态成员变量

🍂友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,因为朋友的朋友不一定也是自己的朋友,如果基类,子类都想使用必须都在各自的域里面声明

代码示例

class A 
{
public:
  friend void Print(const A& a, const B& b);
protected:
  int _a = 10;
};

class B : public A
{
public:
  // 
protected:
  int _b = 100;
};

void Print(const A& a, const B& b)
{
  cout << a._a << endl;
  cout << b._b << endl;
}

int main()
{
  A a;
  B b;
  Print(a, b);
}

🍁静态成员

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

代码示例

class A 
{
public:
  A()
  {
    ++_count;
  }
  static int _count;
protected:
  int _a = 10;
};
int A::_count = 0;
class B : public A
{
public:
  // 
protected:
  int _b = 100;
};

int main()
{
  A a;
  B b;
  cout << A::_count << endl;
}

📜6. 多继承

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

class B : public A

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

class D : public B , public C

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

class B : public A
{......};
class C : public A
{......};
class D : public B , public C

🌞菱形继承

class A
{
protected:
  int _a = 1;
};
class B :public A
{
protected:
  int _b = 2;
};
class C :public A
{
protected:
  int _c = 3;
};
class D :public B, public A
{
protected:
  int _d = 4;
};

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中_a成员会有两份,我们在访问的时候无法明确知道访问的是哪一个,必须要显示指定访问哪个父类的成员,但是数据冗余任然无法解决!


🌙虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在A和B的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用

代码示例

class A
{
protected:
  int _a = 1;
};
class B : virtual public A
{
protected:
  int _b = 2;
};
class C : virtual public A
{
protected:
  int _c = 3;
};
class D : public B, public A
{
protected:
  int _d = 4;
};

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

  • 虚拟继承通过将共同的祖先类(即虚基类)的拷贝在派生类对象中只保留一份,来解决这个问题。具体来说,虚拟继承会在内存中创建一个虚基表,并在派生类对象中存储一个指向这个虚基表的指针(即虚基表指针)。虚基表中存的偏移量。通过偏移量可以找到下面的A,而无需在派生类对象中多次存储这些数据成员。因此,虚拟继承通过减少重复存储的数据成员来消除数据冗余
  • 虚拟继承通过改变派生类访问虚基类成员的方式来解决这个问题。在虚拟继承中,派生类对象通过虚基表指针来访问虚基类(即共同祖先类)的成员。由于虚基表中存储了虚基类成员的地址,因此派生类对象可以明确地知道应该访问哪个虚基类成员,从而消除了二义性。

加上表中偏移量可以找到最底下的A


🔥7. 总结

回顾学习过程,我们学会了如何定义基类与派生类,掌握了访问控制规则,理解了构造函数与析构函数在继承中的作用,还探讨了多重继承及其带来的挑战。这些知识不仅丰富了我们的编程技能,更为我们解决实际问题提供了有力的工具

在结束对C++继承的学习之旅后,我们不禁感叹其强大的功能和灵活性。通过深入探究继承的基本概念、语法规则以及高级应用,我们逐渐揭开了其背后的奥秘,并体验到了它在面向对象编程中的独特价值

学习C++继承并非一蹴而就的过程。它需要我们不断地实践、思考、总结和创新。在未来的编程之路上,我们将继续深化对继承的理解,探索其更多的应用场景和高级特性,如虚继承、接口继承等,我们也要认识到继承并非万能的。在使用继承时,我们需要权衡其带来的好处和潜在的风险,避免过度使用导致代码结构复杂、难以维护。我们应该根据具体的需求和场景,选择最合适的编程范式和工具!!!希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

目录
相关文章
|
4月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
15天前
|
存储 安全 Java
c++--继承
c++作为面向对象的语言三大特点其中之一就是继承,那么继承到底有何奥妙呢?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用,继承就是类方法的复用。
|
3月前
|
域名解析 存储 缓存
深入学习 DNS 域名解析
在平时工作中相信大家都离不开 DNS 解析,因为 DNS 解析是互联网访问的第一步,无论是使用笔记本浏览器访问网络还是打开手机APP的时候,访问网络资源的第一步必然要经过DNS解析流程。
|
2月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
66 4
|
3月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
208 6
|
5月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
102 16
|
4月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
4月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
6月前
|
算法 网络安全 区块链
2023/11/10学习记录-C/C++对称分组加密DES
本文介绍了对称分组加密的常见算法(如DES、3DES、AES和国密SM4)及其应用场景,包括文件和视频加密、比特币私钥加密、消息和配置项加密及SSL通信加密。文章还详细展示了如何使用异或实现一个简易的对称加密算法,并通过示例代码演示了DES算法在ECB和CBC模式下的加密和解密过程,以及如何封装DES实现CBC和ECB的PKCS7Padding分块填充。
142 4
2023/11/10学习记录-C/C++对称分组加密DES
|
5月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
137 5

推荐镜像

更多
  • DNS