C++-继承

news2025/6/18 17:29:10

继承

  • 继承的基本概念
    • 继承的概念
    • 继承的定义
      • 继承的格式
      • 继承的方式
      • 继承基类成员访问方式的变化
  • 基类与派生类的对象赋值转换
  • 继承中的作用域
  • 派生类中的默认成员函数
  • 继承与友元
  • 继承中的静态成员
  • 菱形继承
  • 菱形虚拟继承
  • 继承的总结

继承的基本概念

继承的概念

继承机制是面向对象程序设计中一种使代码得到复用的重要手段,让程序员可以在原有类的特性的基础上机型扩展,增加功能,产生新的类,新的类称为派生类,原有的类叫做基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

例如,对于人这个类而言,都会有姓名和年龄,对于学生这个类而言它还具有一些特有的属性,比如说学号。所以,定义学生类的时候可以复用人这个类。

class Person
{
public:
	void Print()
	{
		cout << "name : " << _name << " age : " << _age << endl;
	}
protected:
	int _age = 18;
	string _name = "小明";
};
class Student : public Person
{
protected:
	string _id = "20230101";
};
int main()
{
	Student st1;
	st1.Print();
	return 0;
}

在这里插入图片描述

派生类继承了来自基类的成员,这些继承的成员都会成为派生类的一部分。像上面的Student类来说,并没有定义Print()这个函数,但是由于其继承了Person类,所以在Student类中也会有Print这个成员。在监视窗口下可以看到派生类对象的内部构成。

在这里插入图片描述

继承的定义

继承的格式

在这里插入图片描述

继承的方式

在这里插入图片描述

  • 注意:
    继承方式与访问限定符是两套体系。

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

在这里插入图片描述

这里有个记忆的技巧,就是继承方式与基类的访问限定符与较小者。
public > protected > private

总结:

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

student以private的继承方式继承Person这个基类:

class Person
{
public:
	void Print()
	{
		cout << "name : " << _name << " age : " << _age << endl;
	}
private:
	int _age = 18;
	string _name = "小明";
};
class Student : public Person
{
public:
	void st_print()
	{
		cout << "name : " << _name << " age : " << _age << " id :" << _id << endl;
	}
protected:
	string _id = "20230101";
};
int main()
{
	Student st1;
	st1.st_print();
	return 0;
}

在这里插入图片描述

可以看到,编译时会报错,由于_name,_age都是基类的私有成员,在派生类中是不可见的不管是在类内还是在类外都是不能被访问的。但是在监视窗口可以看到它们确实被继承下来了,只是语法规定它们在派生类中是不可见的。

在这里插入图片描述

基类与派生类的对象赋值转换

  • 派生类的对象可以赋值给基类的对象/指针/引用。有个形象地说法叫做切片,意思就是将派生类中继承自基类的那部分切割出来。
    在这里插入图片描述
  • 基类的对象不可以赋值给派生类
  • 基类的指针或者引用可以通过强势类型转换的方式赋值给派生类的指针或者引用。
class Person
{
public:
	void Print()
	{
		cout << "name : " << _name << " age : " << _age << endl;
	}
protected:
	int _age = 18;
	string _name = "小明";
};
class Student : public Person
{
public:
	int _a = 10;
protected:
	string _id = "20230101";
};
int main()
{
	//Student st1;
	//st1.Print();
	Person* pp = new Person;
	Student* ps = (Student*)pp;
	cout << ps->_a;
	return 0;
}

在这里插入图片描述

可以看到打印出来的是随机值,这是因为基类对象中就没有这个_a变量,你是强制的将基类对象的地址赋值给了派生类指针,去访问派生类中特有的内容。这种做法是不安全的。

继承中的作用域

  • 继承体系中,基类和派生类有独立的作用域。
  • 派生类和基类中存在同名的成员时,在派生类中会屏蔽基类中的同名成员,这种情况叫做隐藏也叫做重定义。但是可以使用 基类::基类成员的这种方式显示的访问。
