【C++】12.继承

文章详细介绍了C++中的继承概念,包括单继承、多继承和菱形继承。强调了菱形继承带来的数据冗余和二义性问题,以及如何通过虚继承来解决。此外,还讨论了继承与组合的区别,提倡在设计时优先考虑组合以降低类之间的耦合度。
摘要由CSDN通过智能技术生成

1.引入继承

学生管理系统

  • 学生

  • 老师

  • 社管阿姨

  • 保安大叔

    4个类 4个类有很多重复的东西

    name age telephone ...等

    继承 Person->老师 学生等

从这里我们可以看到继承目的:类之间的复用

2.继承

1°定义

class Student(派生类):public(继承方式) Person(基类)

{};

2°访问方式

#include <iostream>
using namespace std;

class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter";//姓名
    int _age = 18;//年龄
};

//继承
//private和protected在当前类无差别
//在继承的子类有差别 
//继承下来的private不可用 protected可以用
//谁的范围小就取谁
//public>protected>private
//实际当中都用public继承 几乎不使用protected/private
class Student :public Person
{
protected:
    int _stuid;//学号
};

class Teacher :public Person
{
protected:
    int _jobid;//工号
};

int main()
{
    Student s;
    Teacher t;
    s.Print();
    t.Print();
    return 0; 
}

3.切片

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用->切片

  • 而基类对象不能赋值给派生类对象 范围大的可以给范围小的

  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指

    向派生类对象时才是安全的

class Person
{
protected:
    string _name; //姓名
    string _sex;  //性别	
    int _age;	  //年龄
};

class Student :public Person
{
public:
    int _No;//学号
};

int main()
{
    Person p;
    Student s;

    //子类和父类之间的赋值兼容规则
    //1.子类对象可以赋值给父亲对象/指针/引用
    p = s;
    Person* ptr = &s;
    Person& ret = s;
    //切片
    //自己的_No不给 其他继承的可以赋值
    //指针和别名也是 只会有部分的指针和别名
    //2.反过来行不行?
    //s=p;绝对不行
    //3.Student* sptr=(Student*)ptr;
    //有可能可以 这个父类的指针指向子类对象v 
    return 0;

4.继承中的作用域

就近:如果两个变量是一样的不会报错 先找自己类里的变量 再找父类里的变量

如果就想访问父类的同名变量的话 加上Person::进行限定

class Person
{
protected:
    string _name = "小李子"; // 姓名
    int _num = 111; // 身份证号
};
class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:" << _name << endl;
        cout << " 身份证号:" << Person::_num << endl;//指定访问父类
        cout << " 学号:" << _num << endl;//默认访问子类
    }
protected:
    int _num = 999; // 学号
};
void Test()
{
    Student s1;
    s1.Print();
}

int main()
{
    Test();
    return 0;
}
//这两个num不会报错 因为两个num在不同的作用域
//那么访问num的时候到底是父类的num还是子类的nums
//子类的num 就近原则 先找自己类里面 再找其他类
//当父类和子类同时有同名成员时,子类的成员隐藏了父类的成员
//那假设就想访问父类怎么办
//指定 Person::_num

  • 练习

  • A和B的fun构成什么关系

    a.重载 b.重写 c.重定义 d.编译不通过/以上都不对

class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
class B : public A
{
public:
    void fun(int i) {
        A::fun();
        cout << "func(int i)->" << i << endl;
    }
};
void Test()
{
    B b;
    b.fun(10);
    //b.fun();//调父类调不动      不可以
    //b.A::fun();//指定去调父类的 可以
};

很容易选到重载 应该是c 重定义 也就是隐藏

隐藏不是说调不动 而是要指定父类去调用

重载的要求:必须在同一作用域

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

  • 派生类的默认成员函数是如何生成的?
  1. 构造:必须调用基类的构造函数初始化基类的那一部分成员 如果基类没有构造函数 构造时必须显示调用(加上作用域)
  2. 拷贝构造:必须调用基类的拷贝构造
  3. 赋值:必须调用基类的operator=
  4. 析构:派生类的析构函数调用完后自动调用基类的析构函数 保证正确顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构

#include <iostream>
using namespace std;

class Person
{
public:
    Person(const char* name = "peter")//构造
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)//拷贝构造
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p)//赋值
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;

        return *this;
    }

    ~Person()//析构
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

//子类继承的父类 父类初始化就调用父类的构造函数
//子类不写默认函数 会调父类的默认构造函数 全缺省可以调
class Student :public Person
{
public:
    Student(const char* name, int stuid)
        :Person(name)//指定
        ,_stuid(stuid)
    {
        cout << "Student(const char* name, int stuid)" << endl;
    }

