【C++深度探索】继承机制详解(一)

avatar
作者
筋斗云
阅读量:0

hello hello~ ,这里是大耳朵土土垚~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
在这里插入图片描述

💥个人主页大耳朵土土垚的博客
💥 所属专栏C++入门至进阶
这里将会不定期更新有关C++的内容,希望大家多多点赞关注收藏💖💖

目录

1.继承的概念

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

2.继承定义

2.1定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

//基类或父类 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 _major;	//专业 }; 

在这里插入图片描述

2.2访问限定符

C++类的访问限定符用于控制类的成员(包括成员变量和成员函数)在类的外部的可访问性。C++中有以下三种访问限定符:

  • public: 公共访问限定符,任何地方都可以访问公共成员。可以在类的外部使用对象名和成员名直接访问公共成员。

  • private: 私有访问限定符,只有类内部的其他成员函数可以访问私有成员。类的外部无法直接访问私有成员,但可以通过公共成员函数间接访问私有成员。

  • protected: 保护访问限定符,只有类内部的其他成员函数和派生类的成员函数可以访问保护成员。类的外部无法直接访问保护成员,但可以通过公共成员函数或派生类的成员函数间接访问保护成员。

需要注意的是,访问限定符只在类的内部起作用,在类的外部没有直接的影响。同时,访问限定符可以用于类的成员变量和成员函数的声明中,默认情况下,成员变量和成员函数的访问限定符是private。

2.3继承方式

在这里插入图片描述
C++类的继承方式有以下几种:

  • 公有继承(public inheritance):使用关键字"public"表示的继承方式。在公有继承中,基类的公有成员和保护成员都可以在派生类中访问,私有成员不能在派生类中直接访问。
class Base { public:     // 公有成员 protected:     // 保护成员 private:     // 私有成员 };  class Derived : public Base {     // 公有继承 }; 
  • 保护继承(protected inheritance):使用关键字"protected"表示的继承方式。在保护继承中,基类的公有成员和保护成员在派生类中都变为保护成员私有成员不能在派生类中直接访问。
class Base { public:     // 公有成员 protected:     // 保护成员 private:     // 私有成员 };  class Derived : protected Base {     // 保护继承 }; 
  • 私有继承(private inheritance):使用关键字"private"表示的继承方式。在私有继承中,基类的公有成员和保护成员在派生类中都变为私有成员,私有成员不能在派生类中直接访问。
class Base { public:     // 公有成员 protected:     // 保护成员 private:     // 私有成员 };  class Derived : private Base {     // 私有继承 }; 

总结如下:

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

①基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private

这些继承方式可以根据具体的需求选择合适的方式

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

例如:

//基类或父类 class Person { public: 	void Print() 	{ 		cout << "name:" << _name << endl; 		cout << "age:" << _age << endl; 	}  //保护成员 protected: 	string _name = "tutu";// 姓名 	int _age = 20;  //年龄  //私有成员 private: 	string _tele = "123456";  };  //派生类或子类 class Student : public Person { public: 	void sPrint() 	{ 		Person::Print();	//可以使用父类的公有成员 	} protected: 	int _stuid; // 学号 	string _sname = _name;//可以访问父类的保护成员_name  	string _stele = _tele; //不可以访问父类的私有成员_tele }; 

结果如下:
在这里插入图片描述

上述父类Person中成员有三种访问限定分别是public、protected、private,而子类Student使用public继承父类,那么对于父类的公有成员在子类中的访问方式还是public,protected成员访问方式选择继承方式public和protected中较小的protected,同理父类的private成员继承到子类中也是选择private方式,在子类中不可访问

对于私有成员也是被继承到子类中,只是不可访问:

在这里插入图片描述

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

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

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

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

  • 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

如下图所示:

在这里插入图片描述

  • 基类对象不能赋值给派生类对象

例如下面代码:

//父类 class Person { protected: 	string _name; // 姓名 	string _sex;  //性别 	int _age; // 年龄 }; //子类 class Student : public Person { public: 	int _No; // 学号 }; void Test() { 	Student sobj; 	// 1.子类对象可以赋值给父类对象/指针/引用 	Person pobj = sobj; 	Person* pp = &sobj; 	Person& rp = sobj;  	//2.基类对象不能赋值给派生类对象 	sobj = pobj;//error } 

在这里插入图片描述

4.继承中的重定义(隐藏)

  • 在继承体系中基类和派生类都有独立的作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽对父类同名成员的直接访问,这种情况叫隐藏,也叫重定义。

当一个类继承另一个类时,它可以重定义继承的成员函数或者成员变量。
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

