吃透C++继承:不止是代码复用,更是面向对象设计的底层思维

(ฅ´ω`ฅ)博主的博客主页------>Cinema KI
( 。ớ ₃ờ)ھ博主的gitee主页------>IIirving


👀 前言

继承绝对不简单,它涉及的知识点很多很杂,请大家要下足功夫去攻克继承大关。


提示:以下是本篇文章正文内容,下面案例可供参考

👍一、继承的概念及定义

1.1继承的概念

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

下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量和函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有变量是职称,学生的独有变量是学号;学生的独有函数是学习,老师的独有成员函数是授课。

代码示例,如下:

cpp 复制代码
class Student
{
public:
      //学习
      void study
      {
          //...
      }
protected:
      string _name;//姓名
      string _address;//地址
      string _tel;//电话
      int _age;//年龄
      int _stuid;//学号
};

class Teacher
{
public:
      void teaching()
      {
          //...
      }
protected:
      string _name;
      string address;
      string _tel;
      int _age
      string _title;
};

大家可以看到,Student和Teacher这两个类,除了有自己特有的部分(Sudent特有的部分为成员函数study和成员变量_stuid,Teacher特有的部分为成员函数teaching和成员变量_title)。其余部分,像什么_name,地址,都是两者共有的,两份都写,就会显得很冗余 。所以可以把共有的部分提取单独携程一个类,然后运用继承 让这个类与Student和Teacher建立联系。

这个公共的类我设为Person

cpp 复制代码
class Person
{
public:
       void identity()
       {
          //身份认证函数
       }
protected:
       string _name;//姓名
       string _address;//地址
       string _tel;//电话
       int _age;//年龄
};
////
class Student:public Person
{
public:
     void study()
     {
          //...
     }
protected:
     int _stuid;//学号
};
////
class Teacher:public Person
{
public:
      void teaching()
      {
          //...
      }
protected:
     string title;//职称
};
int main()
{
     Student s;
     Teacher t;
     s.identity();
     t.identity();
}

1.2继承格式

继承方式分为:public、protected、private继承

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

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类的private成员 派生类的private成员 派生类的private成员

①基类的private成员在派生类无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法中限制派生类对象不管在类里面还是类外面都不能去访问它。(提示:这里可以调用基类中函数体有涉及到基类私有成员变量的函数进行访问)

②基类private成员在派生类中是不能被访问的,如果基类成员不想再类外直接被访问,但需要在派生类中能被访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
代码示例,如下:

cpp 复制代码
class Base
{
private:
     int pri_num;//私有成员:类外、派生类都不能访问
protected:
     int pro_num;//保护成员:类外不能访问,派生类可以访问
public:
     Base()
        :pri_num(10)
        ,pro_num(20)
     {}
};
///
class Derived : public Base
{
public:
     void show()
     {
         //pri_num = 100;//错误,派生类不能访问基类的private变量
         pro_num  = 200;//正确,基类的protected成员派生类可以访问
     }
};
///
int main()
{
    Derived d;
    d.show();
    //d.pro_num = 300;//错误,protected成员在类外无法直接访问
    Base b;
    //b.pro_num = 400;//错误,基类的protected成员,类外也不能直接访问。 
}

③实际上面的表格我们进行总结一下会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public>protected>private。

使用关键字class时默认的继承方式时private,使用struct时默认的继承方式的public。

⑤在实际运用这种一般使用都是public继承,几乎很少用protected/private继承,也不提倡使用protected/private继承,因为它们两者继承下来的成员都只能在派生类的类类里面使用,实际中扩展维护性不强。


1.3继承类模板

cpp 复制代码
namespace Zehong
{
    template<class T>
    class Stack : public std::vector<T> 
    {
    public:
          void push(const T& x)
          {
             vector<T>::push_back(x);
          }
          void pop()
          {
             vector<T>::pop_back();
          }
          T& top()
          {
             return vector<T>::back();
          }
          bool empty()
          {
             return vector<T>::empty();
          }
    };
}
int main()
{
    Zehong::Stack<int> s;
    s.push(1);
    s.pop();
}

🤞 二、基类和派生类之间的转换

·public继承的派生类对象 可以赋值给基类的指针/基类的引用 。这种做法被叫做切片/切割 。相当于把从派生类中基类那部分切下来

·基类的对象不能赋值给派生类对象

·基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

cpp 复制代码
class A
{
public:
     void show()
     {
        cout<<_age<<" "<<_sex<<" "<<_name<<" ";
     }
     int _age = 18;
     string _name = "陈泽鸿";
     string _sex = "男";
};
class B: public A
{
public:
     int _stuid = 424240134;
};
int main()
{
    B b;//先定义一个B类型的变量
    A& a = b;//把派生类的对象给给基类的引用,引用的是对象b中的基类的那一部分
    //直接访问:a._age,a._name,a._sex
    A* aa = &b//把派生类对象的地址给给基类的指针,这个指针指向的是对象b中基类的那一部分
    //直接使用:b->show(),b->_age,b->_name,b->_sex
    A aaa = b;//派生类对象可以赋值给基类的对象是通过调用基类的拷贝构造实现的。    
}

✊三、继承中的作用域

①在继承体系中,基类和派生类都有独立的作用域(这就说明了,在基类和派生类中可以定义相同名字的变量)。

②派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派生类成员函数中,可以使用基类::基类成员 显示访问)

③需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(简而言之:函数名相同,就构成隐藏)。

代码示例,如下:

cpp 复制代码
class A
{
public:
     string _name = "陈泽鸿";
     int _num = 111;
};
class B :public A
{
public:
      void show()
      {
          cout<<_num<<endl;//这里打印的_num默认是派生类的_num
          cout<<A::_num<<endl;//想打印基类的_num,需指定作用域
          cout<<_name<<endl;//_name派生类没有定义,自动打印基类的_name
      }
      int _num = 999;//定义了一个跟基类同名的变量
};
int main()
{
    B b;
    b.show();
    return 0;
}

运行结果:
999
111
陈泽鸿

3.1考察继承作用域相关选择题

代码示例,如下:

cpp 复制代码
class A
{
public:
      void func()
      {
          cout<<"func()"<<endl;
      }
};
//
class B : public A
{
public:
      void func(int i)
      {
          cout<<"func(int i)"<<endl;
      }
};
///
int main()
{
    B b;
    b.func(10);
    b.func();
}

题目1

A类和B类中的两个func函数构成什么关系()
A.重载 B.隐藏 C.没关系

题目2

程序运行的结果是什么?()
A.编译报错 B.运行报错 C.正常运行

第一题:

首先两个函数一个在基类一个在派生类,属于不同的作用域,且同名,所以构成了隐藏。重载的话,两个函数得在同个作用域。所以这题选B
第二题:

首先在main函数中定义了一个派生类对象,然后调用func函数,但是派生类和基类的func同名,已经构成了隐藏。所以main函数中利用b变量调用的两个func函数均是派生类中的func,所以第一次调用没有出错,第二次调用因为没有传对应的i 值,所以导致了报错。所以这题选A

重载与隐藏的区别

维度 重载(Overloading) 隐藏(Hiding)
作用域 相同作用域(同类、同命名空间) 不同作用域(基类与派生类)
函数签名 必须参数列表不同(个数/类型/顺序) 同名即构成隐藏
匹配规则 编译器会自动找到最匹配的 派生类作用域优先,基类同名函数被屏蔽,需显式指定
与继承的关系 无继承也可发生(如同一类内) 必须发生在继承体系中(派生类隐藏基类)
返回值影响 不影响 不影响

编译报错和运行报错的区别

·编译报错 :代码编写不符合语法规则或类型检查失败,编译器在编译阶段发现并报错,程序无法生成可执行文件,属于静态错误(如语法错误、未定义变量、类型不匹配)。

·运行报错:代码语法合法且编译通过,但运行时出现逻辑错误、资源问题或异常(如数组越界、空指针访问、除零错误),程序可能崩溃或异常终止,属于动态错误。


🤘四、派生类的默认成员函数

4.1 四个常见默认成员函数

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

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

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

③派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示基类的operator=需指定作用域。

④派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

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

cpp 复制代码
class Person
{
public:
      //有默认构造
      /*Person(string name = "Zehong")
             :_name(name)
      {}
      string _name;*/
      Person(string name)
             :_name(name)
      {
           cout<<"Person()~"<<endl;
      }
};
//
class Student : public Person
{
public:
      /*Student(int num)
           :_num(num)
      {}*/
      //上方基类无默认构造,需显示调用基类构造
      Student(String name,int num)
             :Person(name)//直接调用基类的构造函数
             ,_num(num)
      {
          cout<<"Student()~"<<endl;
      }
      int _num;
};
///
int main()
{
    //Student s(5);
    //这里调用派生类构造会自动先调用基类的构造,因为基类的默认构造,所以我们可以不用在派生类构造显示调用基类构造,如果基类没有构造,那么就要在派生类构造中显示调用基类构造。例如下面的例子
    	Student s("陈泽鸿",5);
}
cpp 复制代码
运行结果:
Person()~
Student()~

进一步说明了先调用基类再调用派生类构造
拷贝构造

cpp 复制代码
class Person
{
public:
     Person(const Person& p)//这里的p引用的是s(派生类)中基类的那一部分(_name)
            :_name(p._name)
     {
         cout<<"Person(const Person& s)~"<<endl;
     }       
     string _name;
};
//
class Student
{
public:
     Student(const Student& s)
            :Person(s)//拷贝构造基类的
            ,_num(s._num)//拷贝构造自己的
     {
         cout<<"Student(const Student& s)~"<<endl;
     }
     int _num;
};
//
int main()
{
    Student s1("jack",18);//初始化
    Student s2(s1);
}
cpp 复制代码
运行结果:
Person(const Person& s)~
Student(const Student& s)~

operator=

cpp 复制代码
class Person
{
public:
      Person& operator=(const Person& s)~
      {
          cout<<"Person& (const Person& s)~"<<endl;
          if(this != &p)
                   _name = p._name;
          return *this;
      }
};
//
class Student
{
public:
      Student& operator=(const Student& s)
      {
          cout<<"Student& (const Student& s)"<<endl;
          if(this != &s)
          {
              Person::operator=(s);//这里基类的派生类都有operator=函数,构成了隐藏,所以要指定作用域
              _num = s._num;
          }
      }
      int _num;
};
//
int main()
{
    Student s1("jack",18);
    Student s2("Peter",11);
    s1 = s2;
}
cpp 复制代码
运行结果:
Student& (const Student& s)~
Person& (const Person& s)~

★析构函数

cpp 复制代码
class Person
{
public:
      ~Person()
      {
          cout<<"~Person()~"<<endl;
      }
      string _name;
};
//
class Student
{
public:
      ~Student()//名字会被处理成destructor,所以析构函数会隐藏
      {
          Person::~Person();
          cout<<"~Student()~"<<endl;
      }
      int _num;
};
int main()
{
    Student s;
    return 0;
}

提示:C++中,析构函数看似名字不同,实际上析构函数的函数名都被处理成了destructor 以便于构成析构函数的隐藏。所以在类Student中要指定作用域才能调用到Person的析构函数。

cpp 复制代码
运行结果:
~Person()~
~Student()~
~Person()~

------大家可以看到,调用了两次父类的析构,这是为什么呢?因为在类Student析构函数中我们先显示定义了一次 ,在Student析构函数结束后。编译器会自动调用父类的析构,所以加起来一共调用了两次。那为什么要这么做呢?

------那是因为析构要遵循先子后父原则,先析构完子类,再自动析构父类。

------因为如果你先析构的父类,父类的成员会置为随机值,但是此时此刻子类的对象是可以访问到这些已被置为随机值的成员的,会造成错误。所以要遵循先子后父。具体的表现就是,调用完子类的析构函数后会立马调用父类的析构函数。

4.2实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法2:C++11新增了一个final 关键字,final修改基类,派生类就不能继承了。
代码示例,如下:

cpp 复制代码
//C++11的方法
class Base final
{
public:
     void func5(){cout<<"Base::func5"}<<endl;}
protected:
     int a = 1;
//C++98的方法
     /*Base()
     {}*/
};
//
class Derive : public Base
{
public:
     void func4(){cout<<"Derive::func4"<<endl;}
protected:
     int b = 2;
}

🦴5、继承与友元

|----------|
| 友元关系不能继承 |

也就是说基类友元不能访问派生类私有和保护成员。 `代码示例,如下:`

cpp 复制代码
class Student;//需声明,不然Display函数找不到类Student(向上寻找原则)

class Person
{
public:
       friend void Display(const Person& p,const Student& s);
protected:
       string _name;
};
//
class Student : public Person
{
protected:
       int _num;
};
void Display(const Person& p,const Student& s)
{
    cout<<p._name<<endl;
    cout<<s._num<<endl;
}
int main()
{
   Person p;
   Student s;
   //Display(p,s);编译报错,无法访问Student的_num。
}

👈6.继承与静态成员

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

cpp 复制代码
class Person
{
public:
     string _name;
     ststaic int _count;//类里声明,类外定义
};
int Person::_count = 0;
//
class Student :public Person
{
protected:
    int _num;
};
//
int main()
{
    Person p;
    Student s;
    cout<<&p._name<<end;
    cout<<&s._name<<endl;
    //
    cout<<&p._count<<endl;
    cout<<&s._count<<endl;
}
cpp 复制代码
运行结果:
0000007D035CF4C8
0000007D035CF508
00007FF7809044C4
00007FF7809044C4

运行结果表示:

基类和派生类的_name地址是不一样的,说明父类和子类各有一份_name。

基类和派生类的_count地址是一样的,说明父类和子类共用一份_count。


👉7.多继承及其菱形继承问题

7.1继承模型

单继承:一个派生类只有一个直接基类 时称这个继承关系为单继承

多继承:一个派生类有两个或以上直接基类 时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员再放到最后面。

菱形继承:菱形继承是多继承的一种特殊情况。有数据冗余二义性 。不建议使用菱形继承。

代码示例,如下:

cpp 复制代码
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 _major;//主修课程
};
///
int main()
{
    Assistant a;
    //a._name = "Peter";//不知道你要访问谁的_name,是Student的还是Teacher的,不明确。
    //需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
}

二义性 :不知道你要访问哪一个_name。
数据冗余:Assistant继承了两个Person副本,原目的只需要继承一份即可。

7.2虚继承

|--------------------|
| 使用虚继承,可以解决数据冗余和二义性 |

`代码示例,如下`

cpp 复制代码
class Person
{
public:
      string _name;
};
//
class Student : virtual public Person//关键字为virtual
{
public:
      int _num;//学号
};
//
class Teacher : virtual public Person
{
public:
      int _id;//职工编号
};
//
class Assistant :public Student,public Teacher//教授助理
{
protected:
      string _major;//主修课程
};
///
int main()
{
    Assistant a;
    a._name = "Peter";
}

✍️总结

本文我带大家了解了继承的内容,前四章较为重要,我作了重点讲解。后面是拓展知识,希望大家能芝麻开花节节高

相关推荐
Dream it possible!3 小时前
LeetCode 面试经典 150_二叉搜索树_二叉搜索树中第 K 小的元素(86_230_C++_中等)
c++·leetcode·面试
Bona Sun5 小时前
单片机手搓掌上游戏机(十四)—pico运行fc模拟器之电路连接
c语言·c++·单片机·游戏机
oioihoii5 小时前
性能提升11.4%!C++ Vector的reserve()方法让我大吃一惊
开发语言·c++
小狗爱吃黄桃罐头6 小时前
《C++ Primer Plus》模板类 Template 课本实验
c++
码力码力我爱你8 小时前
Harmony OS C++实战
开发语言·c++
Vect__8 小时前
别再只懂 C++98!C++11 这7个核心特性,直接拉开你与普通开发者的差距
c++
想唱rap8 小时前
C++ map和set
linux·运维·服务器·开发语言·c++·算法
小欣加油9 小时前
leetcode 1018 可被5整除的二进制前缀
数据结构·c++·算法·leetcode·职场和发展
玖剹10 小时前
递归练习题(四)
c语言·数据结构·c++·算法·leetcode·深度优先·深度优先遍历