C++:继承

简介: C++:继承

面向对象的四大特点:抽象、封装、继承、多态。其中抽象、封装分别对应类、对象。

1、概念

继承指的是在既有类的基础上产生新的类。

1.1、派生类的生成

派生类的生成过程

  • 吸收基类的成员
  • 改造基类的成员
  • 添加自己新的成员

1.2、继承的局限性

不能继承的基类特征

  • 构造函数
  • 析构函数
  • 用户重载的operator new|delete运算符
  • 用户重载的operator=赋值运算符
  • 友元关系

1.3、继承方式

三种继承方式

  • public:公有成员,在本类、派生类和外部都可访问。
  • protected:保护成员,只能在本类和派生类中访问,是一种区分血缘关系内外有别的成员。
  • private:私有成员,默认方式,只能被本类的成员函数访问,派生类和类外都不能访问。

总结:派生类的访问权限,不管以什么继承方式

  • 基类中的私有成员都不能在派生类中访问
  • 基类中的非私有成员都可以在派生类中访问。
  • 派生类对象只能访问基类中的共有成员,其他都不能访问。

2、派生类单继承

2.1、对象创建

派生类构造函数的特点

  • 形式:派生类构造函数的初始化列表中显示调用基类的构造函数
  • 特点:先初始化基类部分,再初始化派生类部分。
  • 原因:构造函数和析构函数不能继承,为了初始化数据成员,派生类必须定义构造函数和析构函数。由于派生类包含基类数据成员,因此,创建派生类对象必须先通过派生类的构造函数来调用基类的构造函数,完成基类成员初始化,然后对派生类成员进行初始化。

例如:

class Derived: public Base {
 public:
     Derived(long base, long derived)
     : Base(base)        // 显示调用基类的构造函数
     , _derived(derived)
     {}
     long _derived;
 };

派生类构造函数的调用顺序

  • 首先初始化基类成员
  • 其次初始化派生类特殊成员:对象成员、引用成员、const 成员、static 成员
  • 最后执行派生类的构造函数体

2.2、对象销毁

与对象创建顺序相反,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数

  • 先调用派生类的析构函数
  • 再调用派生类中成员对象的析构函数
  • 最后调用普通基类的析构函数

3、派生类多继承

3.1、对象创建

单即继承和多基继承的派生类构造函数功能没有本质不同,首先要执行所有基类的构造函数,再执行派生类构造函数初始化列表中的其他内容,最后是构造函数体。

注意:各基类构造函数的执行顺序与其在初始化表中的顺序无关,而是由定义派生类时指定的基类顺序决定的。

3.2、对象销毁

析构函数的执行顺序同样是与构造函数的执行顺序相反。但在使用多基继承过程中,会产生两种二义性。

3.3、多继承的问题

3.3.1、成员名冲突二义性

  • 概念:多个基类存在同名成员,编译器无法判断要访问哪个基类的成员。
  • 解决:通过类名来访问一个类的成员,例:A::print()

3.3.2、存储二义性(菱形继承)

菱形继承

  • 概念:多基派生,多条继承路径上有一个共同的基类,D 对象有共同基类 A 的双重拷贝。
  • 解决:虚拟继承

菱形继承


class B: virtual public A       // 1个虚基指针 + 数据
 class C: virtual public A       // 1个虚基指针 + 数据
 class D: public B, public C     // 2个虚基指针 + 数据

4、基类与派生类的转化

4.1、派生类转化为基类

派生类适应于基类,派生类对象能直接用于基类对象的场景,具体形式为:

  • 派生类的对象赋值给基类的对象
  • 基类的引用绑定到派生类的对象
  • 基类的指针指向派生类的对象(向上转型)
// 1、派生类的对象赋值给基类的对象
 base = derived;
 base.operator=(derived);
 // 2、基类的引用绑定到派生类的对象
 const Base &ref = derived; 
 // 3、基类的指针指向派生类的对象
 Base *pbase = &derived;

4.2、基类转换为派生类

向上转型与向下转型

  • 向上转型:安全,派生类指针转化为基类指针,Base *pbase = &derived;
  • 向下转型:不安全,基类指针转化为派生类指针。此时,新的指针多控制了内存,可能存在内存越界问题。

例:

// 不安全的向下转型:直接将 base 指针转换为 derived 指针
 // base 8B, derived 16B,新的指针多控制了 8B,可能存在内存越界的问题
 Derived *pderived = static_cast<Derived *>(&base);
 // 安全的向下转型
 Base *pbase = &derived;  // base 指针指向派生类对象,先向上转型
 Derived *pderived = static_cast<Derived *>(pbase); // 再向下转型

5、派生类对象间的复制控制

原则:先构造基类部分,后构造派生类部分

派生类对象的复制控制

  • 派生类没有显示定义复制控制函数,则会自动调用基类相应的复制控制函数,不管基类是否有显示定义。
  • 派生类有显示定义复制控制函数,那么基类部分不会再自动调用相应的复制控制函数,若要调用基类的复制控制函数需要手动调用。

测试代码:

