继承(C++)

news2025/1/11 21:04:03

继承

  • 一、初识继承
    • 概念
      • “登场”
      • 语法格式
    • 继承方式
      • 九种继承方式组合
      • 小结(对九种组合解释)
  • 二、继承的特性
    • 赋值转换 一一 切片 / 切割
    • 作用域 一一 隐藏 / 重定义
  • 三、派生类的默认成员函数
    • 派生类的默认成员函数
    • 1. 构造函数
    • 2. 拷贝构造
    • 3. 赋值运算符重载
    • 4. 析构函数
  • 四、延伸知识
    • 1. 继承与友元
    • 2. 继承与静态成员
  • 五、单继承和多继承
    • 单继承
    • 多继承
      • 菱形继承
      • 菱形虚拟继承
        • 语法
        • 原理
  • 总结
    • 拓展知识:组合

一、初识继承

概念

继承保持原有类特性的基础上进行扩展,增加功能,产生新的类。新的类就叫做派生类(子类),原有类就叫做基类(父类)。
继承的作用:继承机制是面向对象程序设计使代码可以复用的最重要的手段,继承是类设计层次的复用,呈现了面向对象程序设计的层次结构

“登场”

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << endl;
	}

protected:
	string _name = "张三";
	int _age = 18;
};

//继承后,父类的Person成员(成员函数 + 成员变量)都会成为子类一部分
class Student : public Person
{
protected:
	int _stuId;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

展现继承
结论:代码体现出Student对Person的继承(复用)

语法格式

定义格式

继承方式

继承方式和访问限定符:
继承方式和访问限定符

九种继承方式组合

C++中的继承方式和访问限定符组合,形成了九种情况的继承结果

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

eg:实例演示三种继承关系下基类成员的各类型成员访问关系的变化

class Person
{
public:
	void Print()
	{
		cout << "名字:" << _name << endl;
	}
protected:
	string _name = "张三";
private:
	int _age = 18;
};

//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
	int _stuId = 111;
};


int main()
{
	Student s;
	s.Print();
	return 0;
}

关系变化

小结(对九种组合解释)

不需要全部记完,这两种是最常用的记住即可:
常用

  1. 在九种组合表中,基类的私有成员是不可见的。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符, 继承方式),(public > protected > private)。 Min:两者的较小者。
  2. 基类private成员在派生类中什么方式都不可见。不可见:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能访问
  3. 基类成员不想在类外直接被访问,但要在派生类中能访问,就要定义为protected。保护成员限定符是因为继承才出现的
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。建议:显示写
  5. 实际运用一般都是public继承,扩展维护性强

二、继承的特性

赋值转换 一一 切片 / 切割

派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这个过程叫切片或者切割,不会产生临时变量,发生赋值兼容,就同把派生类中父类那部分切来赋值过去。

原理:(切片 / 切割)
切片/切割

eg:证明:不会产生临时变量

class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};

class Student : public Person
{
public:
	int _No;
};

int main()
{
	int i = 0;
	//double& d = i;   //error
	const double& rd = i;   //int赋值给double类型的值,会产生临时变量,所以要+const

	//派生类对象可以直接赋值给基类对象,不要+const,也就证明,这个过程没有产生临时变量
	Student s;
	Person& p = s;
	return 0;
}

注意:派生类对象赋值给基类的对象(或者基类的指针,或者基类的引用)这个过程称为向上转换。

拓展(不作详细介绍): 基类的指针和引用可以通过强转赋值给派生类的指针或者引用,但基类的指针是指向派生类对象时才安全。-- 这个过程称为向下转换。注意:基类对象不能赋值给派生类对象。

eg:

class Person
{
//protected:
public:
	string _name = "peter";
	string _sex = "male";
	int _age = 18;
};

class Student : public Person
{
public:
	int _No = 2140104111;
};

int main()
{
	Student s;
	//1.派生类对象可以赋值给父类对象/指针/引用
	Person p = s;
	Person* ptrp = &s;
	ptrp->_age = 21;
	Person& rp = s;
	rp._name = "张三";

	//2.基类对象不能赋值给派生类对象
	//s = p;   //error
	return 0;
}

