欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > C++之“继承”

C++之“继承”

2025/5/9 12:57:22 来源:https://blog.csdn.net/2402_83250773/article/details/147771086  浏览:    关键词:C++之“继承”

 继续开始关于C++相关的内容。C++作为面向对象的语言,有三大特性:封装,继承,多态。

这篇文章我们开始学习:继承。

一、继承的概念和定义

1. 继承的概念

什么是继承呢?

字面意思理解来看:继承就是获得某个人的所有遗产。在这里我们可以理解为:一个类继承了父类的成员变量以及成员函数。

接下来是准确的介绍:继承机制是面向对象程序设计使代码可以复用的一个重要手段,它允许我们在原来的类上进行扩展,增加新的属性(成员变量)和方法(成员方法),从而生成一个新的类,称之为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的是函数层次的复用,而继承就是类设计层次的复用。【父类/基类:这个类里面放的是共有的属性和方法。派生类/子类:可以看作是在父类上的扩展】

以前我们都是函数层面的复用(在一个函数里调用另一个函数),现在是类设计层面的复用。接下来用例子进一步说明什么是继承:

红色圈住的部分是两个类一模一样的部分,它们的成员函数和成员变量都有很多相似,看起来比较冗余,对于两个类我们可以采取继承的方法:将这两个类里面相同的(重复的)成员提取出来,放到父类,再将不同的成员(各自的成员)分别放在各自的类中不动,紧接着让派生类继承父类。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
class common  //共有成员
{
public:void identity(){std::cout << "void identity()" << std::endl;}
protected:std::string _name;std::string _address;std::string _tel;
private:int _age;
};class student :public common   //学生的那个类 
{
public:void study(){}
protected:int _stuid;
};class teacher :public common   //老师的类
{
public:void teaching(){}
protected:int _title;};
int main()
{//实例化对象student s;teacher t;s.identity(); t.identity();return 0;
}

公共的成员都放到common类中,student和teacher都继承common,就可以复用这些成员,不需要重复定义,省去了很多麻烦。

2. 继承的定义

定义格式

  class student    :public      common
//class 派生类/子类:继承方式 父类/基类

继承方式

从图中可以看出:共有三种继承方式,这三个关键字,既可以在类里取修饰成员变量和函数,还可以在此处继承。它既是继承方式,也是访问限定符。之前了解到的是:使用public(公有的),类里类外都可以访问,private(私有的),只能在类里面使用,类外面不可以。在这里继续了解protected:使派生类可以访问到基类的成员变量/成员方法

分析:
1> 基类的private成员在派生类中无论以什么样的方式去继承,它们在派生类中其实都是不可见的。这里的不可见指的是基类的私有成员它还是被继承到了派生类的对象中,但是在语法上限制了派生类对象(用private限制的基类的成员,它们是存在的,只是我们不可见),不管是在派生类里面还是在派生类外面都不可以去访问基类中用private访问限定符限定的成员的,只可以在基类中自己使用。

第一种

第二种:

2>如果想让基类成员在类外面不能被访问,但是可以在派生类中被访问,可以将这个成员的方位限定符设为protected。(从这里就可以看出来protected(保护限定符)其实就是为了继承才设计出来的。)

3> 基类中的私有成员在子类(派生类)或者类外面是绝对无法被访问的,那基类的其他成员在派生类中的访问方式是什么呢?是:Min(成员在基类的访问限定符,继承方式)(public >protected> private)

举例:在上图中,基类中的成员变量_age是被protected访问限定符限制的,派生类在这里是以public继承方式去继承基类的,public和protected中protected权限最小,所以_age就相当于是被访问限定符限制protected限定在派生类中

4>使用关键词class时默认的继承方式是private,使用struct关键字是默认的继承方式是public,不过最好还是显示的写出继承方式,以增加代码的可读性。

5>我们在实际运用中一般都是用public的继承方式去继承的,几乎很少看见使用protected / private这两种继承方式去继承的,因为protected / private继承下来的成员都只能在派生类里面去使用,实际中扩展维护性也不强。

