C++继承

C++继承

  • 一.继承的概念和定义
    • 1.继承的概念
    • 2.继承的基本语法
    • 3.继承的代码演示
  • 二.基类和派生类对象赋值转换
  • 三.继承中的作用域
    • 1.概念
    • 2.演示
    • 3.经典题目
  • 四.派生类的默认成员函数
    • 1.编译器默认生成的成员函数
    • 2.构造函数
    • 3.拷贝构造
    • 4.赋值运算符重载
    • 5.析构函数
  • 五.继承与友元
  • 六.继承与静态成员
  • 七.菱形继承和菱形虚拟继承
    • 1.单继承,多继承,菱形继承的概念
    • 2.菱形继承的问题
    • 3.菱形虚拟继承解决菱形继承问题
      • 1.菱形虚拟继承的概念
      • 2.验证菱形虚拟继承
    • 4.菱形虚拟继承解决菱形继承问题的原理
      • 1.说明
      • 2.验证
      • 3.几个问题
        • 1.为什么类B和类C要存储这个偏移量呢?
        • 2.为什么要搞到一个虚基表当中,为什么不能直接记录类A成员的地址呢?
        • 3.类B的对象模型是什么样的呢?
  • 八.继承与组合
  • 九.小拓展
    • 1:如何实现一个不能被继承的类?
      • 1.C++11之前的实现方案
      • 2.C++11新增关键字final
    • 2:如何统计一个父类以及其子类一共实例化出多少个对象?

一.继承的概念和定义

1.继承的概念

在这里插入图片描述
可以把一些类的共性提取出来单独封装成一个类,让这个类作为基类
其他类继承这个类作为派生类

比方说下面这个Student类,Teacher类和Person类:
继承之前:
在这里插入图片描述
继承之后:
在这里插入图片描述
可见继承的确就是类设计层次的复用

2.继承的基本语法

了解了继承的概念之后,下面我们来学习一下继承的基本语法
比方说父类是A,子类是B
那么子类就可以通过在类名后面加上: 继承方式来继承父类

class A
{...}
class B : public A
{...}

这里的继承方式分为public(公有继承),protected(保护继承),private(私有继承)
继承的父类的成员的属性也分为三种:public(公有成员),protected(保护成员),private(私有成员)
因此经过排列组合就可以组合出这9种情况:
在这里插入图片描述

3.继承的代码演示

下面我们来演示一下,就拿Student,Teacher,Person类为例
在这里插入图片描述
继承成功
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
下面我们来演示一下,看看到底能不能赋值转换呢?
在这里插入图片描述
可见,完全可以完成赋值转换

我们都知道,临时变量具有常性,因此对临时变量进行引用的话必须要加const.
而且发生类型转化时就会产生临时变量
就像是这样

double d=1.1;
int& ri=d;//此时没有加const就会报错,因为d是double类型,转换为int类型时会产生临时变量

而我们刚才的时候

//引用赋值
Person& rp = s;

在这里s对象是Student类型,引用赋值给rp对象时并没有任何报错或者报警,说明的确没有产生任何临时变量
也就是说基类和派生类对象进行赋值转换时是不会发生类型转换的

三.继承中的作用域

1.概念

在这里插入图片描述
不过不建议在子类中设计跟父类同名的成员,否则会让代码变得复杂

2.演示

下面我们来演示一下
在这里插入图片描述

3.经典题目

class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)->" << i << endl;}
};

请问这两个fun函数是什么关系?
A:两个fun构成函数重载
B:两个fun构成隐藏关系
C:编译时报错
D:运行时报错

注意:函数重载要求在同一作用域
而且只要这两个函数的函数名相同,就会构成隐藏关系
因此答案是B,而不是A

下面这两种调用方法正确吗?

B b;
b.fun();
b.fun(1);

答案是:b.fun();不正确,因为子类的fun和父类的fun构成了隐藏关系
因此按照就近原则会先匹配子类的fun函数,而子类的fun函数要求传入一个参数,因此这种调用方法不正确,
而下面这种调用方法是正确的b.fun(1);

那么我就是想要调用父类的fun函数呢?
指定类域

b.A::fun();

在这里插入图片描述

四.派生类的默认成员函数

在这里插入图片描述
在这里插入图片描述

下面来演示一下

1.编译器默认生成的成员函数

编译器默认生成的成员函数会去调用父类相应的默认成员函数