class A
{
public:
	int _a = 1;
};
class B : public A
{
public:
	int _a = 2;
	void Print()
	{
		cout << "_a : " << _a << endl;
	}
};
int main()
{
	B b;
	b.Print();
	return 0;
}

在这里插入图片描述

如果这里想打印的是从基类中继承的_a变量,那么就要显示的访问。

class B : public A
{
public:
	int _a = 2;
	void Print()
	{
		cout << "_a : " << A::_a << endl;
	}
};

在这里插入图片描述

  • 要注意的是,如果是成员函数构成重定义,只需要函数名相同即可。
class A
{
public:
	void Print(int a = 0)
	{
		cout << "a is : " << a << endl;
	}
};
class B : public A
{
public:
	void Print()
	{
		cout << "this is B " << endl;
	}
};
int main()
{
	B b;
	b.Print();
	return 0;
}

这里要想访问基类的Print函数,也要进行显示的访问。

int main()
{
	B b;
	b.A::Print();
	return 0;
}

在这里插入图片描述

  • 在实际中在继承体系中最好不要使用同名成员。

派生类中的默认成员函数

  • 派生类的构造函数必须调用基类的构造函数来初始化基类的那一部分成员。如果基类没有默认的构造函数,那么就要在派生类的构造函数初始化列表阶段显示调用基类的构造函数。
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}

};
class B : public A
{
public:
	B()
	{
		cout << "B()" << endl;
	}
};
int main()
{
	B b;
	return 0;
}

在这里插入图片描述

  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数来拷贝基类的那部分成员。
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
};
class B : public A
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	B(const B& b)
		:A(b)
	{
		cout << "B(const B& b)" << endl;
	}
};
int main()
{
	B b;
	B b1(b);
	return 0;
}

在这里插入图片描述

  • 派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数来对基类的那部分成员进行赋值。
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		if (this != &a)
		{
			cout << "operator=(const A& a)" << endl;
		}
		return *this;
	}
};
class B : public A
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	B(const B& b)
		:A(b)
	{
		cout << "B(const B& b)" << endl;
	}
	B& operator=(const B& b)
	{
		if (this != &b)
		{
			A::operator=(b);
			cout << "operator=(const B& b)" << endl;
		}
		return *this;
	}
};
int main()
{
	B b;
	B b1(b);
	b1 = b;
	return 0;
}

在这里插入图片描述

  • 派生类的析构函数调用完,会自动调用基类的析构函数做数据的清理。这样才能保证派生类对象先析构,基类对象后析构,因为基类对象先构造,派生类对象后构造。
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		if (this != &a)
		{
			cout << "operator=(const A& a)" << endl;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	B(const B& b)
		:A(b)
	{
		cout << "B(const B& b)" << endl;
	}
	B& operator=(const B& b)
	{
		if (this != &b)
		{
			A::operator=(b);
			cout << "operator=(const B& b)" << endl;
		}
		return *this;
	}
	~B()
	{
		cout << "~B()" << endl;
	}
};
int main()
{
	B b;
	B b1(b);
	b1 = b;
	return 0;
}

在这里插入图片描述

  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类对象析构清理先调用派生类析构再调基类的析构。
  • 多态中要对析构函数进行重写,而重写的要求之一是函数名相同,为此编译器会将析构函数的名字变为destrutor(),所以基类的构造函数不加virtual的情况下,派生类中的析构函数与基类中的析构函数构成隐藏。

继承与友元

友元关系不能继承,也就是说基类的友元函数不饿能访问派生类的私有和保护成员。

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成员,则整个继承体系中只有只有这一个static成员,无论实例化出多少对象。

class A
{
public:
	void Print()
	{
		cout << "_a : " << _a << endl;
	}
	static int _a;
};
int A::_a = 10;
class B : public A
{
public:
	void func()
	{
		++_a;
	}
};
int main()
{
	A a;
	B b;
	a.Print();
	A::_a = 100;
	b.Print();
	B::_a = 1000;
	a.Print();
	return 0;
}

