C++ | 继承

🦌云深麋鹿
专栏C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了 模板,接下来这篇文章让我们进入到新的内容 继承 的学习,体会新的设计思路吧~

放个目录

  • [一 介绍继承](#一 介绍继承)
    • [1.1 上代码](#1.1 上代码)
    • [1.2 语法格式](#1.2 语法格式)
    • [1.3 继承方式&访问限定符](#1.3 继承方式&访问限定符)
      • [1.3.1 介绍](#1.3.1 介绍)
      • [1.3.2 怎么访问继承下来的不可访问成员?](#1.3.2 怎么访问继承下来的不可访问成员?)
    • [1.4 继承类模板](#1.4 继承类模板)
  • [二 基类和派生类之间的转换](#二 基类和派生类之间的转换)
    • [2.1 指针/引用赋值](#2.1 指针/引用赋值)
      • [2.1.1 (回顾)类型转换](#2.1.1 (回顾)类型转换)
      • [2.1.2 复制兼容转换](#2.1.2 复制兼容转换)
    • [2.2 派生类对象赋值给基类对象](#2.2 派生类对象赋值给基类对象)
    • [2.3 基类对象不能赋值给派生类对象](#2.3 基类对象不能赋值给派生类对象)
  • [三 继承中的作用域](#三 继承中的作用域)
    • [3.1 规则](#3.1 规则)
    • [3.2 选择题](#3.2 选择题)
      • [3.2.1 俩fun构成关系](#3.2.1 俩fun构成关系)
      • [3.2.2 程序运行结果](#3.2.2 程序运行结果)
    • [3.3 总结](#3.3 总结)
  • [四 派生类的默认成员函数](#四 派生类的默认成员函数)
  • [五 继承和友元](#五 继承和友元)
  • [六 继承与静态成员](#六 继承与静态成员)
    • [6.1 定义static成员变量](#6.1 定义static成员变量)
    • [6.2 测试](#6.2 测试)
    • [6.3 再写个同名变量](#6.3 再写个同名变量)
  • [七 多继承及其菱形继承问题](#七 多继承及其菱形继承问题)
    • [7.1 继承模型](#7.1 继承模型)
      • [7.1.1 写个 Assistant 类](#7.1.1 写个 Assistant 类)
      • [7.1.2 测试](#7.1.2 测试)
      • [7.1.3 菱形继承的问题](#7.1.3 菱形继承的问题)
    • [7.2 解决方案:虚继承](#7.2 解决方案:虚继承)
    • [7.3 来个选择题](#7.3 来个选择题)
    • [7.4 菱形继承特殊情况](#7.4 菱形继承特殊情况)
  • [八 继承和组合](#八 继承和组合)

一 介绍继承

1.1 上代码

cpp 复制代码
class Person {
public:
    void identity()
    {
        cout << "void identity()" << _name << endl;
    }
private:
    int _age = 18;
    string _name = "xxx";
};
class Student : public Person{
public:
    void study()
    {
        //...
    }
private:
    int _score = 0;
};

1.2 语法格式

cpp 复制代码
class sub_name : public base_name{};
  • 这里用public继承。

1.3 继承方式&访问限定符

1.3.1 介绍

  1. 简单来说,min(访问限定符 , 继承方式)。
  2. 一般来说最多用public继承。
  3. 不写继承方式,使用class默认是私有继承;使用struct默认是私有继承。

1.3.2 怎么访问继承下来的不可访问成员?

在父类里访问,函数设置成public。

cpp 复制代码
public:
    string name() {
        return _name;
    }

1.4 继承类模板

1.4.1 写一个stack继承vector

(1)上代码
cpp 复制代码
template<class T>
class stack01 :public vector<T> {
public:
    void push(const T& x)
    {
        vector<T>::push_back(x);
    }
    void pop()
    {
        vector<T>::pop_back();
    } 
    const T& top()
    {
        return vector<T>::back();
    } 
    bool empty()
    {
        return vector<T>::empty();
    }
};
注意点
  1. 模板调用之后才实例化,调用之前编译器不知道基类里有什么成员,会报错。
  2. 所以调用基类成员函数需要指明类域,或者加this指针。
cpp 复制代码
void push(const T& x)
{
    //vector<T>::push_back(x);
   this->push_back(x);
}
(2)比较之前的写法

适配器的方式就是组合。

cpp 复制代码
template<class T,class Container = vector<T>>
class stack02 {
public:
    void push(const T& x)
    {
        c.push_back(x);
    }
    void pop()
    {
        c.pop_back();
    } 
    const T& top()
    {
        return c.back();
    } 
    bool empty()
    {
        return c.empty();
    }
private:
    Container c;
};

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

2.1 指针/引用赋值

public继承的派生类对象 可以赋值给 基类的指针 / 基类的引⽤。形象地说就是切片或切割,指向派生类中切出来的基类那部分。

cpp 复制代码
wyzy::Student s;
wyzy::Person* p = &s;
wyzy::Person& r = s;

调试:

为什么可以直接引用?

2.1.1 (回顾)类型转换

隐式类型转换,会产生临时对象,引用需要加const。

cpp 复制代码
// false: Base& ref1 = Base();
const Base& ref2 = Base(); // true

2.1.2 复制兼容转换

这是一种特殊处理,没有产生临时对象,引用的是派生类的切片。

2.2 派生类对象赋值给基类对象

编译器会调用基类的拷贝构造函数(没写就默认生成),用切片构造一个基类对象。

cpp 复制代码
wyzy::Student s;
wyzy::Person p = s;

调试:

2.3 基类对象不能赋值给派生类对象

cpp 复制代码
wyzy::Person p;
wyzy::Student s = p;

报错:

三 继承中的作用域

3.1 规则

  1. 基类和派生类有独立的作用域。
  2. 基类和派生类有同名成员,派生类会隐藏基类的同名成员。
    ① 如果实在要访问基类的同名成员,就指定基类类域。
    ② 基类里访问成员变量,通过public函数继承给派生类。

3.2 选择题

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

3.2.1 俩fun构成关系

俩fun构成隐藏关系。

3.2.2 程序运行结果

编译报错,基类函数隐藏后调用不到。

怎么才能不报错?

还是指定基类类域。

cpp 复制代码
b.A::fun();

3.3 总结

  1. 建议不要跟派生类不要跟基类搞同名成员。
  2. 同名函数就构成隐藏,即使参数不同。

四 派生类的默认成员函数

4.1 常见的默认成员函数

4.1.1 构造函数

(1)编译器生成的

继承自基类那部分(当作一个整体),调用基类的默认构造。

  • 内置类型初始化为随机值。
  • 自定义类型调用默认构造。
(2)我们自己实现
①声明的时候给缺省值
cpp 复制代码
private:
    int _age = 18;
    string _name = "xxx";
②走初始化列表

如果我们初始化基类成员会报错(基类被当作一个整体)。

那我们怎么初始化基类成员嘞?

1.语法
cpp 复制代码
:Base(member),
...
2.初始化顺序

基类成员排在前面初始化(初始化顺序按声明顺序来)。

4.1.2 拷贝构造

没有什么额外的资源开销,就不需要写这个。

(1)怎么自己实现?

先实现一个基类:

cpp 复制代码
Person(const Person& p)
    :_age(p._age)
    , _name(p._name)
{
    cout << "Person(const Person& p)" << endl;
}

函数体里打印个东西方便观察。

初始化列表里怎么调用基类的拷贝构造?

用到上面的切片。

cpp 复制代码
:Base(...),

运用上述语法,实现派生类的拷贝构造:

cpp 复制代码
Student(const Student& s)
    :Person(s)
    , _score(s._score)
{
    cout << "Student(const Student& s)" << endl;
}
(2)测试
cpp 复制代码
wyzy::Student s1;
wyzy::Student s2(s1);

运行输出:

监视窗口:

4.1.3 赋值重载

写不写跟4.2一样的情况。

(1)怎么自己实现?

基类实现:

cpp 复制代码
Person& operator=(const Person& p)
{
    cout << "Person operator=(const Person& p)" << endl;
    if (this != &p)
        _name = p._name;
    return *this;
}

一样整个打印方便观察。

怎么调用基类的复制重载?

注意需要指明基类类域。

cpp 复制代码
Base::operator=(...);

派生类实现:

cpp 复制代码
Student& operator=(const Student& s)
{
    cout << "Student& operator= (const Student& s)" << endl;
    if (this != &s){
        Person::operator =(s);
        _score = s._score;
    }
    return *this;
}
(2)测试
cpp 复制代码
wyzy::Student s1;
wyzy::Student s2(20,"lll",100);
s2 = s1;

运行输出:

调试:

4.1.4 析构函数

写不写情况也一样。

(1)怎么自己实现?

基类:

cpp 复制代码
~Person(){
    cout << "~Person()" << endl;
}
显式调用基类析构?
cpp 复制代码
~Student() {
    ~Person();
    cout << "~Student()" << endl;
}

编译报错,因为一些场景下要析构函数构成多态,需要 派生类 和 基类 析构名称相同。

这里底层把析构函数的名称统一处理成destructor,所以和派生类的析构函数重名了,被隐藏了,需要指定基类类域。

cpp 复制代码
Person::~Person();
(2)测试
cpp 复制代码
wyzy::Student s;

运行结果:

为什么析构函数调用了两次?
  1. 默认先析构派生类成员,再析构基类成员。
  2. 所以我们不需要调用基类的析构函数,基类析构完会自动调用。
cpp 复制代码
~Student() {
    //~Person();
    cout << "~Student()" << endl;
}

4.2 实现不被继承的类

4.2.1 让构造函数私有化

把构造放到public前。

cpp 复制代码
    Person(int age = 18, string name = "xxx")
        :_age(age)
        , _name(name)
    {}
public:
    // ...

编译报错:

4.2.2 给final关键字

cpp 复制代码
class Person final{
    // ...
}

编译报错:

五 继承和友元

给基类写一个友元:

cpp 复制代码
class Person {
public:
    friend void Display(const Person& p);
    // ...    
}
void Display(const Person& p)
{
    cout << p._name << endl;
}

编译通过。

  • 友元关系不能被继承,基类的友元不能访问派生类的private和protected成员。
cpp 复制代码
class Person {
public:
    friend void Display(const Person& p, const Student& s);
    // ...    
}
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

编译报错:

六 继承与静态成员

6.1 定义static成员变量

基类定义一个static成员,在整个继承体系中只有这一个成员。

cpp 复制代码
class Person {
    // ...
private:
    // ...
    static int _count;    
}
int Person::_count = 0;

6.2 测试

cpp 复制代码
wyzy::Student s1;
wyzy::Student s2;

调试:

6.3 再写个同名变量

在派生类写个同名成员:

cpp 复制代码
class Student : public Person{
    // ...
private:
    // ...
    int _count = 0;
}

调试:
可以发现static _count被隐藏了。

七 多继承及其菱形继承问题

7.1 继承模型

单继承:派生类继承自一个基类。

多继承:派生类继承自多个基类。

菱形继承:多继承可能导致菱形继承。

7.1.1 写个 Assistant 类

依照上图,Assistant代表上图的Dderived。

cpp 复制代码
class Assistant : public Student, public Teacher {
public:
    Assistant(int age = 20, string name = "aaa", int score = 0, int salary = 0, string majorCourse = "x")
        /*:_name("yyy")*/
        :Student(age, name, score)
        ,Teacher(age, name, salary)
        , _majorCourse(majorCourse)
    {}
private:
    string _majorCourse = 0;
};

7.1.2 测试

cpp 复制代码
 wyzy::Assistant a;

调试:

7.1.3 菱形继承的问题

菱形继承有 数据冗余 和 ⼆义性(有两份基类切片) 的问题。

在Assistant里写个函数访问_age:

cpp 复制代码
void grow(int years = 1) {
    _age += years;
}

编译报错:

7.2 解决方案:虚继承

引入菱形虚拟继承,会损失性能,最好不要设计出这种虚继承。

怎么实现?

加virtual关键字。

cpp 复制代码
class Student : virtual public Person{
    // ...
}
class Teacher : virtual public Person{
    // ...
}

报错问题就不一样了:

具体实现:构造函数也有多份,析构函数也有多份,比较复杂。

7.3 来个选择题

是多继承中指针偏移的问题:

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    return 0;
}

需要选出正确的一个:

A:p1 == p2 == p3 B:p1 < p2 < p3

C:p1 == p3 != p2 D:p1 != p2 != p3

这里涉及多继承中的切片。

选C:

7.4 菱形继承特殊情况

怎么特殊嘞?如图:

  • virtual加在最先触及多继承那一层。
    这里就加在Derived1和Derived2上。

八 继承和组合

  • 继承是 is-a ,组合是 has-a。
  • 这两种都是一种代码复用。

组合优于继承

  • 组合相比于继承,更加体现低耦合思想。
  • 当然特定场景下,该用继承还是要用继承。

继承 的学习就到这里,下一篇也是学习 C++面向对象的特性[多态],不久后就会更出来啦~


相关推荐
小辉同志2 小时前
Epoll+线程池
开发语言·c++·c·线程池·epoll
史迪仔01122 小时前
[QML] Qt Quick Dialogs 模块使用指南
开发语言·前端·c++·qt
谭欣辰2 小时前
Floyd算法:动态规划解最短路径
c++·算法·图论
杨凯凡2 小时前
【019】IO/NIO 概念:Web 开发要掌握到什么程度
java·开发语言·nio
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 84. 柱状图中最大的矩形 | C++ 两次单调栈基础扫法
c++·算法·leetcode
季明洵2 小时前
Java基础---逻辑控制(上)
java·开发语言·循环结构·分支结构·顺序结构
小苗卷不动2 小时前
OJ刷题之栈和排序(中等)
c++
沫璃染墨2 小时前
重生之我要手写 C++ list:从底层结构到 const 迭代器与迭代器失效全解
开发语言·c++
paeamecium2 小时前
【PAT甲级真题】- Favorite Color Stripe (30)
数据结构·c++·算法·pat