#include <string.h>
 #include <iostream>
 using std::cout;
 using std::endl;
 class Base
 {
 public:
     Base()
     : _pbase(nullptr)
     {
         cout << "Base()" << endl;
     }
     Base(const char *pbase)
     : _pbase(new char[strlen(pbase) + 1]())
     {
         cout << "Base(const char *)" << endl;
         strcpy(_pbase, pbase);
     }
     Base(const Base &rhs)
     : _pbase(new char[strlen(rhs._pbase) + 1]())
     {
         cout << "Base(const Base &)" << endl;
         strcpy(_pbase, rhs._pbase);;
     }
     Base &operator=(const Base &rhs) {
         cout << "Base &operator=(const Base &)" << endl;
         if(this != &rhs) {
             delete [] _pbase;
             _pbase = nullptr;
             _pbase = new char[strlen(rhs._pbase) + 1]();
             strcpy(_pbase, rhs._pbase);
         }
         return *this;
     }
     ~Base() {
         cout << "~Base()" << endl;
         if(_pbase)
         {
             delete [] _pbase;
             _pbase = nullptr;
         }
     }
     friend std::ostream &operator<<(std::ostream &os, const Base &rhs);
 private:
     char *_pbase;
 };
 std::ostream &operator<<(std::ostream &os, const Base &rhs) {
     if(rhs._pbase) {
         os << rhs._pbase;
     }
     return os;
 }
 class Derived
 : public Base
 {
 public:
     Derived()
     : Base()
     , _pderived(nullptr)
     {
         cout << "Derived()" << endl;
     }
     Derived(const char *pbase, const char *pderived)
     : Base(pbase)
     , _pderived(new char[strlen(pderived) + 1]())
     {
         cout << "Derived(const char *)" << endl;
         strcpy(_pderived, pderived);
     }
     Derived(const Derived &rhs)
     : Base(rhs) // 显示调用基类的构造函数
     , _pderived(new char[strlen(rhs._pderived) + 1]())
     {
         cout << "Derived(const Derived &)" << endl;
         strcpy(_pderived, rhs._pderived);
     }
     Derived &operator=(const Derived &rhs) {
         cout << "Derived &operator=(const Derived &)" << endl;
         if(this != &rhs) {
             Base::operator=(rhs); // 显示调用基类的赋值运算符函数
             delete [] _pderived;
             _pderived = nullptr;
             _pderived = new char[strlen(rhs._pderived) + 1]();
             strcpy(_pderived, rhs._pderived);
         }
         return *this;
     }
     ~Derived() {
         cout << "~Derived()" << endl;
         if(_pderived) {
             delete [] _pderived;
             _pderived = nullptr;
         }
     }
     friend std::ostream &operator<<(std::ostream &os, const Derived &rhs);
 private:
     char *_pderived;
 };
 std::ostream &operator<<(std::ostream &os, const Derived &rhs) {
     // const引用指向const对象,基类的引用绑定到派生类的对象
     const Base &ref = rhs; 
     // <<重载(基类、派生类)
     os << ref << ", " << rhs._pderived;
     return os;
 }
 int main() {
     Derived d1("hello", "world");
     cout << "d1 = " << d1 << endl;
     cout << endl;
     Derived d2 = d1;
     cout << "d1 = " << d1 << endl;
     cout << "d2 = " << d2 << endl;
     cout << endl;
     Derived d3("hubei", "wuhan");
     cout << "d3 = " << d3 << endl;
     d3 = d1;
     cout << "d1 = " << d1 << endl;
     cout << "d3 = " << d3 << endl;
     return 0;
 }
相关文章
|
18天前
|
Java C++
C++的学习之路:21、继承(2)
C++的学习之路:21、继承(2)
18 0
|
2月前
|
C++
8. C++继承
8. C++继承
27 0
|
2月前
|
安全 程序员 编译器
C++之继承
C++之继承
|
18天前
|
安全 编译器 程序员
c++的学习之路:20、继承(1)
c++的学习之路:20、继承(1)
29 0
|
2月前
|
安全 Java 编译器
C++:继承
C++:继承
32 0
|
2月前
|
安全 Java 程序员
【C++练级之路】【Lv.12】继承(你真的了解菱形虚拟继承吗?)
【C++练级之路】【Lv.12】继承(你真的了解菱形虚拟继承吗?)
|
2月前
|
安全 Java 编译器
C++:继承与派生
C++:继承与派生
|
1天前
|
存储 编译器 C++
C++中的继承
C++中的继承
8 0
|
2天前
|
安全 前端开发 Java
【C++】从零开始认识继承二)
在我们日常的编程中,继承的应用场景有很多。它可以帮助我们节省大量的时间和精力,避免重复造轮子的尴尬。同时,它也让我们的代码更加模块化,易于维护和扩展。可以说,继承技术是C++的灵魂。
8 1
|
2天前
|
安全 程序员 编译器
【C++】从零开始认识继承(一)
在我们日常的编程中,继承的应用场景有很多。它可以帮助我们节省大量的时间和精力,避免重复造轮子的尴尬。同时,它也让我们的代码更加模块化,易于维护和扩展。可以说,继承技术是C++的灵魂。
17 3
【C++】从零开始认识继承(一)