【C++】——继承与虚继承

devtools/2024/9/19 12:43:49/ 标签: c++, 开发语言

文章目录

  • 继承
    • 继承的概念
    • 继承的定义
    • 继承类模版
    • 基类与派生类的赋值转换
    • 继承的作用域
    • 派生类的默认成员函数
      • 构造函数与析构函数
      • 拷贝构造
    • 不能被继承的类
    • 继承与友元
    • 继承与静态成员
    • 多继承与菱形继承
  • 虚继承
  • 继承与组合

继承

什么是继承?

继承其实就是胆码复用的一种手段,它允许我们定义一个类来继承另一个类的属性和方法。

非继承:

#define   _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<string>
using namespace std;
class Teacher    //教师类
{
public:void print(){cout << _name << endl;cout << _age << endl;}
private:string _name = "zhangsan";    //名字int _age = 20;   //年龄string _num;   //工号
};
class student
{
public:void print(){cout << _name << endl;cout << _age << endl;}
private:string _name = "zhangsan";    //名字int _age = 20;   //年龄string _id;   //学号
};
int main()
{Teacher t;student s;t.print();s.print();return 0;
}

继承:

class Person
{
public:void Print(){cout << "名字:" << _name << endl;cout << "年龄:" << _age << endl;}
protected:string _name = "renqing"; // 姓名int _age = 20;  //年龄
};class Student : public Person
{
public:void playgame(){// ...}
private:int _id; // 学号
};

继承的概念

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

继承的定义

下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)
在这里插入图片描述
在这里插入图片描述
继承基类成员访问⽅式的变化
在这里插入图片描述
基类的private成员只是不可访问,但是派生类还是得到了继承
在这里插入图片描述

public和protected的区别