代码分析:
代码分析

注意:使用保护继承,成员权限会发生变化

Person p = s;  //就会出现错误

这是因为保护继承下,派生类的对象只能被派生类或派生类的子类引用,而不能被基类引用。

原理: 因为非公有派生类(私有或保护派生类)不能实现基类的全部功能,例如在派生类外不能调用基类的公用成员函数访问基类的私有成员。因此,只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。

作用域 一一 隐藏 / 重定义

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问(子类成员隐藏父类成员),这叫做隐藏(或者重定义)。 (在子类成员中,可以 基类::基类成员 显示访问。但是指定作用域,如果找不到会直接报错,不会再去访问别的域。eg:派生类成员)
  3. 成员函数隐藏:函数名相同就构成隐藏
  4. 建议:最好不要定义同名成员
    拓展:访问成员遵循就近原则(编译器既定顺序):局部域-当前类域-父类域-全局域

eg1:成员变量

class Person
{
protected:
	string _name = "张三";
	int _num = 111;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;

		//指定显示访问
		cout << "Person::_num:" << Person::_num << endl;

		//默认访问子类。子类隐藏了父类
		cout << "_num:" << _num << endl;
	}

protected:
	int _num = 999;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

//output:
//姓名:张三
//Person::_num:111
//_num : 999

eg2:成员函数

//fun不构成重载,因为不在同一作用域
//fun构成隐藏,成员函数满足函数名相同
class A
{
public:
	void fun()
	{
		cout << "fun()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "fun(int i)->" << i << endl;
	}
};


int main()
{
	B b;
	b.fun(1);
	//b.fun();    //参数不匹配

	//指定访问
	b.A::fun();
	return 0;
}

//output:
//fun()
//fun(int i)->1
//fun()

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

派生类的默认成员函数

演示代码:后面会分为四个部分进行拆分讲解

class Person
{
public:
	//如果没有默认构造,必须在派生类的初始化列表显示调用
	//Person(const char* name)
	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;
};

class Student : public Person
{
public:
	Student(const char* name, int num)
		:Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}

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

protected:
	int _num;
};


int main()
{
	Student s1("jack", 18);   //构造函数
	Student s2(s1);           //拷贝构造函数
	Student s3("rose", 17);   
	s1 = s3;                  //赋值运算符重载
	return 0;
}

构造和析构调用和执行顺序图
调用结构

1. 构造函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造,则必须在派生类构造的初始化列表阶段显式调用
  2. 派生类对象初始化先调用基类构造再调派生类构造

eg1:(有默认构造)

class Person
{
public:
	Person(const char* name = "张三")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name, int num)
		:_stuId(num)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuId;
};

int main()
{
	Student s("jack", 18);
	return 0;
}

代码F11逐语句执行过程:
代码F11逐语句执行过程

eg2:(没有默认构造)
基类没有默认构造,在派生类必须显示调用,Person先初始化,然后是_stuId。(基类先声明,所以先初始化Person)

//没有默认构造
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name, int num)
		:Person(name)  //在初始化列表调用基类默认构造。如同定义匿名对象
		, _stuId(num)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuId;
};

int main()
{
	Student s("jack", 18);
	return 0;
}

代码F11逐语句执行过程:代码F11逐语句执行过程

2. 拷贝构造

  1. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

演示代码的拷贝构造部分:
拷贝构造

3. 赋值运算符重载

  1. 派生类的operator=必须调用基类的operator=完成基类的复制

赋值运算符重载

4. 析构函数

  1. 派生类的析构会在调用完成后自动调用基类的析构函数清理基类成员。原因:为了保证派生类对象,先清理派生类成员再清理基类成员的顺序
  2. 因为后续的一些场景,析构函数要构成重写,重写的条件之一就是函数名相同。所以编译器对析构函数名进行特殊处理,处理成destructor()。所以父类析构函数不+virtual,子类析构函数和父类析构函数构成隐藏关系

析构函数

四、延伸知识

1. 继承与友元

友元关系不能继承,所以基类的友元不能访问子类私有成员和保护成员

eg:

class Student;  //先声明,因为在Person中引用了Student对象
class Person
{
	friend void Dispaly(const Person& p, const Student& s);
protected:
	string _name = "张三";
};

class Student : public Person
{
protected:
	int _stuId = 0;
};
void Dispaly(const Person& p, const Student& s)
{
	cout << s._name << endl;  //ok
	cout << p._name << endl;  //ok
	cout << s._stuId << endl;  //error   
}

int main()
{
	Dispaly(Person(), Student());
	return 0;
}

注意:如果想要在Display()中调用s._stuId,要在Student类中也加上友元

2. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员

class Person
{
public:
	Person()
	{
		++_count;
	}
protected:
	string _name;
public:
	static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int stuId;
};
class Graduate : public Student
{
protected:
	string _seminarCourse;
};

int main()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << "人数:" << Person::_count << endl;
	Graduate::_count = 0;
	cout << "人数:" << Person::_count << endl;
	return 0;
}

//output:
//人数:4
//人数:0

注意:静态成员属于父类和派生类,在派生类不会单独再拷贝一份,继承的是使用权。eg:上面的代码使用的始终都是一个_count

五、单继承和多继承

单继承

单继承:一个子类只有一个直接父类时称这个关系为单继承
单继承

多继承

多继承

菱形继承

菱形继承:多继承的一种特殊情况。
菱形继承
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承由数据冗余(浪费空间)和二义性(不知道访问谁) 问题,在Assistant的对象中Person成员有两份

成员模型
菱形继承代码:

class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _stuId;
};

class Teacher : public Person
{
protected:
	int _workId;
};

class Assistant : public Student, public Teacher
{
protected:
	string _course;
};

int main()
{
	Assistant a;
	//这样会有二义性问题,无法明确访问的哪一个
	//a._name = "peter";   //error

	//显示指定访问那个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

	return 0;
}

对于菱形继承解决不了的问题,出现了虚拟继承。

菱形虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余问题。

语法

如上面菱形继承代码的继承关系,在Student和Teacher继承Person时使用虚拟继承。
eg:
虚拟继承

原理

借用简化的菱形继承体系,通过内存窗口观察对象成员的模型

菱形继承

class A
{
public:
	int _a;
};

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

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

菱形继承

菱形虚拟继承

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

菱形虚拟继承

注意: D中为什么B和C部分要找属于自己的A
解释:
在这里插入图片描述
菱形虚拟继承的原理解释:
菱形虚拟继承的原理解释

总结

  1. 虚拟继承的缺陷:虚拟继承会增加程序的复杂性,因为派生类需要特别处理虚基类的初始化和访问。虚拟继承还可能导致一些性能上的损失,因为派生类需要额外的指针来访问虚基类。-- 不建议使用菱形继承
  1. 继承和组合:
    • public继承是一种is-a的关系。eg:植物和花
    • 组合是一种has-a的关系。 eg:轮胎和车

拓展知识:组合

**优先使用对象组合,而不是类继承,**组合耦合度低,代码维护性好

  1. 白盒测试:知道底层
  2. 黑盒测试:不知道底层
  1. 继承,通过生成派生类的复用称为白箱复用(white-box reuse)。白箱(相对可视性而言):在继承方式中,基类内部细节对子类可见。继承一定程度破坏了基类的封装。耦合度高:基类的改变极大的影响派生类,两者关系紧密
  2. 组合,新的更复杂的功能可以通过组装或组合对象获得。被组合对象具有良好定义的接口。这种复用风格称为黑箱复用(black-box reuse),**对象内部细节不可见。**组合类之间没有很强的依赖关系,耦合度低

eg:

//继承
//Car和BMW  Car和Benz构成is-a关系
class Car
{
protected:
	string _color = "白色";
	string _num = "陕IT6666";
};

class BMW : public Car
{
public:
	void Drive()
	{
		cout << "好开" << endl;
	}
};

class Benz : public Car
{
public:
	void Drive()
	{
		cout << "好坐" << endl;
	}
};

//组合
//Tire和Car构成has-a关系
class Tire
{
protected:
	string _brand = "Michelin";
	size_t _size = 17;
};