在这里插入图片描述

菱形继承

单继承: 一个子类只有一个直接父类。
多继承: 一个子类有两个及以上的直接父类。
菱形继承: 菱形继承是多继承的一种特殊情况,就是子类的两个直接父类又有共同的直接父类,可能这句话比较绕,画个图来表示一下。
在这里插入图片描述

从图中就能很明显的看出来,D类中有两份相同的A类中的数据,分别是从B类和C类中继承下来的,这无疑就造成了数据冗余和二义性的问题。如果这样的话,D类中使用继承自A类中的成员时要加B:: 或者 C:: 要不然编译器都不知道你到底想用从B类继承下来的那份还是从C类中继承下来的那份。这虽然能解决二义性的问题,但是数据冗余仍然无法解决,所以就出现了菱形虚拟继承这一概念。

class A
{
protected:
	int _a = 1;
};
class B : public A
{};
class C : public A
{};
class D : public B, public C
{
public:
	void print()
	{
		cout << "B::_a" << B::_a << endl;
		cout << "C::_a" << C::_a << endl;
	}
};

在这里插入图片描述

在监视窗口可以看到d这个对象中有两份_a这个变量。

在这里插入图片描述

菱形虚拟继承

为了解决菱形继承中的数据冗余和二义性的问题,提出了虚拟继承这一概念,B类与C类通过虚拟继承的方式继承自A类,这样D类在多继承B类和C类的时候,就不会出现数据冗余和二义性的问题了。

class A
{
public:
	int _a = 1;
};
class B : virtual public A
{};
class C : virtual public A
{};
class D : public B, public C
{
public:
	void print()
	{
		cout << "_a" << C::_a << endl;
	}
};

int main()
{
	D d;
	d._a = 100;
	cout << "d.B::_a : " << d.B::_a << "d.C::_a : " << d.C::_a << endl;
	return 0;
}

在这里插入图片描述

可以看到,我们可以通过d对象直接修改_a的值,并且继承自B类中的_a与继承自C类中的_a的值都被修改了,这充分的证明通过虚拟继承的方式导致在D类中只有一份_a这个变量,解决了数据冗余和二义性的问题。

但这种做法的原理是什么呢?我们通过内存窗口来看一下。

class A
{
public:
	int _a = 1;
};
class B : virtual public A
{
public:
	int _b = 2;
};
class C : virtual public A
{
public:
	int _c = 3;
};
class D : public B, public C
{
public:
	int _d = 4;
	void print()
	{
		cout << "_a" << C::_a << endl;
	}
};

在这里插入图片描述

可以看到d对象中有继承自B类和C类的一个指针,这个指针指向了一张表,这个表里存出了到一个偏移量,这个偏移量就是到从从A类继承的成员变量_a。这个指针叫做虚基表指针,虚基表指针指向的表叫做虚基表,这张表里存储了到A的偏移量。

即使A类中有多个成员变量,只要有到第一个变量的偏移量根据对齐规则就能顺次找到其他变量。并且D类的对象是共用这张虚基表的。

在这里插入图片描述