3.继承类模板

在这里我们尝试通过继承一个vector类模板来实现栈这个结构

将stack作为vector的派生类来生成的,我们在模拟实现stack的成员方法时,是直接通过复用基类的成员方法来进行实现的。

注意点:

1. 继承的时候,不止要写类名vector,还要写模板参数<T>.

namespace hou
{template<class T>    //这个是模板参数class stack :public vector<T>//类名是vector,模板参数是T{};
}

2. 基类是类模板时,需要指定一下类域

namespace hou
{template<class T>    //这个是模板参数class stack :public vector<T>//类名是vector,模板参数是T{public:void push(const T& x){vector<T>::push_back(x);  //不能直接写push_back(x);// 基类是类模板时,需要指定⼀下类域, 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了(但需要注意的是,这里只是实例化了stack和vector这两个模板中的默认构造函数,其余成员函数均未进行实例化操作)//模版是按需实例化,push_back等成员函数未实例化,所以找不到 }};
}

我们每次复用基类的成员方法时,我们都需要使用作用域运算符来指定类域,虽然按道理来说,我们实例化出来派生类了,基类应该也就实例化出来了,事实确实如此,但是基类并不是一把实例化出来的,他的有些成员还没实例化出来,这时候我们就需要自己手动指示类域进行实例化了(如果我们不指明的话,编译器就会报未声明的报错),这就是我们之前所说的按需实例化

#include<stdio.h>
#include<iostream>
#include<vector>
using namespace std;namespace hou
{template<class T>    //这个是模板参数class stack :public vector<T>{public:void push(const T& x){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();}};
}int main()
{hou::stack<int> st;st.push(1);st.push(2);while (!st.empty()){std::cout << st.top() << " ";st.pop();}return 0;
}

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

public继承的派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫做切片或者是切割。寓意把派生类中基类的那一部分切割出来,基类指针或引用指向的是派生类中切出来的基类那部分

再回顾一下之前的类型转换:

double d=1.1;
const int& i = d;

这里不可以int& i = d;因为d的整型部分先赋给临时变量(临时变量有常性,意思是用const修饰了,所以将临时变量给给i,i必须是const修饰的int类型的对象,代码第二行)


1. 派生类(子类)对象可以赋值给(基类)父类的对象,调用父类中的拷贝构造函数即可实现。

student sobj;   //student是子类
person p = sobj;  //person是父类

 

2. 派生类(子类)对象可以赋值给基类(父类)指针,就相当于是把子类中父类的那部分切割出来,pp指针就指向切割出来的那部分的地址

person* pp = &sobj;

3.派生类(子类)对象可以赋值给基类(父类)引用。(把子类中父类的那一部分切割出来,p它所引用的就是从子类中切割出来的那一部分)

在图片中,红线框里并没有写const,但是没有报错,这说明中间没有产生临时变量。这里是编译器的特殊处理:赋值兼容转换。

(1)父类对象不能赋值给子类对象,会编译报错(少赋值给多?x)

三、继承中的作用域

在C++中,基类与派生类是两个不同的作用域(派生类是由基类继承下来的,但它们两个的作用域不是同一个,是独立的两个作用域)

隐藏规则:

1. 如果派生类和基类中的成员同名,那派生类就会屏蔽基类的同名成员,这叫做隐藏。但如果想在派生类中使用(和派生类同名的成员),也是可以的,直接指明父类的类域即可(基类::基类成员)

2. 对于成员函数,只要两者的函数名相同,那么两者就会构造隐藏关系【意思是:如果两个函数名相同,但是参数不同(void fun()和void fun(int i)),这两个在继承的情况下是隐藏关系。】

还需要回顾一下:什么是重载(需要满足的一个条件是:这两个函数在同一个作用域)【函数重载是指在一个作用域内,允许多个同名函数的存在,只要它们的形式参数(包括参数的数量、类型或顺序)有所不同即可。这种机制使得同一个函数名称能够执行不同的操作。】

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

