继承-C++

继承在我们日常中经常指我们的人伦关系中的父子关系,孩子继承父母的基因、习惯之类的,孩子也会有自己的个性等。然而在我们C++计算机语言中的类也存在继承,我们将作为"父亲"的类称为父类,将作为"孩子"的类称为子类,父类和子类也分别叫做基类和派生类,子类继承父类的内容。--继承的概念

一、基本概念及运用

1.简单例子

在计算机语言中我们的继承往往是把几个类的共同特征构建称为父类,将他们不同的特征放置于子类之中,就比如在学校中我们都是人(Person),但是我们人又分为老师(Teacher)和学生(Student)。其中老师和学生都拥有人的共同特征,就比方名字(name)、地址(address)、电话(tel)、年龄(age),但是我们学生有我们自己的学号而老师没有,老师有自己的职称而学生没有。于是我们就可以按照这种分类方法来构建这几种类:

cpp 复制代码
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
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();
return 0;
}

代码很好的诠释了类之间的关系,基类与派生类的联系以及派生类之间的差距。

2.继承定义

首先我们来看如何继承,即继承的格式:

在这张图片中我们可以清晰的看到继承所需要的格式:在定义子类的同时我们在其后面加上冒号,接着写上继承方式(公有继承,保护继承和私有继承三种)和父类名称。

我们上面提到了三种继承方式,有public、protected和private继承:

(1)public继承就相当于将基类中的所有内容继承到子类之中,但是访问权限依旧没有改变(public、protected、private)。

(2)protected继承就会将原本基类中的public成员的访问权限更改为protected,其余不变。

(3)private继承就会将父类中的public和protected访问权限改为private

此时我们在子类会发现我们无法调用父类中的private权限成员变量和成员函数,那我们能不能说父类中的private成员没有被继承呢,事实上并不能,及时我们在子类中我们无法调用,但是我们仍是继承了private权限内的成员,但是由于权限问题而无法调用。

使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。

在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。

我们再看继承类模板:

cpp 复制代码
namespace bit
{
    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();
        }
        const T& top()
        {
            return vector<T>::back();
        }
        bool empty()
        {
            return vector<T>::empty();
        }
   };
}
int main()
{
    bit::stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while (!st.empty())
    {
        cout << st.top() << " ";
        st.pop();
    }
    return 0;
}

我们在上面的代码中可以发现我们每次使用vector实现stack内部的函数都使用了vector<T>::,但是我们不能直接使用push_back等vector内部的函数来进行调用实现吗?答案是不能。基类是类模板时,需要指定⼀下类域,否则编译报错:error C3861: "push_back": 找不到标识,因为stack<int>实例化时,也实例化vector<int>了,但是模版是按需实例化,push_back等成员函数未实例化,所以找不到。

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

对于派生类和基类在使用的过程中我们有时候就会想基类和派生类是有共同特征的,那我们能不能把他们的共同特征给提取出来而互相转化呢?

答案是可以的。但是父类并不能赋值给子类,很容易理解的就是因为子类拥有父类所不具有的一些特征而子类不能够有效的初始化每一个特征。不过基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast 来进⾏识别后进⾏安全转换。

但是对于父类,子类所包含的内容是多余父类的,此时我们就可以想到我们能不能将其中重合的部分"切"出来给父类对象。于是在C++中我们就可以做到这一点来将子类赋值给父类。

我们直接以代码为例来进行分析:

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. 在继承体系中基类和派⽣类都有独⽴的作⽤域。

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

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

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

我们顺道来看一个代码例子:

cpp 复制代码
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
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; // 学号
};
int main()
{
    Student s1;
    s1.Print();
    return 0;
};

如果对于A、B两个类,B继承了A类,那么A和B类中的两个func构成什么关系()

A. 重载 B. 隐藏 C.没关系

这是一个经典的题目,放眼一看,重载和隐藏好像都构成了,但是我们需要知道的事,重载是指在同一作用域下的函数重写,而隐藏是指不同作用域下的函数重写,因此在两个不同的类中func函数构成的是隐藏关系。

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

跟我们前面类的学习一样,默认函数指的就是我们不写编译器也会帮我们自动生成的函数。那么在派生类中我们的默认成员函数是怎么样实现的呢?

1.我们在派生类中的默认构造函数是默认先调用基类函数的构造函数,如果我们在基类中没有默认的构造函数 ,即我们自己写的构造函数,我们就需要在派生类初始化的时候显式调用基类的构造函数。