int main()
{
	D d;
	D d1;
	d.B::_a = 5;
	d.A::_a = 5;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

可以看到不论修改B类中的_a 还是C类中的_a,到头来都是同一个_a。

即使通过虚继承的方式可以解决菱形继承中的数据冗余和二义性的问题,但是并不断推荐这样用。

继承的总结

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
  3. 继承和组合
  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合,而不是类继承 。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
    内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
    大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
    用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
    组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
    封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
    继承,可以用组合,就用组合。

什么是菱形继承?菱形继承的问题是什么?

两个子类继承同一个父类,而又有子类同时继承这两个子类。
数据冗余和二义性。

什么是菱形虚拟继承?它是如何解决数据冗余和二义性的?

两个子类通过虚拟继承的方式继承自同一个父类,它们的子类在多继承自这两个类,这样就形成了菱形虚拟继承。
菱形虚拟继承会在子类对象中原本会有冗余的成员变量只出现一份,并产生一个虚基表指针,这个指针指向一个虚基表,表中存储了到最初的那个类的变量的偏移量,这样便解决了数据冗余和二义性的问题。

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

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

相关文章

【Spring源码】 BeanFactory和FactoryBean是什么?

1、前言 面试官&#xff1a;“看过Spring源码吧&#xff0c;简单说说Spring中BeanFactory和FactoryBean的区别是什么&#xff1f;” 大神仙&#xff1a;“BeanFactory是bean工厂&#xff0c;FactoryBean是工厂bean”。 这么回答&#xff0c;等于面试官问你Spring是什么&…

如何免费使用ChatGPT 4?

自从ChatGPT发布以来&#xff0c;它就取得了巨大的成功。无论是常春藤法学考试还是商学院作业&#xff0c;ChatGPT都被用于各种试验。统计数据显示&#xff0c;ChatGPT每月吸引约9600万用户。随着ChatGPT的巨大成功&#xff0c;Open AI最近推出了它的最新版本&#xff0c;名为“…

Learning to Detect Human-Object Interactions 文章解读

Learning to Detect Human-Object Interactions&#xff0c;WACV&#xff0c;2018 论文下载 code&#xff1a;http://www.umich.edu/∼ywchao/hico/ 摘要 主要研究领域&#xff1a;定义了HOI detection任务&#xff1a;在静态图像中检测人-对象交互&#xff08;HOI&#xff…

Vue路由模式为history的项目部署到Nginx

前言 对于前端工程师而言&#xff0c;多多少少会碰到按需加载的需求。 比如一个系统&#xff0c;需要用户登陆以后才能使用&#xff0c;对于传统的前后端未分离的情况&#xff0c;我们一般的处理方式是&#xff0c;当检测到用户未登录的时候&#xff0c;一般会重定向到登录页面…

JVM运行时数据区的必备知识:Java程序员不容错过

1、JVM运行时数据区概念 JVM运行时数据区是Java虚拟机在执行Java程序时所使用的内存区域。这些区域包括了以下几个部分&#xff1a; 程序计数器&#xff08;Program Counter Register&#xff09;&#xff1a;程序计数器是一块较小的内存区域&#xff0c;它可以看作是当前线程…

测试1号位的自我修养

作者&#xff1a;京东零售 吴聪 引言 目前京东实行BigBoss机制以及积木型组织&#xff0c;同时现阶段再次强调了“经营”理念&#xff0c;以上均是比较大的组织层面的纲领和引导&#xff0c;核心是为了激发大家owner意识可以更好更快为公司产出价值和贡献。落到具体执行层面&…

国内大模型领域进入乱战时代

国内大模型领域进入乱战时代 2023.4.12版权声明&#xff1a;本文为博主chszs的原创文章&#xff0c;未经博主允许不得转载。 什么是大模型 大模型&#xff0c;又称为预训练模型、基础模型等&#xff0c;是指模型参数数量很大&#xff0c;需要大量计算资源才能训练的深度学习…

RHCE-Web服务器

请给openlab搭建web网站​ 网站需求&#xff1a;​ 1.基于域名[www.openlab.com](http://www.openlab.com)可以访问网站内容为 welcome to openlab!!! 首先创建一个名为openlab的网站&#xff1a; &#xff08;1&#xff09;在www目录下创建一个openlab文件夹&#xff1a;mk…

Android UI

什么是 UI 用户界面&#xff08;User Interface&#xff0c;简称 UI&#xff0c;亦称使用者界面&#xff09;是系统和用户之间进行交互和信息交换的媒介&#xff0c;它实现信息的内部形式与人类可以接受形式之间的转换。软件设计可分为两个部分&#xff1a;编码设计与UI设计。A…

JavaScript编程实现tab选项卡切换的效果+1

之前在“圳品”信息系统使用了tab选项卡来显示信息&#xff0c;详见&#xff1a; JavaScript编程实现tab选项卡切换的效果 在tab选项卡中使用其它<div>来显示信息就出现了问题&#xff0c;乱套了&#xff0c;比如下面的这段代码&#xff1a; <!DOCTYPE html> &l…

c/c++:for循环语句,分号不可省略,表达式可以省略,猜数字游戏,跳转语句continue,break,避免写goto

c/c:for循环语句&#xff0c;分号不可省略&#xff0c;表达式可以省略&#xff0c;猜数字游戏&#xff0c;跳转语句continue&#xff0c;break&#xff0c;避免写goto 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学…

树莓派 QT项目开机自启动

我自己用qt设置了一个界面&#xff0c;如何让他开机自启动呢&#xff1f; 目录 1.生成qt项目的可执行文件 2. 编写一个自启动脚本 3.重启树莓派 1.生成qt项目的可执行文件 QT项目的可执行文件就是.exe文件。首先在qt中打开&#xff0c;点击红色方框图标&#xff0c;选择Re…

vue+springboot 上传文件、图片、视频,回显到前端。

效果图 预览&#xff1a; 视频&#xff1a; 设计逻辑 数据库表 前端vue html <div class"right-pannel"><div class"data-box"><!--上传的作业--><div style"display: block" id""><div class"tit…

C++编程法则365条一天一条(359)认识各种初始化术语

文章目录Default initialization默认初始化Copy initialization拷贝初始化Aggregate initialization聚合初始化Direct initialization直接初始化list_initialization列表初始化value_initialization值初始化参考&#xff1a; https://en.cppreference.com/w/cpp/language/copy_…

【unity learn】【Ruby 2D】角色发射飞弹

前面制作了敌人的随机运动以及动画控制&#xff0c;接下来就是Ruby和Robot之间的对决了&#xff01; 世界观背景下&#xff0c;小镇上的机器人出了故障&#xff0c;致使全镇陷入了危机&#xff0c;而Ruby肩负着拯救小镇的职责&#xff0c;于是她踏上了修复机器人的旅途。 之前…

同步I/O实现Reactor和Proactor的差异

有两种高效的事件处理模式&#xff1a;Reactor模式和Proactor模式 Reactor模式 主线程只负责监听socket上是否有事件发生&#xff0c;当有事件发生时&#xff0c;主线程就将该事件放进请求队列&#xff0c;通知工作线程进程处理&#xff1b;主线程不做实质性的工作&#xff0c…

使用颜色检测有向图中的循环

给定一个有向图,检查该图是否包含循环。如果给定的图形至少包含一个循环,您的函数应返回 true,否则返回 false。 例子: 输入: n = 4, e = 6 0 -> 1, 0 -> 2, 1 -> 2, 2 -> 0, 2 -> 3, 3 -> 3 输出:是 解释: <

计网之HTTP协议和Fiddler的使用

文章目录一. HTTP概述和fidder的使用1. 什么是HTTP2. 抓包工具fidder的使用2.1 注意事项2.2 fidder的使用二. HTTP协议格式1. HTTP请求格式1.1 基本格式1.2 认识URL1.3 方法2. 请求报头关键字段3. HTTP响应格式3.1 基本格式3.2 状态码一. HTTP概述和fidder的使用 1. 什么是HTT…

VueRouter路由模式解析

VueRouter路由模式解析 前端路由的实现方式主要有两种&#xff1a;hash模式和history模式。 hash模式 在window.location对象中有一个hash字段&#xff0c;可以获取地址栏中#字符及后边的所有字符。 hash也称作锚点&#xff0c;本身是用来做页面定位的&#xff0c;可以使对…

BGP联邦实验

实验目的&#xff1a; 实验拓扑&#xff1a; IP地址规划&#xff1a; AS2内部&#xff1a; 172.16.0.0/16 172.16.0.0/24---P2P网络 172.16.1.0/24----MA网络 172.16.1.0/29 172.16.1.8/29 172.16.1.16/29 172.16.1.24/29 172.16.1.32/29 172.16.1.40/29 172.16.2.0/24--…