C++三大特性--继承

51 篇文章 38 订阅

一、关于继承

1.概念:继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进                行扩展,增加功能。这样产生的类称为派生类。 (简单来说,通过继承我们可以将原来的数据类型定义 一个              新的数据类型,而这个新的数据类型既有原来数据中的成员,也可以增添新的数据成员。)

2.解释:(1)继承是类与类之间的关系,是一个简单直观的概念,与现实世界中的继承(如儿子继承父亲的财产)相                        似;

              (2)继承可以理解为一个类从另一个类获取方法(函数)和属性(成员变量)的过程。假如B类继承A类                           (一般我们称“A类”为“基类”或“类”,“B类”为“派生类”或“子类”),那么B具有A的方法和属性;

              (3)在本篇博客中,规定所有的Base为“基类”,Derived为“派生类”。

3.定义格式:


二、继承方式

1.公有继承(public)

class Base
{
public:
	Base(int a = 0, int b = 0, int c = 0)
		: _pub(a)
		, _pro(b)
		, _pri(c)
	{}
	int _pub = 1;
protected:
	int _pro = 2;
private:
	int _pri = 3;
};
class Derive :public Base
{
public:
	void display()
	{
		cout << "_pub=" << _pub << endl;
		cout << "_pro=" << _pro << endl;
		cout << "_pri=" << _pri << endl;//在public继承中,派生类访问基类的private成员时就会报错
	}
};
注意事项:

(1)基类的公有成员,派生类可以继承为自己的公有成员,在类内可以访问,类外也可以访问;

(2)基类的保护成员,派生类可以继承为自己的保护成员,在类内可以访问,类外不可以访问;

(3)基类的 私有成员,派生类不可以访问。

2.保护继承(protected)

注意事项:

(1)基类的公有成员,派生类可以继承为自己的保护成员,在类内可以访问,类外不可以访问;

(2)基类的保护成员,派生类可以继承为自己的保护成员,在类内可以访问,类外不可以访问;

(3)基类的私有成员,派生类同样不可以访问。

3.私有继承(private)

注意事项:

(1)基类的公有成员,派生类可以继承为自己的私有成员,在类内可以访问,类外不可以访问;

(2)基类的保护成员,派生类可以继承为自己的私有成员,在类内可以访问,类外不可以访问;

(3)基类的私有成员,派生类同样不可以访问。

4.三种继承方式的比较

简单来看,好像保护继承和私有继承的作用一样,但是当派生类再派生另外一个类时,情况会有所不同(如下图)。


5.几点说明:

(1)public继承是一个接口继承,保持is-a原则,每个基类可用的成员派生类也可用,因为每个派生类对象也都是一            个基类对象;

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

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

三、继承分类

1.单继承:一个子类只有一个父类

(1)定义格式

class  派生类名:继承方式 基类名
{
	派生类成员
};

(2)指向图

注意:派生类的成员主要由两部分组成:基类成员、自己增添的特有成员。(这是为什么指向图派生类的图看起来比基类的图大一些的原因了)

(3)代码实现

以公有继承为例:

#include<iostream>
using namespace std;
class Base
{
public:
	int a = 1;
};
class Derive :public Base
{
public:
	int b = 2;
};

int main()
{
	Derive d1;
	cout << sizeof(d1) << endl;
	return 0;
}

运行结果:

2.多继承:一个子类有两个或以上直接父类

(1)定义格式

class 派生类名:继承方式 基类名,继承方式 基类名
{
	派生类成员
};
(2)指向图

注意:在多继承中,靠近派生类的基类属于先声明的,因此数据在派生类中最先被保存下来。也可以这样来说,在多继承中,派生类的数据模型与继承顺序有很大的关系。所以,我们就需要注意多继承的空间分布。


(3)代码实现

#include<iostream>
using namespace std;
class Base1
{
public:
	int c = 3;
};
class Base2
{
public:
	int d = 4;
};
class Derive :public Base1, public Base2
{
public:
	int e = 5;
};
int main()
{
	Derive d2;
	cout << sizeof(d2) << endl;
	return 0;
}
运行结果:


3.菱形继承:多个派生类继承了同一个公共基类,而这些派生类又被同一个再次派生的类继承

(1)指向图


仅仅看上图,不难发现菱形继承比单继承和多继承要复杂很多,那么会不会引发一些问题呢?那我们来一起看一段代码的实现吧!

(2)代码实现

#include<iostream>
using namespace std;
class Base
{
protected:
	int _base;
public:
	void fun()
	{
		cout << "Base::fun" << endl;
	}
};
class C1 :public Base
{
protected:
	int _c1;
};
class C2 :public Base
{
protected:
	int _c2;
};
class Derive :public C1, public C2
{
private:
	int _d;
};
int main()
{
	Derive d;
	d.fun();//访问不明确,编译器会报错
	getchar();
	return 0;
}
从上述代码我们可以看出,Derived的对象模型里面保存了两份Base,当我们调用从Base里面继承的fun时,就会出现调用不明确的问题,与此同时造成了数据冗余和二义性的问题。

也就是说,菱形继承对象模型是这样的。


出现问题就需要我们去解决,那么,该怎么解决呢?

a.使用域限定我们所需要访问的函数

int main()
{
	Derive d;
	d.C1:fun();
	d.C2:fun();
	getchar();
}
这样的做法虽然可以,但是非常不方便,并且当程序很多的时候会造成我们的思维混乱。于是,C++给了我们一个新的解决方案——虚继承。

b.虚继承


从图中不难看出,虚继承就是给C1和C2在继承Base时加上关键字virtural,而虚继承具体是这样解决数据冗余和二义性问题的。


