【C++】继承

继承

  • 继承的概念
  • 继承的定义
    • 定义格式
    • 继承关系和访问限定符
    • 基类成员在派生类中的访问方式
    • 基类和派生类都为类模板
  • 基类和派生类对象赋值兼容转换
  • 继承的作用域
    • 隐藏(重点)
  • 继承和友元
  • 继承和静态成员
  • 派生类的默认成员函数
  • 菱形继承
    • 单继承
    • 多继承
  • 菱形虚拟继承
  • 组合
  • 面试题(重点)

铁汁们,今天给大家分享一篇继承,来吧,开造⛳️

继承的概念

  1. 继承是面向对象的三大特性之一,是使代码可以复用的重要手段,是类层次的复用,它描述的是类与类之间的关系。函数模板是函数层次的复用。

  2. 父类和子类:在原有类的基础上进行扩展,增加新的功能,就创建出了新的类,这个新的类就被称为子类或者派生类,而原来的类就被称为父类或者基类。

  3. 子类继承父类,如同孩子继承了父亲的财产,即:父类中的成员(成员变量、成员函数)都变成了子类的一部分,可以访问父类中的成员,综上可知子类由两部分组成,一部分是自己独有的数据和方法,另一部分是父类的数据和方法。

  4. 若在类外访问数据和方法时,先子后父,默认先去子类独有的数据和方法中寻找(在子类成员函数中this为子类对象的地址),若没找到,再去父类的数据和方法中寻找(在父类成员函数中this为父类对象的地址)。

继承的定义

定义格式

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class Person {
public:
	void print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}

	string _work = "学生";

protected:
	string _name = "zzx";
	int _age = 18;
};

class Student : public Person
{
public:
	void fun()
	{
		cout << _id << endl;
	}

protected:
	int _id = 2;
};

int main()
{
	Student s;
	s.fun();
	s.print();

	s._work = "老师";
	cout << s._work << endl;
	 
	return 0;
}

继承关系和访问限定符

  • 访问限定符限制的是类中成员在类外是否能被直接访问到,public访问限定符,表示在类外可以直接被访问,通过 类名 ::成员 或者 对象. 成员方式进行访问,protected、private访问限定符,表示在类外不可以直接被访问,可以间接通过公有成员进行访问。protected和private访问限定符在访问上无区别,两者在继承上有区别。

基类成员在派生类中的访问方式

  • 基类中的private成员无论以哪种方式继承都是不可见的。不可见不是指基类私有成员不存在子类中,相反基类私有成员已经被继承到子类对象中了,只是语法上限制了基类私有成员不论是在类外还是在子类中都不能直接去访问它。

  • 若想基类中的成员在类外不能访问,只在子类中能够访问,就将其定义为protected。

  • 基类中其他成员在派生类的访问方式==min(基类成员访问限定符,继承方式), public>protected>private。

  • 若不显示写继承方式,class类默认继承方式为private,struct类默认继承方式为public,一般要求要显示写出继承方式。

  • 在工程中,一般采用public继承方式,基类成员定义为protected或public,反之会造成扩展维护性不高。

基类和派生类都为类模板

//继承中的类模板
template<class T2>
class Person  //父类模板
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
	}
protected:
	 T2 _name = "peter"; 
};

template<class T1>  //子类模板
class Student : public Person<string>  //显示实例化父类
{
protected:
	T1 _stuid; 
};