class Base {  
public:  void publicMethod() {}  protected:  void protectedMethod() {}  
};  class Derived : public Base {  
public:  void anotherMethod() {  publicMethod(); // 可以访问基类的public成员  protectedMethod(); // 可以访问基类的protected成员  }  
};  int main() {  Derived d;  d.publicMethod(); // 正确:可以通过Derived实例访问public成员  // d.protectedMethod(); // 错误:不能从Derived实例的外部访问protected成员  
}

在这个示例中,Derived类可以访问基类Base的public和protected成员,但外界(如main函数中的代码)只能访问Derived的public成员以及从基类继承来的public成员,不能访问protected成员。

总结:
1.在实际运用中一般使用都是public继承,几乎很少使用protetced / private继承,也不提倡 使用protetced / private继承,因为protetced / private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
2. 父类的私有 (private) 成员在子类中无论以何种方式继承都是不可见的。这意味着尽管这些成员被继承到了子类对象中,但由于语法上的限制,子类对象无论是在类内部还是外部都无法直接访问这些私有成员。
3. 基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

继承类模版


#include <vector>
#include <list>
#include <deque>#define CLASSTYPE std::vector
//#define CLASSTYPE std:list
//#define CLASSTYPE std:dequetemplate<class T>
class stack : public CLASSTYPE<T>
{
public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到//push_back(x);CLASSTYPE<T>::push_back(x);}void pop(){CLASSTYPE<T>::pop_back();}const T& top(){return CLASSTYPE<T>::back();}bool empty() const {return ClASSTYPE<T>::empty();}};int main()
{stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}

基类与派生类的赋值转换

派生类可以赋值给基类,但是基类不能赋值给派生类

	Person p;Student s;p = s;//oks = p;//error

原因有以下几点:

  1. 类型不匹配:
    虽然基类和派生类有关系,但是它们仍然是两个不同的类型,派生类通过继承机制从基类继承了属性和方法,但是它也有可能添加了自己的属性和方法,或者重写了基类的某些部分。
  2. 违反了继承的目的:
    派生类本就是通过继承基类后再根据需求对于基类进行扩展或修改。如果把基类赋值给派生类,那么派生类的属性和方法就会被影响,那不就白继承了。
  3. 可能导致数据丢失:
    如果基类把派生类扩展或修改的属性和方法中特有的数据给覆盖了,就会导致数据丢失

派生类对象的引用赋值给基类

	Student s;Person& rp = s;

派生类对象的指针赋值给基类对象

	Student s;Person* pp = &s;

继承的作用域

隐藏(重定义):
在继承机制中基类和派生类都有独立的作用域,如果派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

同名成员变量

class Person
{
protected:double _height = 1.75;//身高int _age = 20;//年龄string _name = "renqing";//姓名
};class Student :public Person
{
public:void Print()//隐藏{cout << _height << endl;cout << _age << endl;cout << _name << endl;}
private:double _height = 1.65;//身高int _stuid = 123456;//学号int _grade = 10;//年级
};
int main()
{Student s;s.Print();return 0;
}

在这里插入图片描述
如果想打印基类的_height,加上作用域限定符 : :

void Print()//隐藏
{cout << Person::_height << endl;cout << _age << endl;cout << _name << endl;
}

在这里插入图片描述

同名成员函数

class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};
int main()
{B b;b.fun(10);return 0;
};

在这里插入图片描述
在隐藏关系中,同名函数默认调用的当前作用域的函数,如果想调用其他作用域的函数,则需要使用域作用限定符。

派生类的默认成员函数

在这里插入图片描述

构造函数与析构函数

派生类对象在调用构造函数时会先调用基类的构造函数,再调用派生类的构造函数。调用析构函数时会先调用派生类的析构函数,再调用基类的析构函数。

class Person
{
public:Person(const char* name = "renqing"): _name(name){cout << "Person()" << endl;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};
class Student : public Person
{
public:Student(){cout << "Student()" << endl;}~Student(){cout << "~Student()" << endl;}
protected:int _num; //学号
};int main()
{Student s;return 0;
}

在这里插入图片描述

拷贝构造

派生类构造函数:
派生类需要定义自己的构造函数来初始化自己的成员变量,同时它还需要通过初始化列表来调用基类的构造函数以初始化从基类继承的成员。如果派生类构造函数没有显式地调用基类构造函数,编译器会尝试调用基类的默认构造函数(如果存在的话)。

我举个基类没有默认构造函数的例子:

class Person
{
public:Person(const char* name)//没有默认构造: _name(name){cout << "Person()" << endl;}~Person()//析构{cout << "~Person()" << endl;}
protected:string _name; // 姓名
};
class Student : public Person
{
public:Student(int num, const char* name)//构造:_num(num)//,_name(name) error, Person(name)// 基类没有默认构造函数,需要显示调用基类的构造函数{cout << "Student()" << endl;}~Student(){//因为构成覆盖关系,所以指定域作用限定符Person::~Person();cout << "~Student()" << endl;}
protected:int _num; //学号
};int main()
{Student s(20, "renqing");return 0;
}

编译器会对派生类与基类的析构函数名进行特殊处理,都会被处理成destrutor(),所以派生类与基类的析构函数构成隐藏关系。
在这里插入图片描述

但是为什么Person的析构函数会多调用一次呢?因为编译器为了保证基类的析构最后调用,所以在调用派生类析构函数之后会自动调用基类的构造函数。所以为了保证调用的正确顺序,派生类的析构函数我们不需要显示定义。

赋值重载

赋值运算符 operator=:
子类的 operator= 需要显式调用父类的 operator=
来完成父类成员的复制,并需要指定父类的作用域。

派生类赋值重载调用基类赋值重载时一定要加域作用限定符,不然就会发生死循环。

//拷贝构造
Person(const Person& p): _name(p._name)
{
}
//赋值重载
Person& operator=(const Person& p)
{if (this != &p)_name = p._name;return *this;
}
Student(const Student& s)//拷贝构造:_num(s._num), Person(s)//派生类赋值给基类
{;
}
//赋值重载
Student& operator = (const Student& s)
{if (this != &s){//加域作用限定,否则发生死循环Person::operator =(s);_num = s._num;}return *this;
}

不能被继承的类

⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。

// C++11的⽅法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的⽅法/*Base(){}*/
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};
int main()
{Base b;Derive d;return 0;
}

在这里插入图片描述

继承与友元

友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。

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

在这里插入图片描述

继承与静态成员

基类定义了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;
}

在这里插入图片描述

多继承与菱形继承

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。

在这里插入图片描述

	// 单继承
class Person {
public:string _name;
};class Student : public Person {
public:int _number;
};class info : public Student {
public:int year;
};int main() {info s1;return 0;
}

在这里插入图片描述

// 多继承
class Person {
public:string _name;
};class Student {
public:int _number;
};class info : public Student, public Person
{
public:int year;
};int main() 
{info s1;return 0;}

在这里插入图片描述

// 菱形继承
class Person {
public:string _name;
};class Student : public Person {
public:int _number;
};class info :  public Person
{
public:int year;
};class Son : public Student, public info {
public:string wang;
};int main() {Son s1;s1._name = "李四";	//E0266	"Son::_name" 不明确	return 0;
}

菱形继承发生在多继承环境中,当一个类(称为“孙子类”)继承自两个或多个基类,而这些基类又共同继承自同一个基类时,这就叫菱形继承。一定要避开菱形继承,因为菱形继承会造成两个问题:数据冗余和二义性。

虚继承

虚继承主要用于解决多重继承中可能出现的二义性和数据冗余问题。
虚拟继承(Virtual Inheritance)在C++中通过在继承声明中加上virtual关键字来实现,使得派生类在继承多个基类时,对于某个共享基类的继承只保留一份基类拷贝,而不是在每个基类中都保留一份。

class Person
{
public:string _name; // 姓名
};
//虚继承
class Student : virtual public Person
{
protected:int _num; //学号
};
//虚继承 
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};

