【C++】深入浅出面向对象三大特性之继承


继承是面向对象程序设计的一个重要特性。可以说,如果没有掌握继承,就等于没有掌握类和对象的精华,就是没有掌握面向对象横须设计的真谛。 继承可以在已有的类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或登记,其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。

什么是继承?为什么要引入继承?

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,就是利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。使用继承,可以在一定程度上忽略相似类型的区别,而用统一的方式使用它们的对象。 继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。面向过程中的复用都是函数复用,继承是类设计层次的复用。C++中继承引入的最终目的就是为了实现代码的复用。

继承的基本性质

①声明一个派生类的一般格式:

class 派生类名:[继承方式] 基类名{
  派生类新增的数据成员和成员函数
}

说明:"基类名"是一个已经声明的类的名称,"派生类名"是继承原有类的特性而生成的新类的名称。"继承方式"规定了如何访问从基类继承的成员,关键字private、protected、public。如果不显示的给出继承方式,系统默认为私有继承(private)。

②继承方式: public(公有继承)、private(私有继承)、protected(保护继承)

公有继承:基类中公有成员和保护成员在派生类的访问权限不发生改变,基类中的私有成员在派生类中不可访问。

保护继承:基类中的公有成员和保护成员在派生类中都被修改为保护成员,基类中的私有成员在派生类中不可访问。

私有继承:基类中的公有成员和保护成员在派生类中被修改为私有成员,基类中的私有成员在派生类中不可访问。
在这里插入图片描述

③继承方式的总结

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

⒉public继承是一个接口继承, 保持is-a的原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。

⒊protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。私有继承以为这is-implemented-in-terms-of(是根据…实现的)。通常比组合更低级,但当一个派生类需要访问基类保护成员或需要重定义类的虚函数时它就是合理的。

⒋不管哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不可访问)。

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

⒍在实际运用中一般使用的都是public继承,极少场景下才会使用protected和private继承。

④派生类的对象模型
在这里插入图片描述

派生类构造函数和析构函数的调用顺序

通常情况下,当创建派生类对象时,首先调用基类的构造函数,随后再调用派生类的构造函数。当撤销派生类对象时,则先调用派生类的析构函数,随后在调用基类的析构函数。运用一个简单的例子加以说明:

①单层派生中构造函数与析构函数的调用顺序

#include <iostream>
#include <Windows.h>
using namespace std;
class Test1
{
public:
	Test1()//基类构造函数
	{
		cout<<"Constructing base class"<<endl;
	}
	~Test1()//基类析构函数
	{
		cout<<"Destructing base class"<<endl;
	}
};
class Test2:public Test1
{
public:
	Test2()//派生类构造函数
	{
		cout<<"Constructing derived class"<<endl;
	}
	~Test2()//派生类析构函数
	{
		cout<<"Destructing derived class"<<endl;
	}
};
int main()
{
	Test2 A;
	system("pause");
	return 0;
}

结果如图

 //结果
Constructing base class
Constructing derived class
Destructing derived class
Destructing base class

·结论:在定义对象时,系统会自动调用构造函数,而撤销对象时则会调用析构函数。构造函数的调用严格遵照先调用基类的构造函数,在调用派生类的构造函数的顺序。析构函数的调用顺序与其恰好相反,先调用派生类的析构函数,在调用基类的析构函数。

②多层派生类的构造和析构函数调用顺序

#include <iostream>
using namespace std;
class Test1
{
public:
	Test1()//基类构造函数
	{
		cout<<"1:Constructing base class"<<endl;
	}
	~Test1()//基类析构函数
	{
		cout<<"1:Destructing base class"<<endl;
	}
};
class Test2:public Test1
{
public:
	Test2()//派生类构造函数
	{
		cout<<"2:Constructing derived class"<<endl;
	}
	~Test2()//派生类析构函数
	{
		cout<<"2:Destructing derived class"<<endl;
	}
};
class Test3:public Test2
{
public:
	Test3()
	{
		cout<<"3:Constructing derived class"<<endl;
	}
	~Test3()
	{
		cout<<"3:Destructing derived class"<<endl;
	}
};
int main()
{
	Test3 A;
	return 0;
}
//结果
1:Constructing base class
2:Constructing derived class
3:Constructing derived class
3:Destructing derived class
2:Destructing derived class
1:Destructing base class