默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?(派生类本质也是一种类,只不过它与基类有着千丝万缕的关系)

派生类的构造函数

普通的类:在默认生成的时候:成员变量分为:内置类型(如果我们不写,它不确定,有可能随机值,也有可能初始化为0;如果有缺省值,就会用缺省值)和自定义类型(会调用这个自定义类型的默认构造,如果自定义类型没有默认构造,需要显式去写它的构造,在初始化列表定义初始化自定义类型)

1. 对派生类进行构造时,要记得,派生类有自己独有的成员,还有在父类里边儿共有的成员,(我们可以将子类中继承下来的父类成员,当作一个整体对象)默认构造:派生类独有的(内置和自定义)+父类成员(调用自己的默认构造)(如果父类没有默认构造,就需要自己显示地去写)

(派生类的构造函数)必须(调用基类的构造函数初始化基类那一部分的成员),如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

之前普通的类的情况:初始化列表中,按照声明的顺序进行初始化的,在这里是先初始化父类的。

派生类对象 初始化时,先调用基类构造,再调用派生类构造函数。(和析构相反)

拷贝构造函数

1. 如果不写拷贝构造,编译器默认生成的是什么样子的?

子类成员内置类型[默认拷贝构造的话是值拷贝]+自定义类型[默认构造的话会调用这个类型的拷贝构造])+父类成员(必须调用父类的拷贝构造)

默认拷贝构造完成的挺好,不需要我们插手。

那什么时候,派生类的拷贝构造需要自己写--->假设一个成员需要深拷贝

int* ptr = new int[10];

(指针是内置类型)内置类型的默认拷贝构造(浅拷贝)会造成两个指针指向同一个空间,析构的时候会崩溃。当需要深拷贝的时候,就需要自己写拷贝构造了。

接下来看,如何显式地写拷贝构造:

//父类中的拷贝构造  Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}   //子类中的拷贝构造 Student(const Student& s): Person(s), _num(s._num),_address(s._address){cout << "Student(const Student& s)" << endl;}

派生类对象的拷贝构造函数,必须调用基类的拷贝构造来完成拷贝初始化。

赋值重载

如果没有需要深拷贝的,就不用自己写了

int main()
{Student s1( 18,"202306","hou");Student s3(99, "39393", "jing");s1 = s3;return 0;
}
//父类Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)  //如果是自己给自己赋值的话,就直接返回自己就可以了,有效地节省了时间,大大地提高了运行效率_name = p._name;return *this;}    //派生类Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s) {// 构成隐藏,所以需要显⽰调⽤Person::operator =(s);   //调用父类的operator=()函数去给父类中的那一部分成员变量进行赋值操作_num = s._num;_address = s._address;}return *this;}

派生类的operator=()函数隐藏了基类的operator=()函数,所以需要我们去显示调用基类的operator=()函数,就必须要去指定基类的作用域,只有这样才能访问基类中的那个operator=()赋值构造函数

析构函数

析构时,必须确保先析构子,再析构父