菱形继承(二义性和数据冗余)
在这里插入图片描述
菱形虚拟继承(解决了二义性和数据冗余)
在这里插入图片描述

继承与组合

is-a关系
public继承就是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。基类的内部细节对子类可见。

class A
{};class B : public A
{};

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为**白箱复用(white - box reuse)。**术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

has-a关机

class A
{};class B
{A _aa;
};

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。

总结:
继承耦合度高,所以依赖关系很强。
组合耦合度低,依赖关系不强。
所以推荐使用组合而不是继承。


http://www.ppmy.cn/devtools/114146.html

相关文章

游戏开发引擎__游戏场景(灯光,摄像机)

1.灯光 重要参数介绍 类型: 控制灯光的类型&#xff0c;有“定向”“点”“区域”和“聚光”4种模式。颜色: 控制灯光的颜色。模式: 控制灯光的光照模式&#xff0c;有“实时”“混合”和“烘焙”3种模式。强度: 控制灯光的明亮程度。间接乘数: 改变间接光的强度。阴影类型: …

【百日算法计划】:每日一题,见证成长(016)

题目 环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环 思路1 用哈希表的思想&#xff0c;遍历链表&#xff0c;判断节点在哈希表中是否存在。 public boolean hasCycle2(ListNode head) {HashSet<ListNode> hashSet new HashSet<>();ListNo…

python 2024-9

第一课 问题 a, b 求最大值&#xff1f;分类讨论 if a > b:print("最大值 "&#xff0c; a)else:print("最大值 "&#xff0c; b)a, b, c 求最大值&#xff1f; 条件语句 if ... elif ... else列表最大值&#xff1f;与参照物循环比较 a [1.7, 1.…

Spring Boot- 配置中心问题

Spring Boot 配置中心相关问题探讨 在现代微服务架构中&#xff0c;随着系统规模的扩展和复杂度的增加&#xff0c;配置管理变得越来越重要。每个微服务都可能有大量的配置文件&#xff0c;包括数据库连接信息、缓存配置、消息队列配置等。如果每个服务独立管理配置文件&#…

java基础面试题总结

java基础面试题总结 目录 前言 1. JVM vs JDK vs JRE的了解 2. 谈谈你对编程、编译、运行的理解 3. 什么是字节码?采用字节码的好处是什么? 5. java中的注解有几种&#xff0c;分别是什么&#xff1f; 6. 字符型常量和字符串常量 7.标识符和关键字的认识 8. 泛型&#xff…

激光粉尘传感器:筑牢粮仓安全防线,有效应对粮食粉尘爆炸高危风险

随着我国农业的持续发展和粮食产量的稳步提升&#xff0c;2023年全国粮食总产量达到了13908.2亿斤&#xff0c;这一丰硕成果不仅保障了国家的粮食安全&#xff0c;也对粮食的储备、加工、运输等环节提出了更高的要求。然而&#xff0c;在粮食产业链的各个环节中&#xff0c;粮食…

Linux用户账号管理

目录 一、useradd 创建新用户 二、usermod 修改用户账号 三、userdel 删除用户账号 四、passwd 设置或更改用户密码 五、who 或 w 查看当前登录用户 六、切换用户 6.1. su命令切换用户 6.2. sudo授权命令 6.2.1. sudo的特性 6.2.2. sudo的相关文件 6.3. exit退出 6…

后端往前端传递数据json方法大全

数据格式/传递方法描述适用场景示例XML可扩展标记语言&#xff0c;结构化数据配置文件&#xff0c;SOAP Web服务response.setContentType("application/xml");// 使用JAXB或DOM解析器生成XMLHTML直接返回HTML片段部分页面更新&#xff0c;传统服务器端渲染response.s…

机器学习与深度学习的区别详解

