C++_进阶:继承详解

🚀1. 继承的概念及定义

在这里插入图片描述

1.1继承的概念

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

以下是一个继承例子:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

class Student : public Person
{
protected:
	int _stuid; // 学号
};


int main()
{
	//实例化派生类
	Student s;
	//在派生类使用父类的函数
	s.Print();

}

1.2 继承定义

1.2.1定义格式

以下代码中,Person被称为基类,Student被称为派生类。
在这里插入图片描述

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

在基类不同访问限定符的成员,通过不同的方式继承之后,访问方式会有不同的变化。下列表格列举了不同成员不同的方式继承之后的属性。

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

备注(从表格中的总结):

  1. 基类private成员在派生类中三种继承后都是不可见。这里的不可见是指基类的private成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问就定义为protected(从中可以看出保护成员限定符是因继承才出现的)
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	//protected:继承类可访问,类外和private一样不可访问
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

class Student : public Person
{
	void fun()
	{
		//可访问基类的protected成员
		_name;
		_age;
	}
};
  1. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
  2. 使用关键字class默认的继承方式是private,使用struct默认的继承方式是public,不过最好显示的写出继承方式。
  3. 表格实际上不怎么需要看,在实际运用中一般使用都是public继承,而public继承的成员访问限定符不变。

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

  • 派生类对象赋值给基类对象,这个行为称为切片(在java叫做上转型)
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};
class Student : public Person
{
protected:
	int _stuid; // 学号
};

int main()
{
	Student t1;
	//1. Student关于Person的切片
	Person t2 = t1;
	//2. t3指向t1父类的部分
	Person* t3 = &t1;
	//3. t4是t1父类部分的别名
	Person& t3 = t1;
}

  • 基类对象不能赋值给派生类对象(沿用上面的Person和Student)
int main()
{
	Person test2;
	//👇报错: 基类对象不能赋值给派生类对e象
	Student test1 = test2;
}
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
int main()
{
	//父类指针指向子类对象 可以强转回子类指针
	Student t1;
	Person* t2 = &t1;
	Student* t3 = (Student*)t2;

	//父类指针指向父类对象,强转子类指针,会存在越界访问的问题
	Person a1;
	Person* a2 = &a1;
	Student* a3 = (Student*)a2;
}

!关于切片的细节内容:

int main()
{
	Student t1;
	//1. Student关于Person的切片
	Person t2 = t1;
	//2. 父类t3指向子类中t1父类的部分
	Person * t3 = &t1;
	//3. t4是t1父类部分的别名
	Person& t4 = t1;
}
  1. 切片并不是类型转换,Person t2 = t1并不是将t1类型转换为Person生成一个临时变量后,再赋值给t2。切片的过程并没有临时变量的产生,只是切下了子类中基类的成员,再赋值给t2
    在这里插入图片描述
  2. Person * t3 = &t1 的语法是子类对象赋给基类指针,t3指向的是t1中Student中Person的部分Person& t4 = t1就为t4是t1父类部分的别名
    在这里插入图片描述
    在这里插入图片描述

🚀3.继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. ! 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
  5. 同名函数的隐藏并不是函数重载,函数重载的前提是同一作用域
//B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏
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;
	}
};

🚀4.派生类的默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?

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

基类派生类关于构造和析构的顺序的例子

class Person
{
public:
	Person()
	{
		cout << "Person" << endl;
		call_Person();
	}
	void call_Person()
	{
		cout << "Person类行为" << endl;
	}
	~Person()
	{
		cout << "~Person" << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	Student()
	{
		cout << "Student" << endl;
		call_Student();
	}
	void call_Student()
	{
		cout << "Student类行为" << endl;
	}
	~Student()
	{
		cout << "~Student" << endl;
	}
protected:
	int _stuid; // 学号
};

int main()
{
	Student s1;
}

运行结果:
在这里插入图片描述
总得来说的顺序就是:
在这里插入图片描述

🚀5.继承与友元

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

class Person
{
public:
	friend Animal;
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

class Student : public Person
{
protected:
	int _stuid; // 学号
};

class Animal
{
public:
	void fun()
	{
		//报错 基类的友元类是不能访问子类私有和保护成员
		_stuid; 
	}
};

🚀6.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。

class Person
{
public:
	static int count;
};

int Person::count = 0;

class Student : public Person
{

};

int main()
{
	Person s1;
	Student s2;
	s1.count++;
	s2.count++;
	//输出2
	//说明s1 与 s2的count是同一个
	cout << Person::count;
}

🚀 7.菱形继承及菱形虚拟继承

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

在这里插入图片描述
多继承:该子类有两个或以上直接父类时称这个继承关系为多继承

class Student 

class Teacher

class Assessment: public Student, public Teacher

在这里插入图片描述

菱形继承:菱形继承是多继承的一种特殊情况
在这里插入图片描述

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题具体来说,在Assistant的对象中Person成员会有两份

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

虚拟继承所用到的关键词为 virtual

使用virtual的例子

class Person
{
public:
	string _name; // 姓名
};
// Student继承Person
class Student : virtual public Person
{
protected:
	int _num; //学号
};
// Teacher继承Person
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
// Assistant继承Student Teacher
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

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

总得来说,就是要在第一继承的子类使用虚拟继承

!虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助监视窗口观察对象成员的模型。

class A
{
public:
	int _a;
};
// B继承A
class B : virtual public A
{
public:
	int _b;
};
// C继承A
class C : virtual public A
{
public:
	int _c;
};
//D继承B 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;
}

在这里插入图片描述
内存具体的物理图解释
在这里插入图片描述

🚀 8.继承和组合

我们知道,关于类的代码的复用不只有继承,还有组合(将父类作为子类的成员变量):

class Student : public Person
{
protected:
	int _No; // 学号
};

//继承
class Student : public Person
{
protected:
	int _No; // 学号
};

//组合
class Student 
{
protected:
	Person t1;
	int _No; // 学号
};

什么时候继承,什么时候组合呢?
建议当A与B的关系为 A是B , 那么就用继承,当A与的关系为A包含B,那么就用组合。如果A与B的关系都满足以上两种情况,优先使用组合。

举个例子: A是老师,B是人,老师是人,更适合用继承。
A是车,B是轮胎,车包含轮胎,更适合用组合。

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值