    Student(const Student& s)
        :Person(s)//除了stuid 其余的继承父类 先调用父类的 再调用自己的
        ,_stuid(s._stuid)
    {
        cout << "Student(const Student& s)" << endl;
    }

    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            Person::operator=(s);//调父类 完成切片 隐藏了父类 所以要指定作用域 调父类的时候全部需要指定
            _stuid = s._stuid;//自己成员
            cout << "Student& operator=(const Student& s)" << endl;
        }
        return *this;
    }

    ~Student()
    {
        //还是需要指定
        //Person::~Person();//子类的析构函数和父类的析构函数构成隐藏 因为他们的名字会被编译器统一处理成destructor(跟多态有关)
        //析构时会发现有三个析构 多一个Person析构 应该去掉第一个Person析构
        //不需要 析构函数会自动调用
        //结束时会自动调用父类的析构函数 因为这样才能保证先析构子类后析构父类
        //不需要显示的调 因为显示的调会多出一个析构 而且析构顺序不对
        cout << "~Student()" << endl;
    }
protected:
    int _stuid;
};


int main()
{
    //Student s1("jack",1);
    //Student s2(s1);//不写也会调用父类的拷贝构造 写了后先调父类 再调子类
    //Student s3 = s1;
    //Student s4("rose", 2);//析构时会发现有三个析构 多一个Person析构 应该去掉第一个Person析构
    return 0;
}

s1

s2

s3

s4

  • 派生类实现默认成员函数时 先要调用基类相关函数把基类那一部分成员实现功能(需

    要指定基类) 再实现自己类里面的成员功能

  • 析构函数比较特殊 不需要去实现 因为调用基类的析构函数的话 基类会先析构 然后派

    生类再析构 基类会再次析构**(当派生类析构后 基类会自动调用析构函数 保证析构的顺**

    序)

  • 除了析构以外 全部都需要显示调用父类 相当于切片那一段直接找父类的就行

    然后子类自己的成员自己处理

  • 如何设计一个不能被继承的类?

构造私有 析构私有

继承后不可见私有的 子类调不动父类的构造和析构 子类对象都生成不了 所以不能被

继承

6.继承和友元

父类中友元关系不能被继承下来

7.基类与静态成员

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

个子类,都只有一个static成员实例。

#include <iostream>
using namespace std;

class Person
{
    public:
        Person() { ++_count; }
    protected:
        string _name; // 姓名
    public:
        static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
    int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
    string _seminarCourse; // 研究科目
};

int main()
{
    Person p;
    Student s;

    p._count = 1;
    s._count = 2;
    
    Person::_count++;
    cout << Person::_count << endl;
    cout << Student::_count << endl;
    //派生了多少子类 静态成员只有一个
    return 0;
}

_count都是3

8.菱形继承

1°继承类型

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

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

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

  • 菱形继承的问题:

    **Assistant会有两份相同数据导致数据冗余 两份数据到底用哪份 产生歧义(二义性) **

    需要指定是哪个 但还是有数据冗余的问题

1.数据冗余

2.二义性

  • 如何解决菱形继承问题?

    官方引入了virtual继承 虚继承 解决了数据冗余和二义性

class Person
{
public:
    string _name; // 姓名
};
class Student : virtual public Person//添加virtual
{
protected:
    int _num; //学号
};
class Teacher : virtual public Person//添加virtual
{
protected:
    int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse; // 主修课程
};
void Test()
{
    // 这样会有二义性无法明确知道访问的是哪一个
    Assistant a;
    a._name = "peter";//不加virtual会报错

    // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
}
  • C++的缺陷有哪些?

多继承就是一个问题->菱形继承->虚继承->底层结构的对象模型非常复杂 且有一定

效率损失

  • 什么是菱形继承?

多继承的一种特殊情况

  • 菱形继承问题是什么?

数据冗余和二义性

  • 如何解决菱形继承?

虚继承

  • 那么解决原理是什么?

需要了解内存对象模型 也就是对象在内存中是怎么存的

2°内存对象模型

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;
    cout << sizeof(d) << endl;//8+8+4=20 //加了virtual变为了24
    
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    d._a = 6;
    //a放到了公共位置
    //多了两个像指针一样的东西 变为了24
    //所以这两个指针是干啥的
    //两个指针最后表示的是偏移量 多付出了8个字节
    //存偏移量的表叫虚基表->虚基类
    return 0;
}

这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚

基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

通过偏移量地址(指针)->偏移量值->对象

9.总结

  • 多继承是C++的缺陷之一,Java设计的时候避开了多继承

继承与组合

//继承
class A{};
class B:public A
{};

//组合
class C{};
class D
{
    C c;
}

继承是一种白箱(看得见)复用 父类对子类基本是透明的 但是一定程度破坏了父类的封装

组合是一种黑箱(看不见)复用 C对D是不透明的 C保持着封装

对比认为组合是更好的

组合的类耦合度更低

继承的类是一种高耦合

  • 面试:用组合还是继承?

更符合is a 就继承

更符合has a 就组合

都可以 优先使用组合

【C++】12.继承 完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的小恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值