注意:图中解二义性时在vs下使用的是偏移量,而不是图中直接的指针指向。

从上图我们可以看出,C1和C2中不在保存Base中的内容,而是保存了一份偏移地址,然后将Base中的数据保存在一个公共位置处。这样既降低了数据的冗余问题,同时也能直接的通过d.fun()来调用Base中的fun函数。其代码实现如下:

#include<iostream>
using namespace std;
class Base
{
protected:
	int _base;
public:
	void fun()
	{
		cout << "Base::fun" << endl;
	}
};
class C1 :virtual public Base
{
protected:
	int _c1;
};
class C2 :virtual public Base
{
protected:
	int _c2;
};
class Derive :public C1, public C2
{
private:
	int _d;
};
int main()
{
	Derive d;
	d.fun();
	getchar();
	return 0;
}
运行结果:


注意:虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已,都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。

四、继承关系中的构造函数和析构函数

1.构造函数

(1)调用顺序


(2)继承中构造函数的特点

~1.如果两个类之间属于父子关系,那么在实例化子类时,先会调用基类的无参构造函数,后调用派生类的构造函数;

~2.在实例化派生类对象时,可指定派生类构造函数调用基类中任何构造函数。

注意:

:base()--指定派生类构造函数调用基类的无参构造函数
:base()--指定派生类构造函数调用基类带有一个参数的构造函数
(3)说明

<1>基类没有缺省构造函数,派生类必须在初始化列表中显示给出基类名和参数列表;

<2>基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数;

<3>基类定义了带有形参表构造函数,派生类就一定定义构造函数;

<4>基类没有定义带参数(没有缺省)的构造函数,派生类可以定义也可以不定义。

(4)代码实现

#include<iostream>
using namespace std;
class Base
{
public:
	Base()
	{
		cout <<" Base::Base "<< endl;
	}
};
class C :public Base
{
public:
	C()
	{
		cout << "C::C" << endl;
	}
};
int main()
{
	C c;
	return 0;
}
运行结果:


2.析构函数

(1)调用过程


(2)说明

在C++的继承中,析构函数必须是虚函数,否则会出现内存泄漏的问题。

(3)代码实现

#include<iostream>
using namespace std;
class Base
{
public:
	~Base()
	{
		cout <<" Base::~Base "<< endl;
	}
};
class C :public Base
{
public:
	~C()
	{
		cout << "C::~C" << endl;
	}
};
int main()
{
	Base*B = new C();
	delete B;
	return 0;
}
运行结果:


经过我们验证发现,确实出现了内存泄露的问题,我们该怎样防止出现这种情况呢?答案是加上关键字:virtual

代码如下:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual ~Base()
	{
		cout <<" Base::~Base "<< endl;
	}
};
class C :public Base
{
public:
	~C()
	{
		cout << "C::~C" << endl;
	}
};
int main()
{
	Base*B = new C();
	delete B;
	return 0;
}
运行结果:


这样就不会出现内存泄漏的问题啦!

五、继承体系中的作用域

几点说明

<1>在继承体系中,基类和派生类是两个不同的作用域,因此两者不能构成重载;

<2>派生类和基类有同名成员,派生类成员将屏蔽基类对成员的直接访问;(在派生类成员函数中,可以使用--

基类::基类成员来访问)。隐藏;

<3>在实际的继承体系中,最好不要定义同名的成员。

六、继承与转换--复制兼容规则--public继承

在这块我们需要注意一下几点要求,首先我们来看个图


1.派生类对象可以赋值给基类对象,而基类对象不能赋值给派生类对象(切片/切割);

根据我们所知道的继承的关系,以及从图中不难看出,派生类对象可以赋值给基类对象因为它有_D,赋值时将其中的_D切割掉,而基类对象不可以赋值给派生类对象,因为它没有_D。

2.基类的指针/引用可以指向派生类对象,而派生类的指针/引用不可以指向基类对象(但可以通过强制类型转换来完成)。

根据我们所知,基类的指针是指向4个字节大小的地址,因此完全可以访问派生类对象,而派生类是指向8个字节大小的地址,如果指向基类,则基类后面的4个字节的访问是越界的,所以不可以。

七、友元、静态成员与继承

1.友元

友元关系不能继承,也就是说基类友元不能访问派生类的私有和保护成员。(如下代码)

#include<iostream>
using namespace std;
class A
{
	int a;
public:
	A(int x = 0);
	{
		a = x;
	}
	friend class B;
};
class B
{
	int b;
public:
	void fun(A& b)
	{
		cout << b.a << endl;
	}
};
class C :public B
{
public:
	/*void fun2(A& b)
   {
	cout << b.a << endl;//派生类新增加的函数不能访问A,此句会报错
	}*/
};
void main()
{
	A a(5);
	C c;
	c.fun(a);//C是A的友元B的派生类,相当于B是基类,通过B的函数fun仍然可以访问
}
2.静态成员

(1)静态成员函数与非静态成员函数都可以被继承到派生类中;

(2)在继承中,如果将一个基类函数的实现改变了,则所有与该函数同名的基类版本将被隐藏(非静态相同);

(3)基类定义了static成员,则在整个继承体系中只有一个这样的成员,无论之后再派生出多少个子类,还是只有一个static成员。

#include<iostream>
using namespace std;
class A
{
public:
	static A*funtext();
protected:
	static A*p;
};
class B :public A
{
};
int main()
{
	A*p = new A;
	B *q = new B;
	return 0;
}
由于静态成员变量属于类,而不属于对象,因此无论多少个对象,都是公用一个静态成员变量。

到这里我们关于继承的知识点也就掌握的差不多了!大笑














  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值