c++重要的特征之一便是代码重用,但是如果希望更进一步,就不能仅仅用拷贝代码或修改代码的方法,而是要做更多的工作。
在C中,这个问题未能得到很好的解决,而在c++中,我们可以通过类的方式来解决,我们通过创建一个新类来实现代码重用,而不是从头创建它们。这样,便可以使用别人已经创建好的并进行调试过的类。
在c++中,我们可以有两种方式来完成这项任务,第一种便是在新类中创建已经存在的类的对象,便可是想代码重用,即组合。
第二种便是本文章中将要着重提到的继承方式了,通俗的讲,在c++中的继承与我们一般理解的继承大同小异,就像每个人的父母创造了现在的我们,我们每个人都继承了父母的相貌,性格,体态等,在继承发生的同时跟根据不同情况可以会发生不同的改变,同时在日常生活中根据自己的经历又造就了与父母不同的特征,不同的是,我们可能只能继承父母的部分的特征,但c++中的继承则是继承了所继承的所有内容(无论私有,保护,公有)。
又或者手机的一代代更新,第一代手机只能打电话,发短信;第二代手机便能打电话,发短信,听音乐了,第二代的手机便是在将第一代手机的功能继承的前提下,从而有了新的功能。
以上是对继承通俗的理解,接下来便给出继承的定义。
(一)、继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继
承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
那么如何继承呢:在代码中和原来一样给出该类的名字,但在类的左括号的前边,加一个冒号和基类(被继承的类)的名字(对于多重继承,要给出多个基类名,它们之间用逗号隔开)
即为继承,便可知至少有两个类的存在,一个被继承的类称为基类(父类),另一个用来继承的类被称作派生类(子类)。如下:
class BaseClass //基类
{
public:
void Funtest()
{
cout<<"BaseClass"<<endl;
}
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class DerivedClass:public BaseClass //派生类
{
public:
void Funtest1()
{
cout<<"DerivedClass"<<endl;
}
private:
int _Dpri;
};
int main()
{
DerivedClass Test;
Test.Funtest();
Test.Funtest1();
system("pause");
return 0;
}
如上代码,BaseClass为基类,DerivedClass为派生类,观察运行结果:
可得派生类将基类中的函数继承下来并可调用。
同样:
cout<<sizeof(BaseClass)<<endl;
cout<<sizeof(DerivedClass)<<endl;
运行之后可得第一个为12,第二个为16;
说明基类中的数据成员被派生类继承。
所有在基类中的私有成员在派生类中仍旧是私有的,同样遵守保护机制,仅占有存储空间,只是不能直接的访问。
若在派生类中访问基类的私有成员,同样只能通过非私有接口来实现。
void Funtest1()
{
_pri = 10;
cout<<"DerivedClass"<<endl;
}
编译器会报错:
error C2248: “BaseClass::_pri”: 无法访问 private 成员(在“BaseClass”类中声明)
继承关系&访问限定符
同成员访问限定符一样,继承关系也有三种如上所示,公有,保护,私有继承。
三种继承关系与三种类成员访问限定符的关系可由一表说明:
如上表所示,对与公有(public)继承方式来说,在基类被派生类继承后,各成员属性不变。(默认情况class关键字继承方式为私有,struct为公有)
还是以上程序:
public:在类内类外都可直接访问;
protected:在类内直接访问,在派生类中直接访问,在外部不可直接访问;
privated:仅在类内可直接访问。
class BaseClass //基类
{
public:
void Funtest()
{
cout<<"BaseClass"<<endl;
}
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class DerivedClass:public BaseClass //派生类
{
public:
void Funtest1()
{
_pri = 10;
_pro = 20;
_pub = 30;
cout<<"DerivedClass"<<endl;
}
private:
int _Dpri;
};
int main()
{
DerivedClass Test;
Test._pri = 10;
Test._pro = 20;
Test._pub = 30;
system("pause");
return 0;
}
会发现基类中成员限定符未发生变化
对于protected继承方式来说,会发现,public成员会降级成为public属性
红线表示不能被访问(仍旧为以上代码)。
若为私有(protected)继承关系,则可发现公有,保护属性成员都降级为私有属性。
以上程序继承关系改为私有后,可得错误信息:
error C2247: “BaseClass::_pub”不可访问,因为“DerivedClass”使用“private”从“BaseClass”继承
继承基本总结:
1. 基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才
出现的。
2. public继承是一个接口继承,保持is-a(是)原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
is-a原则:对于继承有一些争论,继承应当只重载基类(并且不添加基类中没有的新成员函数)吗?这就意味着派生类与基类是完全相同的类型,因为它们有相同的接口。
结果是我们可以用派生类的对象代替基类的对象,被认为是纯代替,常常被称作代替原则,这是一种理想化的原则,基类与派生类的关系可以看作是“is-a(是)”的关系。
(摘录自c++编程思想第一卷P . 8)。
3. protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多
数的场景下使用的都是公有继承。私有继承以为这is-implemented-in-terms-of(是根据……实现的)。通常比组合(composition)更低级,但当一个派生类需要访问基类保护成员或
需要重定义基类的虚函数时它就是合理的。
4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能访问)。
5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
(二)、派生类的默认成员函数
与一般类相同,派生类同样会有六大默认成员函数。
那么在继承关系中定有至少两个类的情况下会有至少两个构造函数,那么就会有构造函数调用先后 的问题了。
接下来通过代码测试如下代码:
class BaseClass //基类
{
public:
BaseClass()
{
cout<<"BaseClass"<<endl;
}
~BaseClass()
{
cout<<"~BaseClass"<<endl;
}
void Funtest()
{
cout<<"BaseClass"<<endl;
}
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class DerivedClass:public BaseClass //派生类
{
public:
DerivedClass()
{
cout<<"DerivedClass"<<endl;
}
~DerivedClass()
{
cout<<"~DerivedClass"<<endl;
}
void Funtest1()
{
cout<<"DerivedClass"<<endl;
}
private:
int _Dpri;
};
void Funtest()
{
DerivedClass Test;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
由运行结果可以看出构造函数 析构函数的调用顺序
但真的是这样吗?
我们来看底层实现:
DerivedClass Test;
00042F4E lea ecx,[Test]
00042F51 call DerivedClass::DerivedClass (0414DDh)
call指令进入后:
DerivedClass()
......
call BaseClass::BaseClass (0414E2h)
可以看出编译器在进行对派生类进行实例化时先调用的为派生类的构造函数,再通过派生类的构造函数调用基类的构造函数,之所以会出现如上代码运行结果,是由于我们
知道在这种情况下相当于组合的形式了,即在新类中定义了原本类的对象,对类的初始化只能在构造函数中初始化列表中才能完成,所以待初始化完成后才会进入函数内部进行
操作,故有以上情况。
说明:
1、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。
2、基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。
3、基类定义了带有形参表构造函数,派生类就一定定义构造函数。
析构函数的调用则与其类的生命周期有关。
(三)、继承体系中的作用域
1. 在继承体系中基类和派生类是两个不同作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用 基类::基类成员 访问)--隐藏 --重定义
3. 注意在实际中在继承体系里面最好不要定义同名的成员。
如下代码:
class BaseClass //基类
{
public:
BaseClass()
:_pub(10)
{}
void Funtest()
{
cout<<"BaseClass_Funtest"<<endl;
}
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class DerivedClass:public BaseClass //派生类
{
public:
DerivedClass()
:_pub(20)
{}
void Funtest()
{
cout<<_pub<<endl;
<span style="white-space:pre"> </span>cout<<BaseClass::_pub<<endl;
cout<<"DerivedClass_Funtest"<<endl;
}
private:
int _Dpri;
public:
int _pub;
};
void Funtest()
{
DerivedClass Test;
Test.Funtest();
}
int main()
{
Funtest();
system("pause");
return 0;
}
程序运行结果:
可说明以上3点
在派生类中定义与基类名字相同的函数与数据成员,未报错,说明是两个不同的作用域。
继承与转换--赋值兼容规则--public继承
1. 子类对象可以赋值给父类对象(切割/切片)
2. 父类对象不能赋值给子类对象
3. 父类的指针/引用可以指向子类对象
4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)