【C++】面向对象三大特性之—— 继承 | 详解

清晨朝暮 2024-08-13 14:35:01 阅读 95

目录

继承的概念

继承语法格式

继承方式

隐藏

继承下来的成员和父类是不是同一份

隐藏

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

继承中的作用域

派生类的默认成员函数

构造

拷贝构造

赋值重载

析构

继承与友元

继承与静态成员

菱形继承及菱形虚拟继承

多继承

菱形继承

菱形虚拟继承

结语


继承的概念

首先我们来谈谈什么是继承

比如有一天,你回到家了之后你爸爸突然对你说:哎呀最近看你学那么辛苦,实在是有些不忍心了

悄悄告诉你吧,我们家其实有一些你不知道的产业(滑稽

在现实生活中,继承就是家里的东西传下来给你了

在C++中,继承就是一个类的成员(成员变量与成员函数)都传给你了

举个例子:

我们现在有一个外卖系统,这里面的骑手、商家、用户都有着共同的信息,比如名字、地址等等

那我们如果每一个类都写一样的信息的话,是不是就有点冗余啊,浪费空间,那我们能不能想办法不劳而获一下呢?(滑稽

这时候就可以引出我们的继承了

我们可以写一个父类(也叫基类)来存储这些共同的信息,然后让下面的三个类全部继承这个父类即可

继承语法格式

<code>class 子类: 继承方式 父类

{

//内容

};

举个例子:

class Person

{

public:

void Print()

{

cout << _num << endl;

}

protected:

int _num = 0;

};

class Student : public Person

{

private:

int _id = 3;

};

继承方式

继承方式分为公有、保护、私有继承,我们先来看一张表:

好,大家把这张表背一背哈(滑稽

开玩笑哒,我们只需要总结规律即可,这个总结了规律之后,以后可以直接现推关系

我们可以将三种继承方式抽象成一种大小关系

public   >    protected    >    private

而我们在继承的时候,只需要将两个进行比较,取小的那个即可

什么意思呢?

假设父类成员有一个公有继承,而我们的继承方式也是公有,所以在子类,这个成员就是公有的

如果这个成员在父类是保护,但我们继承方式是公有继承,公有 > 保护,小的那个就是保护,所以在子类里面这个成员就是保护

如果这个成员在父类里面是私有成员,但我们是公有继承,公有 > 私有,取小就是私有,所以这个成员在子类里面就是私有成员

同理,如果在父类是公有成员,但继承方式是保护,那么这个成员在子类里面就是保护

。。。。。。

隐藏

继承下来的成员和父类是不是同一份

在解决隐藏之前,我们先来看一个问题:继承下来的东西是同一份吗?

举个例子:

<code>class Person

{

public:

void Print()

{

cout << _num << endl;

}

int _num = 0;

};

class Student : public Person

{

private:

int _id = 3;

};

int main()

{

Student s;

Person p;

//继承下来的变量

s._num += 1000;

p._num += 500;

cout << s._num << endl;

cout << p._num << endl;

cout << endl << endl;

//继承下来的函数

s.Print();

p.Print();

return 0;

}

这是一个子类继承了一个父类,然后我们在main函数里面分别定义了一个父类对象和子类对象

这时父类和子类是分开的两个对象,只不过子类对象继承了父类对象的成员

我们可以看到,我们对两个类的对象成员进行了不同的处理,结果也不同,证明继承下来的成员和父类并不是共用同一个

但是成员函数是同一个

这是因为我们的成员函数本身就不存在类中,而是存在一个公共的区域,就好比一个小区

我们可以建一个公共的篮球场,何必每家每户都建一个?

隐藏

我们在写继承的时候,可能会遇到这样一个问题:如果子类和父类成员重名了怎么办?会报错吗?

答案是不会报错,这会构成一个隐藏关系,如下:

<code>class person

{

public:

void print()

{

cout << _num << endl;

}

int _num = 0;

};

class student : public person

{

public:

int _id = 3;

int _num = 3;

};

int main()

{

student s;

cout << s._num << endl;

return 0;

}

我们可以看到,这两个重名的成员可以同时存在,但如果我们要访问这个变量的话,我们就只能访问到子类中定义的那个变量

那如果我们非要访问父类中的呢?

那我们就只能借助域作用限定符——指定类域(指定了我就要访问父类的成员)

<code>int main()

{

student s;

// 子类

cout << s._num << endl;

// 父类

cout << s.person::_num << endl;

return 0;

}

说完了成员变量,我们来说一说成员函数

<code>class person

{

public:

void print()

{

cout << _num << endl;

}

int _num = 0;

};

class student : public person

{

public:

void print(int i)

{

cout << _id << endl;

}

int _id = 3;

};

int main()

{

student s;

s.print(1);// √

s.print();// ×

return 0;

}

因为隐藏了,所以我们在子类和父类中相同的函数是只会去找子类的那个并匹配的

如果我们传的参数或其他不符合子类的条件,也不会去匹配父类的函数,而是会直接报错

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

首先我们需要知道的一点是,我们的子类(派生类)是可以给父类(基类)赋值的

这是因为,我们子类中本来就有一部分是和父类相同的,我们继承了父类,然后我们还有属于自己这个类里面的信息

比如:骑手、商家、客户都有相同的信息,比如名字、地址、电话等等,这些都是相同的

但是像用户,有骑手是否接单、评价这些成员是独属于用户这个类的,所以相比于父类,用户这个子类又多出了一些他自己的东西

而编译器在赋值转换这里其实是走了一个特殊的,也就是为这个语法开了绿灯

规定,子类给父类赋值,会将子类身上父类的那一部分赋值过去

这时我们再想想:父类能不能给子类赋值?

答案是否定的,因为你想啊,子类有一些成员变量父类都是没有的,那父类如果能给子类赋值的话,那些值不就成随机值了,这不就是一个大坑嘛

继承中的作用域

首先我们来总结一下,目前我们所知道的一共有几个域:

局部域全局域类域命名空间域

这四个都有一个共同的特点,就是都能影响查找规则,我要找一个变量,必须先去局部域找,然后再到全局域找,如果找不到就报错

这时如果我们将命名空间域展开或者指定命名空间域的话,我们就能够进入命名空间域中去查找

如果我们定义了一个类的话,那我们就能够通过这个类实例化出的对象来找类里面的公有函数

如果我就在这个类里面,那么这个类里面的变量我就都可以用

那么今天我们再来学习一下,继承中的作用域

首先,子类和父类是两块不同的空间域,如果子类里面有和父类一样的函数的话,就不会去父类的空间里面去找,这叫做隐藏,有些地方也叫做重定义

我们可以想象一下局部域和全局域,如果我们在局部域里面就有了一个变量的话,全局域也有一个变量,那我们是不是就只会拿局部域里面的变量啊,除非指定类域

并且!!!如果子类和父类有相同名字的函数话,不是重载!!!!!

要想清楚,子类和父类都不是一个类,而函数重载的要求就是,两个函数必须在同一个域里面,然后参数不一样函数名一样,这才叫函数重载

派生类的默认成员函数

这里的内容是,如果我们在继承了父类之后,可能有些情况下我们是需要显示写默认成员函数的

接下来我们会一个一个讲解:

构造拷贝构造赋值重载析构

至于剩下两个取地址和const取地址,这俩直接使用编译器默认生成的即可,知道有这么个东西就行,不需要我们学习

我们这里先写好一个父类,到时候下面的类都继承的这个父类

<code>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; // 姓名

};

构造

首先,我们要知道的是,父类是一个类,那么类就会有构造,而且编译器为了防止我们乱搞,就规定了我们在子类构造函数这里,必须使用父类的构造函数

所以我们就是直接使用父类的构造,然后再一个一个初始化子类成员

子类的构造:(不显示写的情况下)

父类那一部分:调用父类的构造

内置类型:默认不做处理

自定义类型:编译器会调用其构造

假设我们子类写一个student,如下:

class Student : public Person

{

public:

private:

int _id;

};

这时我们的构造就是:

class Student : public Person

{

public:

Student(const char* name = "Peter", int id = 0)

:Person(name)

,_id(id)

{}

private:

int _id;

};

拷贝构造

子类的拷贝构造:(不显示写的情况下)

父类那一部分:调用父类的拷贝构造

内置类型:默认不做处理

自定义类型:浅拷贝

但是,如果我们要显示写的话,那我们就需要一个一个处理了

首先,内置类型和自定义类型就和之前一样,直接拷贝即可(如果要深拷贝的话就开空间)

至于父类,还记得我们上面讲过的子类能赋值给父类吗

子类赋值父类就是将子类的父类那一部分直接赋值过去,这也叫切割

所以我们的拷贝构造可以直接传一个子类对象过来,父类那一部分就靠子类切割赋值来拷贝,剩下的就和之前一样

class Student : public Person

{

public:

// 构造

Student(const char* name = "Peter", int id = 0)

:Person(name)

,_id(id)

{}

// 拷贝构造

Student(const Student& s)

:Person(s)

,_id(s._id)

{}

private:

int _id;

};

赋值重载

子类的赋值重载:(不显示写的情况下)

父类那一部分:浅拷贝

内置类型:默认不做处理

自定义类型:浅拷贝

这个赋值重载和上面的构造、拷贝构造大体逻辑一样,显示写的话都是以调用父类的赋值重载为主,剩下的就是老老实实拷贝了,就是正常类的拷贝了

我们先来看看这么写:

// 赋值重载

Student& operator=(Student& s)

{

// 确保不是我赋值给我自己

if (this != &s)

{

operator=(s);

_id = s._id;

}

return *this;

}

这么一看我们写的好像是对的,运行之后发现,编译器崩溃了

我们会看到这里显示栈溢出了

其实回想一下我们上面一直在说的隐藏,两个类的赋值重载函数是不是都叫做operator=啊

那么子类的就会将父类的隐藏,所以就会不断调用子类的这个赋值重载,所以,就栈溢出了

正确的解决方法应该是指定类域,我要访问的就是父类的operator=

如下:

<code>// 赋值重载

Student& operator=(Student& s)

{

// 确保不是我赋值给我自己

if (this != &s)

{

Person::operator=(s);

_id = s._id;

}

return *this;

}

析构

析构这里就有一个大坑了,因为我们这里的析构是不能显示调用父类的析构的

我们开的函数会存到栈里面,栈要符合先进先出,也就是,我先创建的父类,再是子类,那我就应该先析构子类,再析构父类:

我们先说构造吧,为什么要先父后子:

如果我构造的时候,子类有一部分成员要靠父类的成员来初始化呢?那如果此时父类成员还没初始化,就是随机值,我们拿随机值初始化就完蛋了

再来说说为什么析构要先子后父:

如果我在析构完父类的成员之后,我子类还会调用父类成员变量或成员函数呢?

而且父类也没法调到子类的,因为是继承,子类继承父类

但是子类可以在父类析构之后调用父类啊,所以我们需要先析构子类,再析构父类

那为什么构造的时候要我们显示调用,析构的时候就不行呢?

因为构造的时候,一定能保证先父后子

想想,我们构造的时候,用的是初始化列表,初始化列表的顺序就是声明顺序

而编译器在此时默认规定了,我们在继承的时候,就相当于已经声明了

所以无论你顺序怎么换,先初始化的永远是父类

但是析构不一样,我们没办法保证一定会先子后父,所以C++就规定了,不用显示写父类的析构,编译器会在结束后自动调用,我们只需要析构子类的就行了

<code>~Student()

{

cout<<"~Student()" <<endl;

}

继承与友元

这个部分,只需要一句话就能解释清楚:友元不能继承

我们可以拿生活中的场景举例子:你爸有一个朋友,那那个叔叔是你的朋友吗?不是吧

所以我们父类的友元,并不能继承给子类

同样的,我们的子类的友元,父类同样不能使用(这个更离谱,子类继承的父类,父类连子类成员都用不了,更别提友元了)

/ 友元关系不能继承 父的友元子不能用 子友元不能用父的成员

class Person

{

friend class Show;

protected:

int _num = 0;

};

class Student : public Person

{

private:

int _id = 3;

};

class Show

{

Show(Student& s, Person& p)

{

cout << s._id << endl;

cout << p._num << endl;

}

};

继承与静态成员

这个地方也只有一个内容就是:静态成员是属于整个继承的,也就是说,都是同一个

举个例子:父类里面定义了一个静态成员变量,子类继承下去了,那么子类里面的那个继承自父类的静态成员变量,和父类的都是同一个,也就是说,修改了一个,另一个会跟着修改

<code> 静态成员变量属于整个继承类

class Person

{

public:

static char x;

protected:

int _num = 0;

};

char Person::x = 'x';

class Student : public Person

{

private:

int _id = 3;

};

int main()

{

Student s;

Person p;

printf("%p\n", &Student::x);

printf("%p\n", &Person::x);

return 0;

}

我们这个代码写的是,取父类和子类中静态成员变量的地址,我们根据上图可以发现,地址相同,所以可以得知都是同一个

菱形继承及菱形虚拟继承

多继承

在了解菱形继承之前,我们需要了解一下,什么是多继承

我们可以举一个生活中的例子:助教,既可以是老师,也可以是学生对吧

就像是一些学校里的老教授会有一些研究生学员,这些学员有一部分会选择当新生的助教,会负责什么批改卷子之类的

那么这时,这个助教就有两个身份:

老师学生

这时候我们就引出了多继承了,如下图:

多继承的格式如下:

其实就是在原本的继承后面加一个逗号,然后再在后面接继承的类名

菱形继承

而菱形继承是基于多继承的基础的更复杂的继承

还是举上面的例子,助教可以同时是学生和老师,而这两个身份,都是一种职业,所以我们可以举出这么一个例子:

职业里面分为老师和学生,而助教可以同时是学生和老师

这时就会有一个问题,职业这个类的信息,已经各自存进老师和学生这两个类里面了,而我们的助教这个类,就会同时存两份职业这个类里面的内容

这就会造成代码冗余和二义性!!!

试想一下,如果职业这个类里面有一个name成员,这时老师和学生都会各自存一份name成员

如果我们再继承给助教这个类的话,再去调用name,那我应该调用老师这个父类的name,还是学生这个父类的name

编译器不知道,所以就会报错,这就是二义性

<code>class job

{

public:

int _name = 100;

};

class teacher : public job

{

};

class student : public job

{

};

class assistant : public student, public teacher

{

};

int main()

{

assistant a;

cout << a._name << endl;

return 0;

}

因为编译器不知道是要调用哪一个类的name,所以就报错了

菱形虚拟继承

这时,祖师爷就想了这么一个办法,就是菱形虚拟继承

具体就是,在接收的类那里,在继承方式前面加上一个virtual

加上这个之后,编译器就会将继承的类的内容放到一个公共的区域,到时候这个类再被继承的时候,如果是菱形继承这样子的情况,就不会继承多份了

<code>class job

{

public:

int _name = 100; // 姓名

};

class Student : virtual public job

{

protected:

int _num; //学号

};

class Teacher : virtual public job

{

protected:

int _id; // 职工编号

};

class Assistant : public Student, public Teacher

{

protected:

string _majorCourse; // 主修课程

};

int main()

{

Assistant a;

cout << a._name << endl;

return 0;

}

结语

到这里,我们有关继承的相关内容就讲完啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。