一、关于继承
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;
}
由于静态成员变量属于类,而不属于对象,因此无论多少个对象,都是公用一个静态成员变量。
到这里我们关于继承的知识点也就掌握的差不多了!