int main()
{
	Student<int> s; //显示实例化子类
	s.Print();

	return 0;
}

  • 父类和子类都为类模板,子类显示实例化再创建子类对象时进行实例华,父类在继承方式后显示实例化(父类名<数据类型>。与普通类模板使用方法相同。

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

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class Person {
public:
	void print()
	{
		cout << _age << endl;
	}

protected:
	int _age = 18;
};

struct Student : Person
{
public:
	void fun()
	{
		cout <<_age << endl;
	}

protected:
	int _id = 2;
};

int main()
{  //只能子类对象赋值给父类,把子类中父类的那一部分切下来进行赋值
	Student s;  //派生类对象
	Person d1 = s;  //赋值
	Person& d2 = s;  //引用
	Person* d3 = &s;  //指针
}

  • 把派生类对象可以赋值给基类的对象或基类的指针或者基类的引用,称为切片、切割、赋值兼容转换,即:把子类中父类的那一部分切下来赋值给基类。对于基类的指针,它是指向子类中父类那一部分,对于基类的引用,他是子类中父类那一部分的别名。

💡Tips : 基类对象不能赋值给子类对象。

继承的作用域

在继承体系中,基类和派生类中有独立的作用域。

隐藏(重点)

  1. 隐藏:也称为重定义,子类会隐藏父类的同名成员(子类成员将会屏蔽父类对同名成员的直接访问),两个成员分别在基类和派生类的作用域。优先访问子类的同名成员,若想访问父类的同名成员,可以使用 基类 ::基类成员 显示访问。

  2. 若子类和父类中有同名的成员函数,只要成员函数名相同,就构成了隐藏关系,返回值和参数可以不相同。

  3. 子类和父类的析构函数构成了隐藏关系。由于后面多态的原因,析构函数被特殊处理,函数名都会被处理为destructor。

  4. 在继承体系中,一般来说子类和父类不要出现同名成员。

  5. 函数重载是参数不同,函数相同,但构成重载的函数必须要在同一作用域中。operator运算符为函数名。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person {
public:
	void print()
	{
		cout << _age << endl;
	}

protected:
	int _age = 18;  //同名成员变量
};

struct Student : Person
{
public:
	void fun()
	{
		cout <<_age << endl; //隐藏,优先访问子类中的同名成员变量
	}

protected:
	int _age = 10; //同名成员变量
};

int main()
{  
	Student s;

	s.fun();
}

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

//以下两个fun函数构成了隐藏关系

class Person {
public:
	void fun()
	{
		cout << "父类的fun()" << endl;
	}
};

struct Student : Person
{
public:
	void fun(int n) 
	{
		cout << "子类的fun()" << endl;  
	}
};

int main()
{  
	Student s;
	//s.fun();  错误-》说明两fun()是有关系的,子类继承了父类,按理上是可以调动父类中的fun(),但隐藏了
    s.Person::fun();  //显示访问父类的同名函数

	return 0;
}

继承和友元

友元不能被继承,即:基类的友元不能访问派生类的私有成员和保护成员。若想访问,则要将其变为派生类的友元。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Student;  //注意,这个忘记书写,友元函数就不能访问了
class Person {
public:
	friend void Display(const Person& p , const Student& s); //

	void print()
	{
		cout << _age << endl;
	}

protected:
	int _age = 18;
};

struct Student : Person
{
public:
	friend void Display(const Person& p, const Student& s);//

	void fun(int n) 
	{
		cout << _id << endl;  
	}

protected:
	int _id = 0;
};

void Display(const Person& p, const Student& s)
{
	cout << p._age << endl;
	cout << s._id << endl;  
}

int main()
{  
	Student s;
	Person p;
	Display(p, s);

	return 0;
}

继承和静态成员

  • 若在基类中定义了静态成员,在继承体系中,无论派生出多少个子类,这个静态成员始终仅存在一份,因为类中的静态成员作用域存在于静态区,它不属于任何一个类,只是受类域和访问限定符的限制。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

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; // 研究科目
};


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

	return 0;
}