2.派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

3.派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。

4.派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序。

5.需要注意的是初始化列表的顺序并不能决定调用基类与派生类的构造函数的顺序,一定是先调用基类的构造函数。

6.析构则是就近原则先析构派生类,再析构基类。

7.因为多态 中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系

我们已经知道了派生类的构造必须先调用基类的构造,于是我们就可以通过将类的构造函数私有化来使我们创建一个不能被继承的类

还有另外一种方式来创建一个不能被继承的类,在C++11中新增了一个关键字final,我们将其加到类名后面便不能被继承了:

五、继承与友元

我们需要知道的是友元并不能被继承,也就是说基类友元不能访问派生类私有成员和保护成员

我们直接来看例子:

cpp 复制代码
class Student;
class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};
class Student : public Person
{
protected:
    int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}
int main()
{
    Person p;
    Student s;
    Display(p, s);
    return 0;
}

如果我们拿着这串代码进去编译器就会编译报错:error C2248: "Student::_stuNum": ⽆法访问protected 成员,正是体现了上面所说,解决方式就是将Display也变成Student 的友元即可

六、继承与静态成员

如果基类定义了static静态成员,那么不管派生出多少个派生类,都只有这样一个static实例化成员,我们直接上代码观察:

cpp 复制代码
class Person
{
public:
    string _name;
    static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
    int _stuNum;
};
int main()
{
    Person p;
    Student s;

    // 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
    // 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
    cout << &p._name << endl;
    cout << &s._name << endl;

    // 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
    // 说明派⽣类和基类共⽤同⼀份静态成员
    cout << &p._count << endl;
    cout << &s._count << endl;

    // 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
    cout << Person::_count << endl;
    cout << Student::_count << endl;
    return 0;
}

七、多继承以及菱形继承

1.多继承与菱形继承的概念

什么叫做多继承呢,简单而言就形如爷爷有爸爸做儿子,爸爸有儿子,此时儿子就是多继承,也形如爸爸、妈妈的儿子,儿子继承于爸爸妈妈也是多继承,我们直接画图解释:

那么我们既然已经知道了可以有多继承,那么两个派生类经过多继承他们的最初基类会不会同一个呢?必然是有可能的,此时我们就构成了菱形继承

但是我们可以看到对于Son而言继承了两次Family,这就会导致数据的冗余和二义性问题。

我们来看代码分析问题:

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 _majorCourse; // 主修课程
};
int main()
{
    // 编译报错:error C2385: 对"_name"的访问不明确
    Assistant a;
    a._name = "peter";

    // 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

所以,实践中我们也是不建议 设计出菱形继承这样的模型的。

2.虚继承

我们在菱形继承问题中会有数据冗余和二义性的问题产生,此时我们就可以使用虚继承来进行解决:

我们在其"中间继承"类继承的时候在前面加上virtual表示其是虚继承即可。

我们来思考一个问题,在下面这张图中从上往下是基类和派生类,也构成了类似菱形的问题,那我们的另外一个virtual是加在C上还是D上呢?

答案是C,从谁开始分叉就加在谁身上。

感谢阅读和支持~

相关推荐
DanB2440 分钟前
Java笔记4
java·开发语言·笔记
Dddle11 小时前
C++:this指针
java·c语言·开发语言·c++
studyer_domi1 小时前
Matlab 234-锂电池充放电仿真
开发语言·matlab
yuanpan1 小时前
.net/C#进程间通信技术方案总结
开发语言·c#·.net
吃面不喝汤661 小时前
破解 Qt QProcess 在 Release 模式下的“卡死”之谜
开发语言·qt
不見星空1 小时前
2025年第十六届蓝桥杯软件赛省赛C/C++大学A组个人解题
c语言·c++·蓝桥杯
@十八子德月生1 小时前
8天Python从入门到精通【itheima】-1~5
大数据·开发语言·python·学习
jiunian_cn1 小时前
【c++】异常详解
java·开发语言·数据结构·c++·算法·visual studio
梁下轻语的秋缘2 小时前
每日c/c++题 备战蓝桥杯(洛谷P1387 最大正方形)
c语言·c++·蓝桥杯
熬夜学编程的小王2 小时前
【C++进阶篇】多态
c++·多态·静态绑定与动态绑定