  • 如果要访问被隐藏的父类的同名成员,可以在子类成员函数中,使用 父类::父类成员来显示访问

注意在实际中在继承体系里面最好不要定义同名的成员。

例如:

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 //父类 class Person { protected: 	string _name = "胡土土"; // 姓名 	int _num = 1234; // 身份证号 };  //子类 class Student : public Person { public: 	void Print() 	{ 		cout << " 姓名:" << _name << endl; 		cout << " 身份证号:" << Person::_num << endl; 		cout << " 学号:" << _num << endl; 	} protected: 	int _num = 999; // 学号,与父类的_num重名构成隐藏 };  void Test() { 	Student s1; 	s1.Print(); }; 

结果如下:

在这里插入图片描述

我们发现当子类与父类有隐藏关系时,对于同名变量_num的调用,除非显示使用Person::_num 调用的是父类的成员变量,其他情况_num表示的都得子类中定义的变量,这是因为它们有不同的作用域,在子类中调用变量都是先从子类这个作用域中寻找。

再看下面的例子:

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; 	} };  void Test() { 	B b; 	b.fun(10); } 

这里 B中的fun和A中的fun不是构成重载,因为不是在同一作用域
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。

结果如下:

在这里插入图片描述
如果Test函数中:

void Test() { 	B b; 	b.fun();//这里没有给参数 } 

结果如下:

在这里插入图片描述

使用对象b调用fun()没有给参数,这样编译是不通过的,因为这样调用是调用的类B中的成员函数fun是需要传参的,如果要调用基类中的fun函数就必须显示调用,代码如下:

void Test() { 	B b; 	b.A::fun();//显示调用A中的fun函数 } 

结果如下:
在这里插入图片描述

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

在这里插入图片描述

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

例如如下父类:

//有如下Person父类 class Person { public: 	Person(const char* name = "tutu") 		: _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; // 姓名 };  

✨构造函数

  • 派生类的构造函数必须调用基类的默认构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数。
//基于上面Person的派生类Student class Student : public Person { protected: 	int _num; //学号 }; int main() { 	Student s; 	return 0; } 

结果如下:
在这里插入图片描述

在这里插入图片描述

我们发现对于父类中的成员它会自动调用父类Person的默认构造函数与析构函数

  • 如果父类Person没有默认构造函数,那么我们就需要在初始化列表里显示调用父类的构造函数
    例如:
    在这里插入图片描述

当我们将基类的默认构造函数中的缺省值"tutu",去掉,它就不再是默认构造函数,那么在创建子类Student对象时就不会自动调用默认构造函数,会保错,那么这时我们就需要在初始化列表里显示调用

代码如下:

class Student : public Person { public: 	Student(const char* name, int num) 		:Person(name)	//显示调用父类构造函数 		, _num(num) 	{}	 protected: 	int _num; //学号 };   int main() { 	Student s("tutu", 111);; 	return 0; } 

结果如下:
在这里插入图片描述

还有一种显示调用情况:

在这里插入图片描述

这种情况是不可取的,这是因为规定在初始化列表中是不可以使用父类的成员的

✨拷贝构造

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

默认生成拷贝构造一般情况下够用,只有当子类成员涉及深拷贝时就必须自己实现拷贝构造

这里也可以自己显示实现一下拷贝构造:

class Student : public Person { public: 	//构造函数 	Student(const char* name, int num) 		:Person(name)	//显示调用父类构造函数 		, _num(num) 	{}	  	//拷贝构造 	Student(const Student& st) 		:Person(st)	//利用前面学习的基类与派生类的赋值转换 		,_num(st._num) 	{} protected: 	int _num; //学号 }; 

注意这里Person(st)中调用Person中的拷贝构造实现赋值兼容

✨赋值运算符重载

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。

✨析构函数

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

如果自己显示写析构函数:

//析构函数 ~Student() { 	~Person();//这样写是错误的 } 

因为多态的原因,析构函数的名字会被统一处理为destructor(),所以这里调用会构成隐藏,会循环调用,所以要指定作用域:

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

但是我们发现Person的析构函数居然调用了两次:

在这里插入图片描述

这是因为析构函数具有特殊性,在子类析构函数调用完之后会自动调用父类的析构函数,所以即便是自己显示实现了子类的析构函数也不需要自己主动调用父类的析构函数

所以不需要自己主动调用父类的析构函数,否则会报错

其核心原因在于初始化时先构造父类再构造子类,而析构时先析构子类再析构父类,因为子类析构时是可能用到父类成员的,先父后子可能会出错

所以为了保证先析构子类再析构父类,编译器会在析构了子类后自动调用父类的析构函数

总结如下:

默认成员函数\子类成员内置成员自定义成员子类中的父类成员(整体)
默认生成的构造不做处理调用自定义类型的默认构造调用父类的默认构造
默认生成的拷贝构造值拷贝调用自定义类型的拷贝构造调用父类的拷贝构造
默认生成的赋值重载直接赋值调用自定义类型的赋值重载调用父类的赋值重载
默认生成的析构函数不做处理调用自定义类型的析构函数自动调用父类的析构函数

对于构造和析构:
派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构

6.结语

继承可以分为公有继承(public inheritance)、保护继承(protected inheritance)和私有继承(private inheritance)。继承在C++中的应用非常广泛,可以用于构建复杂的类层次结构,提供代码的复用性和灵活性。但是,在使用继承时也需要注意避免多层次的继承导致的类关系复杂性增加,以及合理设计基类和派生类之间的关系。以上就是今天的所以内容啦~ 完结撒花~ 🥳🎉🎉

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!