//基类
class Person
{
public://构造Person(const char* name="root"):_name(name){cout << "Person的构造函数调用" << endl;}//拷贝构造Person(const Person& p):_name(p._name){cout << "Person的拷贝构造函数调用" << endl;}//赋值运算符重载Person& operator=(const Person& p){cout << "Person的赋值运算符重载函数调用" << endl;if (this != &p){_name = p._name;}return *this;}//析构~Person(){cout << "Person的析构函数调用" << endl;}
protected:string _name;
};
//派生类,公有继承
class Student : public Person
{
public:
protected:int _StudentId;
};
int main()
{Student s;Student s2(s);s = s2;return 0;
}

在这里插入图片描述

2.构造函数

如果父类没有默认的构造函数,那么我们就要去给子类提供构造函数
但是父类成员不能在子类构造函数的初始化列表当中一个一个地进行初始化
在这里插入图片描述
而应该调用父类的构造函数去初始化父类成员:

Student(const char* name,int StudentId):Person(name)//显式调用父类的构造函数去初始化父类成员,_StudentId(StudentId)
{}

关于这个规定,我们可以理解为父类成员必须调用父类的成员函数
子类成员必须调用子类的成员函数
在这里插入图片描述
子类成员的初始化顺序:先初始化父类成员,然后才会初始化子类特有的成员
也就是说子类构造函数的初始化列表中先声明的是父类成员,其次才是子类特有的成员

父类成员默认调用父类的构造函数进行初始化,有些类似于自定义对象会默认调用自定义对象的默认构造函数

3.拷贝构造

下面我们来完成子类的拷贝构造,注意:父类成员依然是要调用父类的拷贝构造
而且此时我们之前提到的基类和派生类的对象赋值转换就派上用场啦

Student(const Student& stu):Person(stu)//利用父子之间的对象赋值转换规则  显式调用父类的拷贝构造函数,_StudentId(stu._StudentId)
{}

在这里插入图片描述

4.赋值运算符重载

对于赋值运算符重载来说,因为子类和父类的赋值运算符重载的函数名都是operator=,所以构成隐藏关系,所以需要指定父类的作用域才能调用父类的赋值运算符重载函数

Student& operator=(const Student& stu)
{if (this != &stu){Person::operator=(stu);_StudentId = stu._StudentId;}return *this;
}

在这里插入图片描述

5.析构函数

对于析构函数,
1.为了保证先析构子类成员,再析构父类成员
所以编译器说:父类析构函数不需要我们显式调用,子类析构函数结束时会自动调用父类的析构函数
2.析构函数的名字会被编译器统一特殊处理为destructor(),因此需要显式指定父类的作用域来调用父类的析构函数
在这里插入图片描述

五.继承与友元

友元关系不能继承,也就是说父类的友元不能访问子类的私有和保护成员,
可以理解为父类的朋友不一定是子类的朋友
在这里插入图片描述
在这里插入图片描述

六.继承与静态成员

在这里插入图片描述
下面我们来演示一下

//基类
class Person
{
public:static int _num;
protected:string _name = "root";
};//静态成员变量:类内声明,类外定义
int Person::_num = 10;//派生类,公有继承
class Student : public Person
{
public:
protected:int _StudentId = 123;
};int main()
{Person p;Student s;p._num++;s._num++;cout << p._num << endl;cout << s._num << endl;cout << &p._num << endl;cout << &s._num << endl;return 0;
}

在这里插入图片描述
可见,对于静态成员,子类继承到的只是静态成员的使用权
并没有拷贝一份副本给自己,而是跟父类共用同一个静态成员

七.菱形继承和菱形虚拟继承

1.单继承,多继承,菱形继承的概念

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
在这里插入图片描述
多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承

多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序

在这里插入图片描述
多继承当中有一种特殊情况:菱形继承
菱形继承:多继承的情况下,两个直接父类有一个共同的祖先
在这里插入图片描述
注意:菱形继承也可以这个样子,不是说必须要是菱形的继承关系才叫做菱形继承
在这里插入图片描述

2.菱形继承的问题

在这里插入图片描述
下面我们来证明一下菱形继承的问题