派生类的默认成员函数

  1. 派生类成员:基类部分+自己独有(内置类型+自定义类型)。

  2. 构造函数中初始化列表初始化的顺序与其在初始化列表中的顺序无关,只与声明的顺序有关。对于多继承,子类继承父类的顺序就是声明的顺序,与父类定义顺序无关。

  3. 派生类的默认成员函数,规则和之前一样,对自定义类型它去调用自己的默认成员函数,对内置类型不做处理,就是多了父类这部分,父类这部分调用父类对应的默认成员函数。

  4. 派生类的构造函数必须调用父类的默认构造函数来对父类那一部分完成初始化。若父类没有默认构造函数,必需在子类构造函数的初始化列表中显示传参调用。

  5. 派生类对象初始化是先调用父类构造,在调用子类构造(先父后子),与它们在初始化列表的顺序无关。

  6. 派生类对象销毁是先调用子类构造,在调用父类构造(先子后父)。原因:若先父后子,父类中的资源被清理掉了,若子类析构在访问父类,会造成野指针(指向已经被释放的空间),有一定得安全隐患。

  7. 为了保证派生类对象先清理父类的成员,在清理子类的成员的顺序,父类的析构会在子类析构调用完后由编译器自动调用。

  8. 父类和子类的析构构成 隐藏 关系,因为后面多态的原因,析构函数被特殊处理,函数名都被处理为destructor。

  9. 父类和子类的operator=函数构成隐藏,需要指定类域访问Person::operator=。此处需要将子类中父类那部切分给父类,采用切割,Person& p=s。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class Person{  //父类
public:
	Person(const char* name  = "zzx")  //默认构造函数
		:_name(name) 
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p) //拷贝构造函数
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p) //赋值运算符重载
	{
		if (this != &p)
		{
			_name = p._name; 

			cout << "Person& operator=(Person& p)" << endl;
		}

		return *this;
	}

	~Person() //析构函数
	{
		cout << "Person" << endl;
	}

protected:
	string _name; //自定义类型,初始化列表会去调用它自己的默认成员函数
};


/*派生类的默认成员函数,规则和之前一样,就是多了父类这部分,父类这部分调用父类对应的默认成员函数
* 派生类:父类部分+自己独有(内置类型+自定义类型),父类调用父类的默认成员函数,
           自己独有的-》自定义类型调用自己的默认成员函数,内置类型不做处理*/
class Student : public Person //子类
{
public:
	//构造:先父后子; 析构:先子后父
	Student(const char* str, int id) //构造函数,既要初始化父类部分,自己独有的部分也要初始化
		:Person(str) //父类部分调用父类的构造函数 
		,_id(id) 
	{ 
		/*将Person(str)写在函数体内,没有写到初始化列表,会出现编译错误(形参重定义), 
		因为一个对象,只能调用一次构造、析构函数,初始化去调用默认构造函数,此处也调用,进行了两次调用*/

		cout << "Student(const char* str)" << endl;
	}

	Student(const Student& s) //拷贝构造
		:Person(s)   //同上,拷贝构造会创建新的对象,调用构造函数,写在函数体内,也会造成形参重定义
	{    //此处需要将子类中父类那部分给父类,采用切片,Person& p=s
		_id= s._id;

		cout << "Student(const Student& s)" << endl;
	}

	Student& operator=(Student& s) //赋值运算符重载
	{
		if (this != &s)
		{
			Person::operator=(s); //operator=为函数名,父类和子类的operator=函数构成隐藏,需要指定类域访问Person::
			_id = s._id;

			cout << "Student& operator=(Student& s)" << endl;
		}

		return *this;
	}

	/*父类和子类的析构构成 隐藏 关系,因为后面多态的原因,析构函数被特殊处理,函数名都被处理为destructor;
	* 必须要先析构子类,在析构父类,原因:若先父后子,父类中的资源被清理掉了,若子类在访问父类,会造成野指针(指向已经被释放的空间);
	* 为了保证先子后父,父类的析构会在子类析构后编译器自动调用;*/
	~Student()  //析构函数
	{
		cout << "~Student()" << endl;
	}
private:
	int _id;
};

int main()
{
	Student s1("Peter", 18);

	Student s2(s1);

	Student s3("zhangsan", 10);
	s2 = s3;

	return 0;
}

菱形继承

单继承

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

多继承

定义:一个子类有两个或两个以上直接的父类时称这个继承关系为多继承。

💡菱形继承:菱形继承是多继承的一种特殊情况。