通过上面的例子可以清楚地看到多层派生时的构造函数和析构函数依次被调用的顺序。在多层派生的情况下需要从基类层开始执行构造函数撤销对象时,则从派生层撤销对象。

说明:
派生类的拷贝构造必须先调用基类的拷贝构造来完成基类的拷贝初始化
派生类的operator=必须要调用基类的operator=完成基类的赋值

同名隐藏机制

继承体系中的作用域:①在继承体系中基类和派生类是两个不同的作用域子类和父类中有同名成员子类成员将屏蔽父类对成员的直接访问(在子类成员中可使用基类 :: 基类成员访问)。③注意在实际中在继承体系里面最好不要定义同名的成员。比如以下程序中,当子类与父类有同名成员时,通过类名+作用域表示符进行访问即可:

class Person 
{
public:     
	Person( const char * name = "" , int id = 0) 
		: _name(name ), _num( id) 
	{}
protected: 
	string _name;          // 姓名   
	int _num;              // 身份证号 
};
class Student: public Person
{
public :     
	Student(const char * name,  int id, int stuNum) 
		: Person(name , id ), _num(stuNum ) 
	{}
	void DisplayNum()
	{
		cout<<" 身份证号: "<<Person :: _num<< endl ;
		cout<<" 学号"<< _num << endl ;   
	}
protected :  
	int _num ;              // 学号 
};

注意:当基类与派生类成员函数同名时,即使参数列表不同,依旧不会构成重载,因为两个成员函数明显处于不同的作用域当中。

赋值兼容规则
  • 派生类对象可以直接赋值给基类的指针、引用和对象。这种做法叫切片或者切割,意思是将继承基类的那部分切割出来赋值给父类。但是基类的对象是不能赋值给子类的对象。
  • 基类的指针可以通过强制类型转化赋值给派生类的指针。但是当基类的指针指向派生类的对象才是安全的。

总结:

派生类对象可以赋值给基类的对象、引用、指针 * , 基类对象不能赋值给派生类的对象
基类的指针可以赋值给派生类的指针(当这个指针指向派生类对象时时安全的)
基类的指针(指向基类的对象)赋值给派生类指针时,会存在越界访问的问题

①子类对象可以赋值给父类对象(切割/切片)

②父类对象不能赋值给子类对象

③父类的指针/引用可以指向子类对象

④子类的指针/引用不能指向父类对象(但可以通过强制类型转换完成)
class A
{};
class B:public A
{};
main()
{
	A _a,
	A* _pa;
	B _b;
	B* _pb;
	_a = _b//子类对象赋值给父类对象
	_pa = &_b;//父类对象的指针指向子类对象
	A& _aa = _b;//父类对象的引用指向子类对象
	_pb = (A*)&_a;//子类对象的指针指向父类对象
}
友元关系与继承
#include <iostream>
using namespace std;
class Person
{
	friend void disp(Person& p,Student& s);
protected:
	string _name;//姓名
};
class Student:public Person
{
protected:
	int _stuNum;//学号
};
void disp(Person& p,Student& s)
{
	cout<<p._name<<endl;
	cout<<s._name<<endl;
	cout<<s._stuNum<<endl;
}
void FunTest()
{
	Person p;
	Student s;
	disp(p,s);
}
int main()
{
	FunTest();
	return 0;
}

在这里插入图片描述
友元关系不能继承,也就是说基类友元不能访问子类的私有和保护成员。很重要的一个原因就是,友元不是类的成员函数,无法继承于派生类。

继承与静态成员
#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;      // 研究科目 
};
void TestPerson1()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout<<"人数:"<<Person::_count<<endl;  
	Student ::_count = 0;    
	cout<<"人数:"<<Person::_count<<endl; 
}
int main()
{
	TestPerson1();
	return 0;
}

在这里插入图片描述

打印结果不难看出,定义派生类对象,其对象的静态成员变量也会相应增加子类也可以修改静态成员变量。基类定义的static成员,则整个继承体系中只有一个这样的成员。无论派生多少个子类,都只有一个static成员,即静态成员为所有类成员所共享。

单继承特性

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

在这里插入图片描述

多继承特性

一个子类有两个或以上直接父类时称其为多继承。
在这里插入图片描述

菱形继承特性