class Person
{
public:string _name;
};class Student : public Person
{
protected:int _id;
};class Teacher : public Person
{
protected:int _wage;
};
//多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序
class Assistant : public Student,public Teacher
{
protected:int _course;
};int main()
{Assistant a;//a._name = "wzs";//编译报错,Assistant::_name不明确  因为菱形继承导致了二义性a.Student::_name = "张三";//指定类域a.Teacher::_name = "李四";//指定类域//尽管通过指定类域可以解决二义性//但是代码冗余依旧无法解决cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}

二义性的问题:
在这里插入图片描述
数据冗余的问题:
在这里插入图片描述
在这里插入图片描述

3.菱形虚拟继承解决菱形继承问题

1.菱形虚拟继承的概念

只需要在继承的时候加上virtual关键字即可:
在这里插入图片描述

注意:要在继承Person的类上加virtual关键字,进行虚继承
Assistant类不需要加virtual关键字

因此,对于这种菱形继承,应该这么修改为菱形虚拟继承
在这里插入图片描述

2.验证菱形虚拟继承

下面我们来验证一下到底有没有很好地解决这个菱形继承的问题呢?

class Person
{
public:string _name;
};
//菱形虚拟继承
class Student :virtual public Person
{
protected:int _id;
};
//菱形虚拟继承
class Teacher :virtual public Person
{
protected:int _wage;
};
//多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序
class Assistant : public Student,public Teacher
{
protected:int _course;
};int main()
{Assistant a;a._name = "wzs";//解决了二义性cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;a.Student::_name = "张三";//也可以指定类域访问cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;a.Teacher::_name = "李四";//也可以指定类域访问cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;return 0;
}

在这里插入图片描述
无论指定那个类域来修改Person成员,都是只修改那一个变量
说明Assistant类当中只有一份Person成员
成功解决了二义性和数据冗余问题

4.菱形虚拟继承解决菱形继承问题的原理

1.说明

为了更好地说明菱形虚拟继承的原理
我们给出一个简化的菱形虚拟继承体系,并且通过调试中的内存窗口来观察对象成员
在这里插入图片描述

class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

2.验证

在这里插入图片描述
我们可以看出
1:
在这里插入图片描述
在d对象刚被创建之后就更新了两个值
2:类D中的4种成员:
类A的成员存到一个单独的位置
类B和类C成员位置有一个"奇怪"的值
类D自己的成员单独存放
在这里插入图片描述
下面我们来看一下那个奇怪的数字到底是什么?
在这里插入图片描述
这两个指针叫做虚基表指针
这两个表叫做虚基表,
虚基表当中存放的是偏移量
在这里插入图片描述
因此我们就可以得出一个重大结论:
在这里插入图片描述

3.几个问题

1.为什么类B和类C要存储这个偏移量呢?

如果发生了D的对象赋值转换为B对象或者C对象时
需要找到D对象当中的B/C成员中的A成员才能赋值

D d;
B& rb=d;
C& rc=d;

切片的时候就需要通过偏移量去计算位置,也就需要找到A成员也就是_a

2.为什么要搞到一个虚基表当中,为什么不能直接记录类A成员的地址呢?

在这里插入图片描述
验证类D实例化出的每一个对象都共用同一张虚基表
在这里插入图片描述

3.类B的对象模型是什么样的呢?

在这里插入图片描述
一起来看一下:
在这里插入图片描述

八.继承与组合

继承和组合都是一种复用,最大的区别是访问方式有所不同
在这里插入图片描述
在这里插入图片描述

九.小拓展

1:如何实现一个不能被继承的类?

1.C++11之前的实现方案

我们知道子类的构造函数当中需要调用父类的构造函数去对父类的成员进行初始化
因此我们可以把父类的构造函数私有化,这样子类就调用不到父类的构造函数了,这样的话继承之后也没有意义了,因为此时子类不能定义对象

class A
{
private:A() {}
};
class B :public A
{};
int main()
{B b;return 0;
}

在这里插入图片描述

2.C++11新增关键字final

关键字final:用来修饰父类,让父类不能被继承
在这里插入图片描述

2:如何统计一个父类以及其子类一共实例化出多少个对象?

我们知道子类的构造函数当中需要调用父类的构造函数去对父类的成员进行初始化
因此我们可以在父类当中定义一个静态成员变量_count
并且在父类的构造函数当中++这个_count
_count当中的值就是实例化出对象的个数

class A
{
public:A() { _count++; }static int _count;
};
int A::_count = 0;
class B : public A
{};
class C : public A
{};
class D : public A
{};
void test()
{A a;B b;C c;D d;cout << A::_count << endl;
}

在这里插入图片描述

以上就是C++继承的全部内容,希望能对大家有所帮助!