class Car
{
protected:
	string _color = "白色";
	string _num = "陕IT6666";
	Tire _t;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/915774.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

部署FTP服务(二)

目录 2.访问FTP服务 1.使用ftp命令行工具 2.使用浏览器 3.使用FileZilla Client 3.Serv-U 1.定义新域 2.创建用户 4. windowsserver搭建ftp服务器 一、FTP工具 二、Windows资源管理器 三、IE浏览器访问 2.访问FTP服务 下面在一台装有Windows10操作系统的计算机中&#…

vue 简单实验 v-on html事件绑定

1.代码 <script src"https://unpkg.com/vuenext" rel"external nofollow" ></script> <div id"event-handling"><p>{{ message }}</p><button v-on:click"reverseMessage">反转 Message</but…

医药市场调研--原始价值数据库分享<医药行业必读>

医药市场调研公司是专门从事医药行业市场调研的企业。它们的主要职责是收集、分析和解读与医药行业相关的市场数据和信息&#xff0c;为企业提供决策支持和战略指导。这些公司通过各种调研方法和工具&#xff0c;如市场调查、数据分析、定性研究等&#xff0c;帮助企业了解市场…

TCP的三次握手 四次挥手以及TCP的11种状态

三次握手流程&#xff1a; 客户端给服务端发送数据时&#xff0c;数据包中带有一个头&#xff0c;这个头就是前几十个字节&#xff0c;就是下面这张图。从源端口号&#xff0c;目的端口号&#xff0c;一直到序列号&#xff0c;直到Options。第一个包会将这前十几个字节中的SYN置…

【从零学习python 】72. 深入理解Socket通信及创建套接字的方法

文章目录 1. 不同电脑上的进程之间如何通信2. 什么是socket3. 创建socket进阶案例 1. 不同电脑上的进程之间如何通信 首要解决的问题是如何唯一标识一个进程&#xff0c;否则通信无从谈起&#xff01; 在1台电脑上可以通过进程号&#xff08;PID&#xff09;来唯一标识一个进程…

取模运算符在数组下标的应用

什么是取模运算符%&#xff1f; 定义&#xff1a; a mod b&#xff0c;设a、b属于正整数且b>0&#xff0c;如果q、r属于正整数满足aq*br&#xff0c;且0≤r<b&#xff0c;则定义&#xff1a; a mod b r 注意&#xff1a;取模运算符两侧的除数和被除数都是整数&#xff…

中秋节思维导图怎么绘制?教你使用这种绘制方法

中秋节思维导图怎么绘制&#xff1f;中秋节是中国传统的一个重要节日&#xff0c;许多人会在这一天与家人、朋友聚在一起庆祝&#xff0c;品尝月饼、猜灯谜、赏月等。中秋节作为一个具有浓厚文化底蕴的节日&#xff0c;其历史文化知识十分丰富&#xff0c;而通过绘制思维导图&a…

Three.js 实现模型材质局部辉光(发光,光晕)效果和解决辉光影响场景背景图显示的问题

1.Three.js 实现模型材质局部辉光&#xff08;发光&#xff0c;光晕&#xff09;效果 2.解决辉光效果影响场景背景图显示的问题 相关API的使用&#xff1a; 1. EffectComposer&#xff08;渲染后处理的通用框架&#xff0c;用于将多个渲染通道&#xff08;pass&#xff09;组…

iPhone 14 Pro 动态岛的功能和使用方法详解

当iPhone 14 Pro机型发布时,苹果公司将软件功能与屏幕顶部的药丸状切口创新集成,称之为“灵动岛”,这让许多人感到惊讶。这篇文章解释了它的功能、工作原理,以及你如何与它互动以执行动作。 一、什么是灵动岛?它是如何工作的 在谣言周期的早期‌iPhone 14 Pro‌ 在宣布时…

只考一门数据结构,计算机学硕复录比1:1的山东双非学校考情分析

青岛理工大学 考研难度&#xff08;☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、23专业目录、23复试详情、各专业考情分析、各科目考情分析。 正文1420字&#xff0c;预计阅读&#xff1a;3分钟 2023考情概况 青岛理工…

Linux下的系统编程——基础操作(一)

前言&#xff1a; linux系统编程是基于Linux系统进行程序开发的一个过程&#xff0c;主要涉及到的是linux系统中的函数使用如下图所示&#xff1a; 最外层的是咱们的应用程序&#xff0c;这部分程序大多调用的是咱们标准库&#xff0c;或者说是C库&#xff0c;这部分库函数能…

PDF中的表格怎么转换为Excel?这两个工具一定得收藏!

PDF是一种常见的文件格式&#xff0c;它可以保持文件的原始样式和内容&#xff0c;但是也有一些缺点&#xff0c;比如不易编辑和处理数据。如果你想要将PDF中的表格或数据导出到Excel中&#xff0c;以便进行分析、计算或制作图表&#xff0c;那么你可能需要一个专业的PDF转Exce…

Window异常提示:“为了对电脑进行保护,已经阻止此应用”

目录 1.Window异常提示图片&#xff1a; 2.家庭版额外需要执行的解决方式&#xff1a; 3.本地组策略编辑器(后续家庭版和专业版一致) 4.禁用后重新启动电脑就可以正常运行程序了 1.Window异常提示图片&#xff1a; 2.家庭版额外需要执行的解决方式&#xff1a; 代码&#xff…

超纯水中硼离子去除,特种除硼树脂CH-99

超纯水是指水中杂质含量极低的一种水质&#xff0c;其中矢量硼是一种常见的杂质。矢量硼的存在会影响超纯水的质量&#xff0c;因此需要采取一定的方法去除。 常用的去除矢量硼的方法有离子交换法、反渗透法和电化学法等。 电化学法&#xff1a; 是一种利用电化学反应对水中…

Java JDBC学习教程

Java JDBC JDBC 独立于数据库JDBC 不独立于SQLJDBC 不适用于非关系数据库流行的关系数据库JDBC 教程范围JDBC 核心概念JDBC 示例加载JDBC驱动程序打开数据库连接创建语句更新数据库查询数据库关闭数据库连接 Java JDBC API&#xff08;Java Database Connectivity&#xff09…

无涯教程-PHP - preg_grep()函数

preg_grep() - 语法 array preg_grep ( string $pattern, array $input [, int $flags] ); 返回由与给定模式匹配的输入数组元素组成的数组。 如果将flag设置为PREG_GREP_INVERT&#xff0c;则此函数返回输入数组中与给定模式不匹配的元素。 preg_grep() - 返回值 返回使用…

8路光栅尺磁栅尺编码器或16路高速DI脉冲信号转Modbus TCP网络模块 YL99-RJ45

特点&#xff1a; ● 光栅尺磁栅尺解码转换成标准Modbus TCP协议 ● 高速光栅尺磁栅尺4倍频计数&#xff0c;频率可达5MHz ● 模块可以输出5V的电源给光栅尺或传感器供电 ● 支持8个光栅尺同时计数&#xff0c;可识别正反转 ● 可以设置作为16路独立DI高速计数器 ● 可网…

vue 简单实验 v-bind 变量与html属性绑定

1.代码 <script src"https://unpkg.com/vuenext" rel"external nofollow" ></script> <div id"bind-attribute"><span v-bind:title"message">鼠标悬停几秒钟查看此处动态绑定的提示信息&#xff01;</sp…

“产业应用创新奖2023”启动征集

当前&#xff0c;人工智能已经成为新一轮科技革命和产业变革的重要驱动力量&#xff0c;基于强算法、大算力和大数据的大模型成为主流方向。文心大模型和飞桨一直致力于发挥算法模型技术优势&#xff0c;助力AI 大生产加速升级。 文心飞桨赋能千行百业 产业创新不断涌现 数字医…

你做的可视化大屏老被老板嫌弃丑?那是你没掌握这7个动态效果!

数据可视化大屏成为了最近的爆火需求&#xff0c;自从老李我在朋友圈发了一条关于可视化大屏的朋友圈&#xff0c;客户、亲戚、朋友、同学都过来问我这种可视化大屏是怎么做出来的&#xff0c;要花多少钱&#xff1f; 老李也很实诚&#xff0c;直接跟他们说&#xff0c;免费&a…