C++自己的族谱规则:继承详解上


文章目录


前言

在写代码时我们经常面临一个问题,两个类具有相同成员变量/函数,这会导致代码的冗余,比如Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。在此背景下C++设计出了继承。


一、继承的概念

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

实例:

cpp 复制代码
// 父类:Person 人
class Person
{
public:
    // 身份认证:进校园、图书馆、实验室扫码
    void identity()
    {
        cout << "void identity() " << _name << endl;
    }
protected:
    string _name = "张三";
    int _stuid;         // 学号/人员编号
    string _address;    // 地址
    string _tel;        // 电话
    int _age = 18;      // 年龄
};

// Teacher 公有继承 Person
class Teacher : public Person
{
public:
    // 授课
    void teaching()
    {
        cout << _name << " 正在授课" << endl;
    }
protected:
    string title; // 职称
};

// Student 公有继承 Person(补全)
class Student : public Person
{
public:
    // 学习
    void study()
    {
        cout << _name << " 正在学习" << endl;
    }
protected:
    // 学生独有的成员,例如:专业
    string major;
};

二、继承的定义

1.定义格式

下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)

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

  1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

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

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

  4. 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式。

cpp 复制代码
// class 默认 private 继承
class Base{};
class Son : Base{};   // 等价于 class Son : private Base{}

// struct 默认 public 继承
struct Base2{};
struct Son2 : Base2{};// 等价于 struct Son2 : public Base2{}
  1. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

注意 :

实际日常中最常用这两个,同时想要快速记住是否可用,只要看谁的权限小就能用谁。

3.继承类模板

cpp 复制代码
namespace MY
{
    //template<class T>
    //class vector
    //{};
    // stack和vector的关系,既符合is - a ,也符合has - a
    template<class T>
    class stack : public std::vector<T>
    {
    public:
        void push(const T& x)
        {   
            //基类是类模板时,需要指定⼀下类域,
            //否则编译报错
            //: error C3861 : "push_back":
            //找不到标识符

            //因为
            //stack<int>
            //实例化时,也实例化vector<int>了
            
            //但是模版是按需实例化,
            //push_back等成员函数未实例化,所以找不到。

            vector<T>::push_back(x);
            //push_back(x);
        }
        void pop()
        {
            vector<T>::pop_back();
        }
        const T& top()
        {
            return vector<T>::back();
        }
        bool empty()
        {
            return vector<T>::empty();
        }
    };
}

记住 :普通类 继承 类模板(必须指定模板参数)

三、基类和派生类间的转换

  1. 通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加 const,如:int a = 1; const double& d = a;

    public 继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象。

  2. 派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的 ,这个过程就像派生类自己定义部分成员切掉了一样,所以也被叫做切割或者切片,如下图中所示。

  3. 基类对象不能赋值给派生类对象。

    因为派生类对象具有基类对象没有的,成员变量或函数。

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

cpp 复制代码
class Person
{
protected :
    string _name; // 姓名
    string _sex;  // 性别
    int    _age;  // 年龄
};

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

int main()
{
    Student sobj ;
    // 1.派生类对象可以赋值给基类的指针/引用
    Person* pp = &sobj;
    Person& rp = sobj;

    // 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的
    Person pobj = sobj;

    //2.基类对象不能赋值给派生类对象,这里会编译报错
    // sobj = pobj;

    return 0;
}

四、 继承中的作用域

1.隐藏规则

  1. 在继承体系中基类和派生类都有独立的作用域。

    查找的规则为:遵循由内到外 的名字查找顺序:

    先在派生类自身的作用域中搜索这个名字;

    如果找到匹配的名字,查找立即终止,不会再往上层搜索;

    如果派生类中没找到,才会进入基类作用域继续查找。

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

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。与参数列表、返回值类型是否一致完全无关。

    也就是说,哪怕派生类的函数和基类函数参数不同、返回值不同,只要名字一样,派生类的函数就会把基类中所有同名函数(包括全部重载版本)全部隐藏。

  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

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;
};
  1. A和B类中的两个func构成什么关系?

    按照我们的规则,它们在不同作用域且函数名相同所以那它就是隐藏。

  2. 上面程序的编译运行结果是什么

    这题的答案是编译报错。

    执行 b.fun()编译器先在 B 类(派生类)的作用域找名字fun

    找到了 B::fun(int i),只要找到同名,查找就停止,不会再去基类 A 里面找!

    现在找到的函数要求传入一个 int,但是代码里 b.fun() 没有传参数,参数列表不匹配。

    直接报编译错误,不是运行时错误。

注意 :很多人以为会自动去找 A 的无参 fun,这是错的。派生类的同名函数会屏蔽基类所有同名函数,不管参数一不一样。

总结

由于继承知识较多我们分为上下两个篇章来分析,有兴趣请关注我的主页。

相关推荐
老王爱玩车1 小时前
深入理解指针6
c语言·开发语言·学习
牢姐与蒯1 小时前
位运算及其oj题
c++·算法
渡我白衣1 小时前
深入理解 Transformer:Decoder与Masked_Attention
linux·服务器·网络·c++·人工智能·深度学习·transformer
励志不掉头发的内向程序员1 小时前
【从零写一个CAD 04】中键拖动平移:抓住一个点,让它一直待在鼠标底下
开发语言·c++·qt·学习·系统架构·计算机外设
UIU1141 小时前
同一个变量在两个 .c 文件里类型不一致,程序会怎样?
c++·学习·c#
Logic1014 小时前
C语言/数据结构字符串题解:按字符类型重排(数字→字母→符号)——三趟扫描稳定分区
c语言·数据结构·字符串·时间复杂度·计数排序·算法题·ascii
weixin_4241836110 小时前
开放—封闭原则实战(下):基于策略的流程编排与编译期多态
c++·分布式·系统架构
Logic10110 小时前
C语言/数据结构位运算题解:异或XOR找出学习计划中的“独特学习时间“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
wuminyu10 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++