IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 继承的详细介绍与理解,看了就懂 -> 正文阅读

[C++知识库]继承的详细介绍与理解,看了就懂

继承的概念及定义

继承也是面向对象的三大特性之一,是为了代码能够复用的重要手段,它使得我们在原有的类特性的基础上进行扩展,产生新的功能,这样的类我们成为派生类,而原有的类则叫做基类。继承就和我们以前的函数复用一样,只是这次复用的是属于设计层次上的。

定义格式

例如:

class Person {
public:
	string _name = "mingzi";
};

class Student :public Person{
public:
	void print() {
		cout << "name" << _name << endl;
		cout << "stid" << _stid << endl;
	}
private:
	int _stid = 202238;//学号
};
int main() {
	Student stdt;
	stdt.print();
}

此时Student和Person就成为了父子类关系,Person被称为基类,也叫做父类,Students被称为派生类,也叫子类,而public则叫做继承方法。上面的代码我们可以发现子类复用了父类的成员,并且我们在子类里可以使用父类的成员变量,这就是因为用的是public的继承方式
继承方式和访问限定符都是一样的,分别为public,protected,private三种

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

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

此时我们可以发现,不管是什么类型的成员还是继承方式,最后子类获得的都是范围最小的那一种即public>protected>private,protect叫做保护成员限定符,是因继承才出现的。简单来讲,private和不可见的区别是,派生类不可见是子类无法使用父类的private成员,而private是属于类内可以使用,但类外无法使用。而protected,父类的protected成员子类可以使用,但类外无法使用。
不过我们实际运用中一般都是public

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

//同上一份类
int main(){
	Person p;
	Student s;
	//public继承
	p=s;//父类=子类
	s=p;//不可以
	Person* ptr=&s;//指针
	Person& ref=s;//引用
	return 0;
}

我们把子类对象赋值给父类对象/指针/引用的行为叫做切割,天然行为,不存在类型转换(没有const临时变量)。形象的来讲因为子类中有全部的父类成员,把多余(属于自己子类)的部分给切除,就可以把子类内的成员依次给父类赋值了

继承中的作用域

基类和派生类都有独立的作用域,当子类和父类中有同名成员时,子类成员会隐藏父类成员,这种情况叫隐藏,也叫重定义

class Person {
public:
	string _name = "mingzi";
	int _stid=111;
};

class Student :public Person{
public:
	void print() {
		cout << "name" << _name << endl;
		cout << "stid" << _stid << endl;
		//若想打印父类的可以使用 Person::_stid
	}
private:
	int _stid = 202238;//学号
};
int main() {
	Student stdt;
	stdt.print();
}

此时将会打印202238,因为子类的成员函数会优先调用自己的成员变量,将父类的隐藏(注:尽量不要重名,但是在虚函数中又不一样了,后面多态会讲)

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

此时A类和B类的两个fun函数构成隐藏关系(只要函数名相同,不管参数怎么样,就是隐藏关系),继承中函数名相同就是隐藏值得注意的是,重载的条件是在同一个作用域中。

派生类的默认成员函数

子类的构造函数——我们不写,编译器默认生成,此时
1.继承的父类成员作为一个整体——调用父类的默认构造函数初始化
2.自己的自定义类型成员 ——调用它的默认构造函数
3.自己的内置类型成员 ——不处理(除非声明时给了缺省值)

子类的拷贝构造函数也是同理——我们不写,编译器默认生成,此时
1、继承的父类成员作为一个整体 ——调用父类的拷贝构造
2、自己的自定义类型成员 —— 调用它的拷贝构造
3、自己的内置类型成员 —— 值拷贝

子类的拷贝赋值函数也是同理——我们不写,编译器默认生成

子类析构函数 – 我们不写,编译器默认生成 ——此时
1、继承的父类成员作为一个整体 – 调用父类的析构函数
2、自己的自定义类型成员 – 调用它的析构函数
3、自己的内置类型成员 – 不处理
子类析构函数和父类析构函数构成隐藏关系
因为编译器会对析构函数名做特殊处理,所有类的析构函数名都会被处理成统一名字destructor()
。编译器为什么要这么做呢,多态会讲到
子类的析构函数在执行结束会后,会自动调用父类的析构函数