机器学习与深度学习的区别详解 在数据科学和人工智能领域&#xff0c;机器学习&#xff08;Machine Learning, ML&#xff09;和深度学习&#xff08;Deep Learning, DL&#xff09;是两个非常重要的概念。尽管这两个术语常常被提及&#xff0c;并且有时会被混淆&#xff0c;但…

WIFI路由器的套杆天线简谈

❝本次推文简单介绍下WIFI路由器的套杆天线。 路由器天线 路由器在这个万物互联的时代&#xff0c;想必大家对其都不陌生。随着科技的发展&#xff0c;常用的路由器上的天线也越来越多&#xff0c;那么问题来了&#xff1a;天线越多&#xff0c;信号越好吗&#xff1f;路由器…

ElK 8 收集 Nginx 日志

1. 说明 elk 版本&#xff1a;8.15.0 2. 启个 nginx 有 nginx 可以直接使用。我这里是在之前环境下 docker-compose.yml 中启动了个 nginx&#xff1a; nginx:restart: alwaysimage: nginx:1.26.1ports:- "80:80"- "443:443"volumes:#- ./nginx/html:/…

二百六十四、Java——Java采集Kafka主题A的JSON数据,解析成一条条数据,然后写入Kafka主题B中

一、目的 由于Hive是单机环境&#xff0c;因此庞大的原始JSON数据在Hive中解析的话就太慢了&#xff0c;必须放在Hive之前解析成一个个字段、一条条CSV数据 二、IDEA创建SpringBoot项目 三、项目中各个文件 3.1 pom.xml <?xml version"1.0" encoding"UTF…

Xorbits Inference(Xinference):一款性能强大且功能全面的大模型部署与分布式推理框架

大模型部署与分布式推理框架Xinference Xinference的基本使用概述安装启动服务模型部署模型参数配置说明 API接口概述对话接口模型列表嵌入模型Rerank模型使用Xinference SDK使用OpenAI SDK 命令行工具概述启动模型引擎参数其他操作 集成LoRA启动时集成LoRA应用时集成LoRA 部署…

STM32 单片机最小系统全解析

STM32 单片机最小系统全解析 本文详细介绍了 STM32 单片机最小系统&#xff0c;包括其各个组成部分及设计要点与注意事项。STM32 最小系统在嵌入式开发中至关重要&#xff0c;由电源、时钟、复位、调试接口和启动电路等组成。 在电源电路方面&#xff0c;采用 3.3V 直流电源供…

【微服务-注册中心】

注册中心的作用&#xff1a; 微服务将业务拆分成了一个一个服务&#xff0c;当实现一个业务的时需要调用多个服务&#xff0c;那么每个服务的调用都需要知道它的URL。如何更方便的调用&#xff0c;注册中心就出现了。 我们可以把注册中心当作通讯录&#xff0c;通讯录中记录了服…

【C++】猜数字小游戏

写一个简单的C程序&#xff0c;用于实现一个猜数字游戏。 要求&#xff1a; 生成一个1到100之间的随机数&#xff0c;并让玩家猜测这个数是什么。如果玩家的猜测不正确&#xff0c;程序会提示猜测过大或过小&#xff0c;直到玩家猜对为止。 要点&#xff1a; _CRT_SECURE_NO…

[数据集][目标检测]智慧养殖场肉鸡目标检测数据集VOC+YOLO格式3548张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;3548 标注数量(xml文件个数)&#xff1a;3548 标注数量(txt文件个数)&#xff1a;3548 标注…

智能体趋势:未来科技的核心驱动力

随着人工智能&#xff08;AI&#xff09;技术的不断发展&#xff0c;**智能体&#xff08;intelligent agents&#xff09;**逐渐成为当今科技发展的重要趋势。这些智能体不仅仅是软件&#xff0c;它们正在改变我们生活和工作的方式&#xff0c;成为推动科技和社会变革的核心力…

C#中的委托

Action委托 Action 委托是 C# 中预定义的委托类型之一&#xff0c;它是泛型委托 Action<T...> 的非泛型形式&#xff0c;用于表示不接受返回值的方法。Action 委托通常用于需要执行操作但不需要返回结果的场景&#xff0c;比如事件处理、异步编程或回调函数。 Action 委…

QT实现TCP协议

QT中实现服务器原理 QT中实现客户端原理 网络聊天室服务器实现 用QTcpServer服务器类实例化一个服务器对象通过listen&#xff08;&#xff09;函数&#xff0c;监听客户端&#xff0c;监听可以监听指定主机&#xff0c;也可以监听任意主机&#xff0c;监听的端口号&#xff0…