派生类的析构函数会在被调用完成后,自动去调用基类的析构函数去清理基类成员【所以,不需要显示调用父类析构】。因为只有这样才能保证派生类对象(先清理派生类成员),(再去清理基类成员的顺序。

~student()
{_num = 0;_address=nullptr;person::~person();//我们在去调用父类的析构函数去清理父类中的成员变量时,我们一定要在这里指定作用域,因为析构函数在最后它都会被处理成desructor()这个函数,进而会构成隐藏。
}//我们在显示调用父类的析构函数时,一定要注意要等到将子类中的成员变量全部清理完了以后,再去调用父类的析构函数去清理父类中的那一部分成员变量,通过我们前面所学过的知识可知,先构造的后析构,由于person是被继承过来的,就相当于是先有person,再有student,因此后析构person(也就是person)。

因为多态中一些场景导致析构函数需要重写,而重写的条件之一就是函数名相同(这个写一篇博客会讲到,这里我们只需要明白析构函数会被进行特殊处理即可),那么编译器在这里会对析构函数进行一个特殊处理的操作,将析构函数名处理成destructor(),所以基类的析构函数在不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

实现一个不能被继承的类

方法一:将构造函数私有化(C++98)

为什么将构造函数私有化就不能被继承了呢?

派生类的构造必须调用基类的构造函数,但是基类的构造函数被私有化private,派生类看不见就自然不能去调用了,进而就无法实例化出对象了。

方法二:final关键字

C++11中增加了一个final关键字,final修改了基类后,派生类就不可以继承基类了。

继承与友元

友元关系不能被继承

回顾:通过使用friend关键字,可以让(一个类或函数)访问(另一个类)中的私有成员(私有成员变量和私有成员函数)和保护成员。Display是父类的友元,那么在Display这个函数里,可以访问父类里的私有和保护成员。

友元关系不能被继承,简单理解:父类的朋友,并不是,子类的朋友。所以,在Display中,并不能访问子类的私有和保护成员。

继承与静态成员

这个地址不一样说明:非静态成员num的地址是不一样的,派生类继承了这个num变量,父类和派生类各有一份。

那静态成员呢?

回顾:

  • 静态成员变量是属于整个类的,而不是某个具体对象的属性。静态成员变量只有一份副本存在于内存中。
  • 声明方式: 在类内部使用 static 关键字声明静态成员变量。
  • 初始化位置: 静态成员变量必须在类外部进行初始化

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

到静态成员 _one 的地址是⼀样的 ,说明派生类和基类共用同一份静态成员。

公有的情况下,父类和派生类,指定类域都可以访问静态成员

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

首先我们先搞清楚,什么是单继承和多继承。

单继承: 一个派生类只有一个直接基类时,称这个继承关系为单继承

在这个图片中,它们的关系是单继承。PostGraduate只是间接继承了Person,PostGraduate的直接基类只有一个,就是Student。

 一个派生类有两个及以上直接基类时,称这个继承关系为多继承

多继承对象在内存中的模型是:先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面。

比如:Assistant这个类同时继承了一个Student类和Teacher类,继承的父类之间分别用","隔开,Assistant这个类先继承的是Student类,后继承的是Teacher类。

class Assistant :public Student, public Teacher

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

多继承很容易造成菱形继承。(如果这两个基类Student和Teacher,都有一个共同的基类,就会形成菱形继承)

菱形继承有不太好的方面:菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份,这样的话,会大大增加空间的消耗。

怎么看出来有两份呢?

从图中可以看出,派生类Student和Teacher当中,都有基类Person。有两份Person就会导致数据冗余(有两份,浪费空间)和二义性(有两份Person,不知道想访问哪一个)

暂时解决二义性问题的办法:指明基类

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;
}

虚继承

C++中的多继承虽然说在实际中可能应用挺广泛的,但是它应运而生的菱形继承所带来的问题有点多,这也是java中没有多继承的一个主要原因,但是C++对于之前已经创立的语法是不可以进行修改的,我们只能够向前兼容,于是我们就又创建了一个新的语法——虚继承。

虚继承就是在我们派生类继承基类的时候,在继承方式前加上一个关键字virtual即可。在C++中,虚继承(Virtual Inheritance)是一种用于解决多继承中重复继承问题的机制。其主要意义在于确保共同的基类只被继承一次,避免数据冗余和访问歧义。

class Teacher :virtual public Person//teacher类虚继承了person类

c++的语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有了虚拟菱形继承,底层实现就会很复杂,性能方面就也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言中基本上都没有多继承。

组合和继承

  1. public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  3. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为⽩箱复用(这里既然说到了这个白盒测试和黑盒测试,我们先来简单地了解一下相关的知识:1 黑盒测试:不了解底层实现,是从功能的角度进行测试的,2 白盒测试(相对于黑盒测试更难):了解底层实现(代码实现),从代码运行的逻辑角度进行测试)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很⼤的影响。派生类和基类间的依赖关系很强,耦合度⾼。
  4. 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复用⻛格被称为⿊箱复用(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  5. 优先使用组合,⽽不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词