class Person
{
public:
	Person(const char* name = "peter")
	//Person(const char* name)特意不给默认构造函数,此时需要子类的构造函数来初始化
		: _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; // 姓名
	//int _age;
};
class Student : public Person
{
public:
	// 我们要自己实现子类构造函数
	// 要注意的父类成是作为一个整体,调用父类的构造函数进行初始化
	Student(const char* name)
		:Person(name)//不可以单独给_name进行赋值
		, _id(id)
		, _address(address)
	{}
	//当我们子写了拷贝构造函数
	//一般情况没必要写子类的拷贝构造,除非子类里的成员变量有深浅拷贝问题,才会需要
	Student(const Student& s)
		:_id(s._id)
		, _address(s._address)//?值得注意的是,这里address用的是自定义类型的拷贝
		//构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝
		, Person(s)//这里我们可以直接传子类对象,给父类引用,这里发生切片
	{}
	//?若子类的拷贝构造没去调用父类的拷贝构造(即没有Person(s)),拷贝构造也是构造函
	//数,构造函数规定,如果你不调用自定义类型,那会去调用它的默认构造,(和编译器生成的不同)
	//当我们子写了拷贝赋值函数
	Student& operator=(const Student& s){
		if (this != &s){
			_id = s._id;
			_address = s._address;
			Person::operator=(s); // 我们显示调用=的函数,?此时父子类都有=的重载,
			//构成隐藏关系,否则自己调自己了,所以我们需要指明类域,切片
		}
		return *this;
	}
	
	~Student(){
		//Person::~Person();Student和父类析构构成隐藏
		// 清理自己的资源
	} // 会自动调用父类的析构函数
private:
	int _id;
	string _address;
};
int main(){
	Student s1("张三", 1, "西安市");
	Student s2(s1);
	Student s3("张思", 2, "北京市");
	s1 = s3;  //此时会打印Person()
			  //        Person(const Person& p)
			  //        Person()
			  //        Person operator=(const Person& p)
			  //		~Person()
			  //        ~Person()
			  //        ~Person()   在有子类析构情况下,顺序父构造子构造子析构父析构
}

当父类的构造函数是无参或者给了全缺省的默认构造函数时,子类的构造函数也可以是默认构造函数(也可以不是),来调用父类的默认构造函数。但是当父类不是默认构造函数时(子类也不能使用默认构造函数),子类的构造函数必须对父类一个整体,进行初始化(如:Person(name),就像缺省值一样)
??声明的顺序,才是初始化顺序,所以子类的初始化列表里(列表里出现的顺序不重要,重要的是声明的顺序),无论父类Person在哪个位置,都是先初始化父类的

PS:这里比较啰嗦,代码里的注释没看懂的话可以看这。

拷贝构造:?值得注意的是,这里address用的是自定义类型的拷贝构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝

继承与友元

Display函数声明为友元,所以Display函数可以使用类里的成员变量,但对子类来说,友元不能继承,即基类友元不能访问子类私有和保护成员

class Student;
class Person{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person{
	friend void Display(const Person& p, const Student& s);
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);
}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,子类不会有这样的static的成员,并且我们可以用类名去访问静态成员(由该类所有对象共享),类名::变量名

?复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称整个继承关系为单继承
多继承:一个子类有两个或者以上父类时整个继承关系称为多继承
菱形继承:时多继承的一种特殊情况
在这里插入图片描述

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;//类中产生两个A类的a成员变量
};
int main(){
	D x;
	//x._a=0;//此时会报错,因为这里产生了二义性
	x.A::_a=0
	x.B::_a=0;//这样可以暂时解决问题,需要显示指定访问

那这种情况下我们应该如何去解决这样的问题呢?
首先我们知道了D类中用拥有两个_a的成员变量,那么此时我们可以通过调试,使用内存可以看到他们的数据,首先我们先取d的地址
在这里插入图片描述
我们可以看到d内存和监视的地址是相同的
在这里插入图片描述
我们可以发现在B类和C类空间中都各有一份_a成员
此时,我们可以通过virtual,虚继承去解决二义性和数据冗余

//...略
class B : virtual public A
class C : virtual public A
class D : virtual public B, public C
//..略
int main(){
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	d._a = 0;
}

此时我们在到内存中查看

在这里插入图片描述
这时候我们可以发现BC空间中没有了_a的数据,在D类的空间中出现了2,但BC中产生了两个地址

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

我们进入到两个地址时,发现了14和0c两个数据
那么这是什么呢?
实际上这里两个值叫做偏移量,0x14=20,0x0c=12,这时候我们可以进行计算,D空间地址-B空间地址=20,D空间地址-C空间地址=12,所以我们可以通过存储偏移量,这样我们就可以只存1份_a了。
上面两个表叫做虚基表,我们通过B和C的两个指针指向一张表,指针叫做虚基表指针
但实际上因为我们需要格外增加指针寻找变量,所以效率降低了,更复杂

B b=d;
B* p=&d;
B& r=&d;//这样也可以获取到_a

总结

在多继承中我们可以感受到C++的复杂性,所以我们一般不建议设计出多继承,一定不要有菱形继承
我们应该优先使用组合,而不是类继承,继承中基类的内部细节对子类可见,继承一定成都破坏了基类的封装,使得基类和派生类耦合度很高
所以我们尽量使用组合,降低耦合度,组合被叫做黑箱复用,内部不可见。而继承叫白箱复用
最后感谢各位看到这里噢!!喜欢的可以点个赞噢!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:14:26  更:2022-03-12 17:15:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:05:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码