当前位置: 首页 > news >正文

C++面向对象之继承

        对于面向对象来说,是有三大特性,分别是:封装、继承、多态。在我们之前的学习中,我们已经初步了解了什么是封装。那么今天我们就来学习一下面向对象的第二大特性:继承。

1. 继承的概念

        继承机制是面向对象程序设计中实现代码复用的最重要手段。它允许程序员在保持原有类特性的基础上进行扩展,增加功能,从而产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

        简单来说,一个子类继承了一个父类,那么这个子类就拥有这个父类的所有公共成员,并且子类可以在此基础上进行更多的操作,这些操作并不会影响父类的成员。

        当我们创建了两个高度相似的类,但是这两个类的成员并不是一模一样的时候,我们就应该使用继承来减少我们的代码书写量。

        举个例子:以下一个学生类,一个老师类,我们可以发现他们的成员函数都存在一个eat函数,并且成员变量有三个都是相同的。那么这个情况下我们写两个类就会增大代码量,他们的大部分成员都是一样的。这个时候我们就应该使用继承的方法来对这个类进行优化。

class Student
{
public:void eat(){//......}void study(){//....}
private:string _name;string _phone;int _age;int _stuid;
};class Teacher
{
public:void eat(){//......}void teach(){//.....}
private:string _name;string _phone;int _age;int _teacherid;
};

        下面这段代码就是优化以后的结果,我们需要先建立一个Person类,把Student类和Teacher类共有的成员都写进去。然后再分别创建两个类继承这个Person类。那么继承的写法我们下面会讲,这里大家只需要先看出继承的好处就可以。

class Person
{
public:void eat(){//......}
private:string _name;string _phone;int _age;
};class Student : public Person
{
public:void study(){//....}
private:int _stuid;
};class Teacher : public Person
{
public:void teach(){//.....}
private:int _teacherid;
};

2. 继承的定义

2.1 定义的格式

        如图所示,这时我们刚才定义的一个Teacher类:

  • 其中Teacher为这个类的名称,也是子类 / 派生类
  • 冒号后面的public代表子类的继承方式
  • 而最后的Person就是父类 / 基类,也就是子类要继承哪个类。

        继承一共有三种继承,分别是:public、protected、private。这和类的访问限定符很相似。但是这个叫继承方法,而非访问限定符。父类中被不同访问限定符修饰的成员根据继承方式的不同,在子类中的权限也不同。

类成员/继承方法public继承protected继承private继承
父类的public成员子类的public成员子类的protected成员子类的private成员
父类的protected成员子类的protected成员子类的protected成员子类的private成员
父类的private成员在子类中不可见在子类中不可见在子类中不可见

        父类中private成员无论如何在子类中都是不可见的。但是这个不可见只是我们无法再子类中调用这个成员,继承的时候还是会正常继承过来。

        如果我们不想让一个成员在类外面被访问,但是又想让子类继承下来这个成员,那么我们应该用protected来修饰这个成员。用protected修饰的成员可以做到在类外不被访问并且可以正常的被子类继承。

        class 的默认继承方式是private , struct 默认的继承方式是public,不过最好还是显示的写出继承方式。此外,大家也要记得,class 的成员默认是被private修饰的,struct 的成员默认是被public修饰的。这两点大家可以一起来记。

        最后我们总结一下上面的表,我们可以发现,父类的私有成员都是不可见的。父类的其他成员在子类中的继承方式为 min(成员在父类中的访问限定符 , 继承方式) 。这里的大小关系为:public > protected > private 。不过在实际运用中一般都是用public继承,几乎很少用protetced / private 继承,因为protetced / private 继承下来的成员都只能在派生类的类里面使用,在实际中维护性不强。

2.2 继承类模板

        既然继承可以继承一个类,那么类模板也是可以继承的。先举个例子:

        在下面这段代码中,我们自己建立了一个stack类,继承了vector。但是要注意的是我们应该在继承的时候指明继承的类是处于哪一个类域的。当我们实例化stack<int>的时候也会同时实例化vector<int>。同样的,成员函数位于哪一个类域也要指明,类中是按需实例化的,所以如果我们没有指定继承的成员位于哪一个类域就进行调用,在函数模板中是不会进行实例化的,所以我们也就没法访问到被继承的成员。

template<class T>
class stack : public std::vector<T>
{void push(const T& x){vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}
};

3. 子类和父类之间的转换

        如果是子类进行的public继承,那么我们用public继承的子类对象是可以赋值给父类的引用或父类的赋值的。这里有一个形象的说法叫做切片或者切割,因为我们在赋值以后,父类的引用或父类的指针只会指向子类中那些父类继承过去的成员。

        这里的指向就是单纯的指向,这点我们在后面的继承的内存模型中再统一进行说明。

        相反,父类对象是不可以继承给子类对象的,因为子类的成员通常多于父类,所以当父类给子类赋值的时候,总会有几个多出来的成员不知道应该赋什么值。

4. 继承中的作用域

        我们在之前的学习中知道,在C++中类是拥有自己的类域的。继承的类也同样遵守这个规则,父类和子类都有自己独立的类域。那么既然是继承就无法避免出现相同名字的成员函数和成员变量,当子类和父类中出现同名的成员的时候,子类的成员就会屏蔽掉对父类成员的直接访问,这种情况称之为隐藏。隐藏并不是不可以访问,而是如果我们直接访问的话只会访问到子类中的成员,如果我们想访问父类的成员需要用 父类 :: 父类成员来进行访问。

        我们需要注意的是,如果是成员函数的隐藏,则只要成员函数名相同就构成隐藏,而不会构成重载。

class Student;
class Person
{
protected:string _name = "张三";int _num = 321; //⾝份证号
};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;    ////这里如果不指定Person域就会访问到Student的_num 也就是12345cout << " 身份证号:" << Person::_num << endl;   //321cout << " 学号:" << _num << endl; //123}
protected:int _num = 12345; //学号
};int main()
{Student s;s.Print();return 0;
}

5. 子类的默认成员函数

        在子类中同样也会有默认成员函数,也就是我们之前说的6个默认成员函数,分别是:默认构造函数、析构函数、拷贝构造函数、赋值重载、两个取地址重载。这里我们还是只讲相对来说比较重要的前四个函数。

        在正式介绍这几个函数之前,先说一下结论:绝大多数情况下我们是不需要实现子类中的默认成员函数的。除非子类中出现了新的需要向内存中申请空间的变量。

5.1 构造函数

        对于子类的构造函数来说,子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认构造函数,则必须在子类构造函数的初始化列表中显示调用。

        我们可以把一个子类的构造函数看作三个部分的构造:

  • 第一个部分是内置类型的初始化,这里和普通的类是差不多的,初始化成什么完全取决于编译器。
  • 第二个部分是自定义类型的初始化,这里会调用自定义类型的构造函数。
  • 第三个部分就是父类的初始化,在对父类的初始化中,我们会把所有的父类成员看做一个整体,然后去调用父类的默认构造。

        所以一般来说,如果父类有默认构造函数的话,我们是不需要自己实现子类的默认函数的。因为子类自己生成的默认构造就已经可以完成初始化的操作了。

        但是如果父类没有默认构造函数,我们就需要在子类中手动实现构造函数了。下图代码中, Person 类没有默认构造函数,所以我们只能在 Student 类中的构造函数中的初始化列表部分手动调用父类的构造函数,这里调用的方法也是固定的,用父类的名字来调用它的构造函数。这样我们就完成了子类的构造函数。

class Person
{
public:Person(const char* name):_name(name){}
private:string _name;
};class Student : public Person
{
public:Student(const char* name, int num):Person(_name),_num(num){}
private:int _num;
};

5.2 拷贝构造

        至于说拷贝构造,大家也是知道的,还是一个构造函数,所以拷贝构造和和构造函数也差不多,也是要分为三个部分:内置类型的初始化、自定义类型的初始化以及父类类型的初始化。

        默认生成的拷贝构造函数也跟默认生成的构造函数类似,所以一般来说,我们也不需要自己手动实现拷贝构造。除非子类成员出现了新的需要申请空间的成员。

        下面我们来显示的写一下子类的拷贝构造,我们只需要在子类的拷贝构造中的初始化列表部分像构造函数一样初始化所有成员变量。其中对父类的拷贝构造要调用它自己的拷贝构造。要注意,我们在调用父类的拷贝构造的时候需要传递父类类型的参数,我们这里只需要传递子类类型的参数就可以了,前面我们提到过,子类类型传递给父类类型的时候会对子类类型进行切割。不过要注意的是,在初始化列表中的初始化顺序是按照成员的出现顺序来进行初始化的,也就是我们会最先初始化父类,然后才会依次初始化成员变量,所以在写的时候最好还是保持一致,这样不会出现歧义。

class Person
{
public:Person(const Person& p): _name(p._name){}
protected:string _name;
};class Student : public Person
{
public:Student(const Student& s):Person(s),_num(s._num),_addrss(s._addrss){// 深拷贝实现}protected:int _num;string _address;//出现了像这样需要申请空间的成员的时候才需要自己显示写拷贝构造//int* _ptr = new int[10];
};

         此外,子类的拷贝构造中的初始化列表里面对父类类型的调用不能省略,因为子类的拷贝构造一定会调用父类的拷贝构造。如果父类没有拷贝构造就会去调用构造函数,这样就不能完成对子类的拷贝构造了。

5.3 赋值重载

        赋值重载和拷贝构造跟拷贝构造基本上是一模一样,同样也是看做三个部分:内置类型的赋值重载、自定义类型的赋值重载、父类类型的赋值重载。自定义类型的赋值重载会调用它的赋值重载,父类类型的赋值重载会调用父类类型的赋值重载。同样,我们不需要显示写出来,默认生成的赋值重载就可以实现。

        当我们显示写赋值重载的时候,要注意在调用父类的赋值重载的时候,要在父类的赋值重载前面加上父类的类域。因为在父类和子类的同名函数会构成函数隐藏,如果我们不指名类域,函数就会一直调用自己,形成死循环。

class Person
{
public:Person& operator=(const Person& p){if (this != &p)_name = p._name;return *this;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student& operator=(const Student& s){if (this != &s){// 父类和子类的operator=构成隐藏关系//一定要指明作用域Person::operator=(s);_num = s._num;_addrss = s._addrss;}return *this;}
protected:int _num;string _addrss;
};

5.4 析构函数

         析构函数和前面的函数也差不多,我们不需要管自定义类型和内置类型的析构,他们会自己析构掉。但是唯一的差别是,我们这里也不需要显示调用父类的析构。因为类的析构顺序是:后定义的先析构。在继承中,子类一定是在父类后定义的,所以子类的析构函数在析构后会自动调用父类的析构函数。同样的,我们在子类的析构函数中,只要没有出现申请空间的变量就不需要自己实现析构函数。

6. 继承和友元

        友元关系是不可以被继承的,父类的友元是不可以访问子类的私有和受保护成员的。如果我们想让在父类的友元函数也可以访问到子类的私有和受保护成员,我们只需要在子类中添加进友元函数就可以了。

//这里要事先做一个声明 
//告诉编译器是有Student这个类的
//不然编译器会报错
class Student;
class Person
{
public:friend void func(const Person& p, const Student& s);
protected:string _name;
};
class Student : public Person
{
public://如果没有这句友元那么下面的func函数就无法访问到 _stuNumfriend void func(const Person& p, const Student& s);
protected:int _stuNum;
};
void func(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}int main()
{Student s;Person p;func(p , s);return 0;
}

7. 继承的static成员

        如果我们在父类中定义了一个 static 成员,那么在所有的继承体系中,只会存在这一个成员。无论出现多少个子类,都只会有这一个static成员。

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

8. 多继承

       一个子类只继承一个父类叫做单继承,而一个子类可以同时继承多个父类,我们称之为多继承。

8.1 继承的内存模型

        这是一个单继承的内存模型,我们可以看到,在内存中,所有父类的成员都在子类的最上面,也就是子类会先继承所有的父类成员。同时,如果父类中有私有的成员,子类也会进行继承,但是不会给这些成员访问的权限。

        我们上面提到的子类和父类的切片或者切割,说是子类的对象可以赋值给父类的指针或引用,那么父类的指针或引用指向的地方也就是子类的开始。大小就是父类的大小。

        多继承也是类似的,代码对应的内存模型如下。只不过我们要记得子类中继承的顺序是和内存中相关联的。下面例子中我们先继承的Student 类 ,再继承的 Teacher 类,所以在内存中Student 类就会在 Assistant 类的最开始的地方。反之,如果我们先继承的是Teacher 类,那么在内存模型中就会变成 Teacher 类在最开始的地方。

class Student
{
protected:string _name;int _num;
};class Teacher
{
protected:string _name;int _id;
};class Assistant : public Student, public Teacher
{
private:int _assistantNum;
};

8.2 菱形继承和虚继承

        在上面的多继承模型中,我们可以看到,Student 类和 Teacher 类还是有一个相同的成员,那么如果我们让这个成员分离出一个Person类,Student 类和 Teacher 类再继承这个 Person 类,Assistant 类再继承 Student 类和 Teacher 类 ,这样就变成了一个菱形继承。

        上面两个图所示就是菱形继承,在 Assistant 类中会有两份 Person 类的成员,会导致数据冗余和二义性。也就是我们单独访问 Assistant 类中的 _name 变量时,编译器不知道要访问Student 类的 _name 还是 Teacher 类中的 _name。

        我们可以通过访问 Teacher 类中的 _name 或 Student 类中的 _name 来解决我们无法访问 Assistant 类中的 _name 的问题,但是还是存在数据冗余的问题。

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
{
private:int _assistantNum;
};int main()
{Assistant a;//这样是访问不了 _name 的,因为 Assistant中有两个 _name//a._name;//通过这样的方式我们才能访问 _namecout << a.Teacher::_name << endl;cout << a.Student::_name << endl;return 0;
}

        如果我们想让 Assistant 类中的 _name 只存在一份,我们就应该对继承了 Person 的两个类进行虚继承,这样我们在菱形继承的时候就不会出现两份数据。在我们对 Student 类和 Teacher 类使用虚继承以后, Student 类和 Teacher 类不受影响,Assistant 类多继承两个类后就只会存在一份相同的数据了。

        虚继承的关键字是 virtual ,演示如下:

class Person{
public:string _name = "张三";
};
class Student : virtual public Person
{
protected:int _num;
};class Teacher : virtual public Person
{
protected:int _id;
};class Assistant : public Student, public Teacher
{
private:int _assistantNum;
};int main()
{Assistant a;//使用虚继承以后 Assistant 类中就只会有一个 _name//我们就可以对其直接进行访问了    a._name;return 0;
}

        多继承是非常好用的,但是菱形虚拟继承就非常麻烦了。无论是使用还是底层都会变得十分复杂。在平时使用的时候还是不要使用菱形继承。

9. 继承和组合

        public 继承是一种 is-a 的关系,就是每个子类对象都是一个父类对象。

        而组合则是一种 has-a 的关系,假设B组合了A,每个B对象中都会有一个A对象。

        继承允许我们根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被定义为白箱复用。在继承方式中,父类的内部细节是对子类可见的,继承在一定程度上其实破坏了父类的封装,如果父类进行改变,对子类的影响是非常大的。父类和子类之间的依赖关系很强,耦合性高。

        而对象组合则是类继承以外的另一种选择。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用。因为我们不可见内部的细节,只可以调用提供的接口,只要接口不改变,我们就不用修改。这样组合类之间没有很强的依赖关系,耦合性低。

        所以总的来说,还是应该优先使用组合,而不是继承。组合的代码维护性更好,耦合性也更低。不过也没有这么绝对,如果继承更合适还是要使用继承。在我们后面提到的多态,也是要使用到继承的。


文章转载自:

http://bNkRKjhK.dmrjx.cn
http://IeW7Rkko.dmrjx.cn
http://GuEgDlm5.dmrjx.cn
http://GRiQEee1.dmrjx.cn
http://X43QA40g.dmrjx.cn
http://OPPLWKdm.dmrjx.cn
http://nSLIvSUL.dmrjx.cn
http://T0Ht5fzM.dmrjx.cn
http://fMUxzdn6.dmrjx.cn
http://bSygIM1H.dmrjx.cn
http://1vIRM8wz.dmrjx.cn
http://JSWs3odS.dmrjx.cn
http://3CdBr0fc.dmrjx.cn
http://kGU9LbyN.dmrjx.cn
http://ga8sBY3S.dmrjx.cn
http://0UPpypNb.dmrjx.cn
http://4Ob8An7L.dmrjx.cn
http://5jJTrYP1.dmrjx.cn
http://FmAGE6VN.dmrjx.cn
http://X67VWe2g.dmrjx.cn
http://n5Mm3vs8.dmrjx.cn
http://VKYsEC5k.dmrjx.cn
http://hPJhtjbl.dmrjx.cn
http://LdI8wxIl.dmrjx.cn
http://2uFrT1iv.dmrjx.cn
http://4kSstGYu.dmrjx.cn
http://LvhxJ1NT.dmrjx.cn
http://iYeE5Elv.dmrjx.cn
http://0e4J8OUC.dmrjx.cn
http://1CjSg1FG.dmrjx.cn
http://www.dtcms.com/a/375503.html

相关文章:

  • AI原生编程:智能系统自动扩展术
  • Wireshark TS | 接收数据超出接收窗口
  • 第一代:嵌入式本地状态(Flink 1.x)
  • 4.1-中间件之Redis
  • Django ModelForm:快速构建数据库表单
  • 【迭代】:本地高性能c++对话系统e2e_voice
  • SSE与Websocket、Http的关系
  • 蓓韵安禧DHA展现温和配方的藻油与鱼油营养特色
  • 基于UNet的视网膜血管分割系统
  • python函数和面向对象
  • 嵌入式 - ARM(3)从基础调用到 C / 汇编互调
  • 07MySQL存储引擎与索引优化
  • 面向OS bug的TypeState分析
  • 【文献笔记】Task allocation for multi-AUV system: A review
  • 小红书批量作图软件推荐运营大管家小红书批量作图工具
  • ArrayList详解与实际应用
  • 德意志飞机公司与DLR合作完成D328 UpLift演示机地面振动测试
  • MongoDB 备份与恢复终极指南:mongodump 和 mongorestore 深度实战
  • ctfshow - web - 命令执行漏洞总结(二)
  • 基于STM32的GPS北斗定位系统
  • 2025年大陆12寸晶圆厂一览
  • VMware Workstation Pro 安装教程
  • Java Spring @Retention三种保留策略
  • 低代码平台的核心组件与功能解析:红迅低代码平台实战探秘
  • linux sudo权限
  • PM2 管理后端(设置项目自启动)
  • 中国香港服务器中常提到的双向/全程CN2是什么意思?
  • DCS+PLC协同优化:基于MQTT的分布式控制系统能效提升案例
  • Backend
  • 分布式专题——6 Redis缓存设计与性能优化