【C++】继承的理解

news/2024/11/5 13:25:14 标签: c++, 开发语言, 学习, 笔记

1.继承的概念和定义

1.1继承的概念

继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象 程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 承是类设计层次的复用。
#include <iostream>
class A
{
public:
	int ma;
private:
	int mb;
protected:
	int mc;
};


class B : public A	// 继承:A叫做基类/父类	B叫派生类/子类
{
public:
	void func()
	{
		std::cout << ma << std::endl;
	}
	int md;
private:
	int me;
protected:
	int mf;
};

int main()
{
	A a;
	B b;
	std::cout << sizeof(a) << std::endl;	// 12
	std::cout << sizeof(b) << std::endl;	// 24
}

1.2继承基类成员访问方式的变化 

 

总结:

 1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected可以看出保护成员限定符是因继承才出现的

3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected > private。

4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过 最好显示的写出继承方式

5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强

1.3默认的继承方式? 

class定义的默认private,struct定义的默认public的.

1.4派生类的构造函数

派生类可以继承基类的构造函数和析构函数,用来初始化和释放从基类继承来的成员变量
派生类的构造函数和析构函数,负责初始化和清理派生类
派生来从基类继承来的成员的初始化和清理由基类的构造函数和析构函数来负责。

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

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
  • 或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
  • 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run
  • Time Type Information)dynamic_cast 来进行识别后进行安全转换。
#include <iostream>
/*
1.把继承结构,也说成从上(基类)到下(派生类)的结构
2.

	基类对象 -> 派生类对象
	派生类对象 -> 基类对象

	基类指针(引用) -> 派生类对象
	派生类指针(引用) -> 基类对象

	总结:在继承结构中进行上下的类型转换,默认支持从下到上的类型的转换
*/

class Base
{
public:
	Base(int data = 10) :ma(data)
	{}
	void show()
	{
		std::cout << "Base:show()" << std::endl;
	}
	void show(int)
	{
		std::cout << "Base:show(int)" << std::endl;
	}
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 20):Base(data), mb(data)
	{}
	void show()
	{
		std::cout << "Derivr::show()" << std::endl;
	}
private:
	int mb;
};

int main()
{
	Base b(10);
	Derive d(20);

	// 基类对象 <- 派生类对象		类型从下到上的转换	Y
	b = d;
	// 派生类对象 <- 基类对象		类型从上到下的转换	N
	// d = b;
	// 基类指针(引用)<- 派生类对象	类型从下到上的转换	Y
	// 只能访问派生类继承的基类的那部分内容
	Base* pb = &d;
	pb->show();
	pb->show(10);
	((Derive*)pb)->show();	// 类型强转为派生类的指针就可以访问派生类的内容
	// 派生类指针(引用)<- 基类对象	类型从上到下的转换	N
	/*Derive* pd = (Derive*) & b;	不安全,涉及了内存的非法访问
	pd->show();*/

	d.show();
	d.Base::show(10);
	// d.show(20);	// 优先找的是派生类自己作用域的show名字成员;没有的话才去基类里面找

	return 0;
}

3.重载、隐藏、覆盖


    1.重载关系    
    一组函数要重载,必须处在同一个作用域中;而且函数名字相同,参数列表不同

    2.隐藏关系
    在继承结构当中,派生类的同名成员把基类的同名成员给隐藏调用了

    3.覆盖关系/相当于重写方法
    虚函数表中虚函数地址的覆盖

4.虚函数、静态绑定、动态绑定

#include <iostream>
#include <typeinfo>

class Base
{
public:
	Base(int data = 10) :ma(data)
	{}
	// 虚函数
	virtual void show()
	{
		std::cout << "Base:show()" << std::endl;
	}
	// 虚函数
	virtual void show(int)
	{
		std::cout << "Base:show(int)" << std::endl;
	}
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 20) :Base(data), mb(data)
	{}
	void show()
	{
		std::cout << "Derivr::show()" << std::endl;
	}
private:
	int mb;
};

int main()
{
	Derive d(50);
	Base* pb = &d;

	/*
	pb是基类类型	Base::show如果发现show是普通函数,就进行静态绑定call Base::show()
	如果发现pb是基类类型,编译阶段在Base类中去看show函数,发现是虚函数,就进行动态绑定了
	*/
	pb->show();		// 静态(编译时期)的绑定(函数的调用)
	pb->show(10);

	std::cout << sizeof(Base) << std::endl;
	std::cout << sizeof(Derive) << std::endl;
	/*
	pb的类型:Base -> 有没有虚函数
	如果Base没有虚函数,*pb识别的就是编译时期的类型, *pb -> Base类型
	如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI 类型 "calss Derive"
	*/
	std::cout << typeid(pb).name() << std::endl;
	std::cout << typeid(*pb).name() << std::endl;

	return 0;
}