·菱形继承也称为钻石继承,一个基类有两个派生类来继承,然后派生类的派生类继承基类的两个子类。无疑,菱形继承的引入会造成严重的二义性以及数据冗余的问题。

在这里插入图片描述

虚继承的引入

虚继承的引入无疑是为了解决菱形继承带来的二义性和数据冗余的问题。

①虚继承解决了在菱形继承体系里面子类对象包含多份父类对象数据冗余空间浪费的问题。

②虚继承体系看起来很复杂,在实际应用中我们通常不会定义如此复杂的继承体系。因为使用虚继承解决数据冗余问题的同时也带来了性能上的损耗。

下面我们通过一个例子来说明一下虚基类的具体作用:

#include <iostream>
#include <Windows.h>
using namespace std;
class Base
{
public:
	Base()//基类
	{
		a = 1;
		cout<<"Base a = "<<a<<endl;
	}
protected:
	int a;
};
class Base1:virtual public Base//声明Base是Base1的虚基类
{
public:
	Base1()
	{
		a = a + 1;
		cout<<"Base1 a = "<<a<<endl;
	}
};
class Base2:virtual public Base
{
public:
	Base2()
	{
		a = a + 2;
		cout<<"Base2 a = "<<a<<endl;
	}
};
class Dervice:public Base1,public Base2
{
public:
	Dervice()
	{
		cout<<"Dervice a = "<<a<<endl;
	}
};
int main()
{
	Dervice test;
	system("pause");
	return 0;
}

在这里插入图片描述
由打印结果不难看出,从基类Base派生出Base1和Base2时,使用关键字virtual,把类Base声明为Base1和Base2的虚基类。这样一来,从类Base1和类Base2派生来的Dervice只会继承Base一次,也就是说,基类Base的成员a只保留了一份。

下面再来看一个程序,虚基类的引用究竟发生了哪些变化?

#include <Windows.h>
using namespace std;
#pragma pack(8)
class A
{};
class B :virtual public A
{
public:
	int _b;
	B()
		:_b(0xffffffff)
	{}
};
int main()
{
	B _b1;
	system("pause");
	return 0;
}

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

最后我用一个例子来求证一下多虚继承的对象模型以及所占字节大小:

#include <iostream>
#include <Windows.h>
using namespace std;
class A
{
public:
     int _a;
};

class B1 :virtual public A
{
public :
     int _b1;
};

class B2:virtual public A
{
public:
     int _b2;
};

class C:public B1, public B2
{
public :
     int _c;
};
int main()
{
	C obj;
	cout<<sizeof(C)<<endl;
	obj._a = 0xaaaaaaaa;
	obj._b1 = 0xbbbbbbbb;
	obj._b2 = 0xeeeeeeee;
	obj._c = 0xffffffff;
	system("pause");
	return 0;
}

在这里插入图片描述
可以看出B1,B2是通过两个指针找到的,这两个指针叫虚基表指针,这两个指针指向的表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到基类,这就是虚拟继承的原理

为什么B1和B2一定要找到自己的基类呢?
当子类对象赋值给父类对象时,会进行切片,这是需要用到基类对象模型。

注:C++的语法太过复杂可以体现在多继承上,多继承可以算上是C++的缺陷之1,它们的底层实现很复杂,一般不建议设计出多继承。

虚继承的注意事项

①即使基类是虚基类,也能通过基类的指针或引用操作派生类的对象。

②虚继承只是解决了一个派生类对象中存在同一个基类的多份拷贝的问题,并没有解决多个基类存在同名成员的二义性问题。

③在虚继承中,虚基类是有最低层的派生类初始化的。

④含有虚基类的对象的构造函数一般的多重继承的构造函数稍微有点区别先初始化虚基类子对象(最低层派生类负责),然后按派生列表中的顺序依次对直接基类(非虚基类)初始化。

析构函数的调用顺序与构造函数调用顺序恰好相反。

组合和继承

公有继承时一种is-a关系,它表示派生类对象都是一个基类对象。而组合是一种has-a的关系,它表示假设A组合了B,那么每一个A对象中都包含一个B对象。例如:车和宝马构成is-a关系,而车和轮胎构成has-a关系。

继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种代码复用选择新的更复杂的功能可以通过组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

实际中多去用组合,因为组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。

优先使用组合,耦合度低,内聚高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农印象

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值