Tips:注意:菱形继承会有数据冗余以及二义性问题。

  • Assistant继承了Student、Teacher,而Student继承了Person,Teacher继承了Person,就会导致Assistant中有两份Person,若要访问Person中的成员,你就不知道是访问Student中Person的成员,还是访问Teacher中Person的成员造成了数据冗余以及二义性的问题。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class Person 
{
public:
	string _name;
	int _age;
};

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

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

class Assistance : public Student, public Teacher
{
protected:
	int _course;
};


int main()
{
	Assistance dd;
	dd._name = "zhangsan";  
	dd._age = 10;

	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;
};
  • 虚拟继承可以解决菱形继承中数据冗余以及二义性的问题,只需要找到对象中存在多份相同类,把该类作为直接父类的子类中的继承方式前加上 virtual关键字。

  • Tips :注意:不要再其他地方使用虚拟继承。一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  • 菱形虚拟继承将对象中存多份相同类中的成员放到成员组的最下面,是通过虚拟继承指针,指向的虚拟表中存放的偏移量,就可以找到对象中存多份相同类中的成员。

组合

  1. public继承是一种is-a关系,即:特殊情况下派生类对象就是一个基类对象。组合是一种has-a关系,派生类对象中一定有一个基类对象。

  2. 继承:是白箱复用,”白箱”是内部细节是可见的,即:基类的内部细节对派生类是可见的。继承在一定程度上破坏了对基类的封装,即:基类的改变,有可能会影响派生类的改变。继承的耦合度高,派生类和基类的依赖关系很强。

  3. 组合:是使代码可以复用的重要手段,通过组装或者组合对象来实现新的功能。是黑箱复用,”黑箱”是内部细节是不可见的,即:对象的内部细节是不可见的。组合的耦合度高,组合类之间没有很强的依赖关系。

  4. 类之间的关系对于继承和组合,都能使用的情况下,优先使用组合。组合的耦合度高,可维护性强。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class A
{
public:
	string s = "zzx";

private:
	int age = 18;
};

class B 
{
public:
	void fun()
	{
		cout << a.s << endl; //B中只能访问A中public部分
	}

private:
	A a; //B组合了A (组合 -》高内聚,低耦合,"黑箱"复用
};



int main()
{
	B b;
	b.fun();

	return 0;
}

面试题(重点)

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

  • 答:菱形继承是多继承的一种特殊情况。 菱形继承会有数据冗余和二义性问题。通过指定类域解决数据二义性,通过虚继承解决数据冗余问 题。

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

  • 答:菱形虚拟继承是解决菱形继承中的数据冗余和二义性问题。
    将对象中相同类放到对象组的最下面公共区域中,从而解决了数据冗余。通过对象中的虚基表指针,指向的虚基表中的偏移量,来找到对象中公共类中的成员,从而解决了数据二义性。

3.继承和组合的区别?什么时候用继承?什么时候用组合?

  • 答:继承和组合都是实现代码复用的手段。继承是is-a关系,每个派生类对象都是一个基类对象。组合是has-a关系,每个派生类对象都有一个基类对象。继承耦合度高,派生类和基类依赖关系强,是黑箱复用。组合耦合度低,组合类之间的依赖关系弱,白箱复用。继承是隐式地获得父类的对象,而组合是显式地获得被包含类的对象。继承关系在编译期就已经决定,而组合关系在运行期决定。继承时,父类的所有方法和变量都被子类无条件继承,子类不能选择。而组合时,组合类可以调用外部类必须的方法。
    使用继承的场景:当子类需要无条件继承父类的所有成员时,类与类之间耦合度高,在编译期间就决定了类与类之间的关系。使用组合的场景:只需要获得被组合类的对象,无需获得父类的所有成员,类与类之间耦合度低,在运行期间间就决定了类与类之间的关系。

铁铁们,继承就到此结束啦,若博主有不好的地方,请指正,欢迎铁铁们留言,请动动你们的手给作者点个👍鼓励吧,你们的鼓励就是我的动力✨

版权声明:本文为博主作者:奶芙c原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/m0_74808907/article/details/137170145

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2024年4月10日
下一篇 2024年4月10日

相关推荐