总结一:
如果一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生了一个唯一的vftable虚函数表,
虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。
RTTI:run-time type information 运行时的类型信息    指向类型字符串,例如我的类是Base那么&RTTI指向"Base"
当程序运行时,每一张虚函数表都会加载到内存的.rodata区,是个只读数据区,也叫常量区。

总结二:
一个类里面定义了虚函数,那么这个类定义的对象。其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向
类型的虚函数表vftable。一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表。

总结三:
一个类里面虚函数的个数,不影响对象内存大小,都是多一个虚函数指针(vfptr    4个字节),影响的是虚函数表的大小

总结四:
如果中的方法和基类继承来的某个方法,返回值,函数名,参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。这两个函数的关系是覆盖的关系,相当于重写了这个函数

5.派生类的默认成员函数

6 个默认成员函数, 默认 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 ( 这个我们后面会讲解) 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

 6.继承和友元

友元关系不能继承 ,也就是说基类友元不能访问子类私有和保护成员
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;
}
void main()
{
 Person p;
 Student s;
 Display(p, s);
}

7.继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子
类,都只有一个 static 成员实例
class Person
{
public :
 Person () {++ _count ;}
protected :
 string _name ; // 姓名
public :
 static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
 int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
 string _seminarCourse ; // 研究科目
};
void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl;
}


http://www.niftyadmin.cn/n/5739517.html

相关文章

JavaScript——(3)

五、JS数组 1. 什么是数组 数组&#xff08;Array&#xff09;&#xff0c;顾名思义&#xff1a;用来存储一组相关值的类型。 数组可以方便地对一组值进行求和、计算平均值、逐项遍历等操作。 java : int[] array new int[]{1,2,3}int[] array {1,2,3}int[] array new in…

23.智能停车计费系统(基于springboot和vue的Java项目)

目录 1.系统的受众说明 2 相关概念和技术介绍 2.1 JAVA技术介绍 2.2 SpringBoot框架 2.3B/S架构 2.4 MySQL数据库 3 系统需求分析 3.1 问题定义 3.2 可行性分析 3.3系统用例分析 3.4 系统流程分析 3.4.1 登录流程 3.4.2 添加信息流程 3.4.3 删除信息流程 4…

什么是组态软件?Web组态软件又是什么?

从事相关工作的对“组态软件”应该都不陌生&#xff0c;那Web组态软件又是什么呢?本文将对Web组态可视化软件&#xff08;下称“Web组态软件”&#xff09;做简单介绍&#xff0c;可视化编辑器是Web组态软件中的一个重要功能模块。除了编辑器&#xff0c;还有哪些功能模块?又…

数据结构之二叉树--前序,中序,后序详解(含源码)

二叉树 二叉树不能轻易用断言&#xff0c;因为树一定有空 二叉树链式结构的实现 在学习二叉树的基本操作前&#xff0c;需先要创建一棵二叉树&#xff0c;然后才能学习其相关的基本操作。 typedef int BTDataType; typedef struct BinaryTreeNode {BTDataType _data;struct B…

1.3 初探OpenCV贡献库

OpenCV贡献库&#xff08;opencv_contrib&#xff09;是OpenCV的一个扩展库&#xff0c;由社区开发&#xff0c;包含更多视觉应用和受专利保护的算法。它提供最新研究算法、扩展功能和社区支持。可以通过pip安装或手动编译。

Spring设值注入

设值注入&#xff08;Setter Injection&#xff09;是Spring框架中依赖注入的一种方式&#xff0c;通过Setter方法将依赖对象注入到目标对象中。设值注入在对象创建后&#xff0c;通过调用Setter方法完成依赖注入。 设值注入的优点 灵活性&#xff1a;设值注入允许在对象创建…

jenkins 构建报错 mvn: command not found

首先安装过 maven&#xff0c;并且配置过环境变量 win r ,输入 cmd 键入 mvn -v 出现上图输出&#xff0c;则证明安装成功。 原因 jenkins 没有 maven 配置全局属性, 导致无法找到 mvn 命令。 解决方案 找到全局属性&#xff0c;点击新增&#xff0c;配置 MAVEN_HOME 路…

STM32CUBEIDE FreeRTOS操作教程(八):queues多队列

STM32CUBEIDE FreeRTOS操作教程&#xff08;八&#xff09;&#xff1a;queues多队列 STM32CUBE开发环境集成了STM32 HAL库进行FreeRTOS配置和开发的组件&#xff0c;不需要用户自己进行FreeRTOS的移植。这里介绍最简化的用户操作类应用教程。以STM32F401RCT6开发板为例&#…