继承性是面向对象程序设计最重要的特征、是类和对象的精华。
1、继承的概念:
一个新类从已有的类哪里获得其已有特性,这种特性称为继承。通过继承,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样的类,称为派生类。
2、继承的作用:
(1、)实现代码复用 (2、)为类实现多态奠定基础。
3、继承定义格式:
class 子类:继承权限 父类名称
例如:
class B
{
public :
int _b;
};
class C : public B //C类公有继承于B类
{
public :
int _c;
};
4、继承的方式:
继承时派生类中包含基类成员和自己增加的成员,这就产生了这两部分成员的关系和访问权限的问题。在派生类中访问基类成员是按不同的原则处理的。当继承的权限不同,其在基类中的访问权限也不相同。
下面这张表就说明了继承的访问权限
总结:
(1)基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
(2)public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类 对象也都是一个父类对象。
(3)protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分, 是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的 都是公有继承。私有继承以为这is-implemented-in-terms-of(是根据……实现的)。通常比 组合(composition)更低级,但当一个派生类需要访问基类保护成员或需要重定义基类的虚函 数时它就是合理的。
(4)不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存 在但是在子类中不可见(不能访问)。
(5)使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最 好显示的写出继承方式。
(6)在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
在继承时,派生类继承了基类的所有数据称成员和成员函数,我们可以用代码测试一下:
#include <iostream>
using namespace std ;
class B
{
public :
int _b1;
protected :
int _b2;
private :
int _b3;
};
class C : public B
{
public :
int _c;
};
int main()
{
cout << sizeof (C) << endl;
system("pause" );
return 0 ;
}
输出结果为:16
5、继承中派生类的存储方式:
既然我们知道了在继承时,派生类将继承基类所有的数据成员和成员函数,那么,派生类在内存中是如何存放的呢?它将所继承的基类的成员放在哪里呢??
我们可以定义一个派生类并在内存中查看它的存储方式,代码如下:
#include <iostream>
using namespace std ;
class B
{
public :
int _b1;
int _b2;
int _b3;
};
class C : public B
{
public :
int _c;
};
int main()
{
C c;
c._b1 = 1 ;
c._b2 = 2 ;
c._b3 = 3 ;
c._c = 4 ;
system("pause" );
return 0 ;
}
在内存中,我们可以取定义的派生类c的地址:
经过分析:我们发现,派生类在内存中应该是按如下方式存储:
即派生类在存放时,先将它所继承的基类成员存放,然后再存放它自己的成员。
6、派生类的默认成员函数:
在继承关系里面,在派生类中如果没有显示定义这六个成员函数,编译系统则会默认合成这六个默认 的成员函数
继承关系中构造函数及析构函数的调用次序:
先来看一个例子:
#include <iostream>
using namespace std ;
class B
{
public :
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class D:public B
{
public :
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
};
int main()
{
D d;
return 0 ;
}
程序输出结果:
B()
D()
~D()
~b()
分析:既然输出结果是先输出B的构造函数,在输出D的构造函数,那么,可不可以理解为在构造D的对象时,先调用B的构造函数,再调用D的构造函数呢??
我们在vs平台中创建D对象后单步调试发现和我们理解的不一样,它在创建派生类对象时是这样实现的:对象在创建时先调用派生类D的构造函数,在D的构造函数初始化列表中,由于派生类继承了基类的所有数据成员和成员函数,并且在存储时先存储基类的成员,那么,编译器在构造派生类时,将基类构造函数放在派生类的构造函数初始化列表来创建,即就是,派生类执行构造函数时,在编译器看来是下面这种形式:
D()
:B() //基类构造函数
{
cout << "D()" << endl;
}
【说明】
1、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。
例如:
class B
{
public :
B (int a) //没有缺省的构造函数
{}
}
class D:public B
{
public :
D ()
:B (10) //此时初始化列表中必须显式给出基类名和参数列表
{}
}
2、基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
例如
class B
{
public :
int _b;
}
class D:public B
{
public :
int _d;
}
此时在派生类中系统将合成默认构造函数如下
D ()
:B()
{}
3、基类定义了带有形参表构造函数,派生类就一定定义构造函数。
此条根据需求来看,既然基类构造函数带参数了,即就是希望创建对象的时候给传个参数过去,所以派生类一般情况下需要定义构造函数。
7、继承赋值兼容规则:
(1)子类对象可以赋值给父类对象(切割/切片)
例如创建一个继承:D类继承于B类
class B
{
public :
int _b;
};
class D :public B
{
public :
int _d;
};
int main()
{
D d;
B b;
b = d;
return 0 ;
}
两个大小不同的类可以赋值,那么该如何理解这种这种现象呢?
在赋值时,将发生切片(切割)行为,将派生类中基类的那部分赋值给基类对象
因为父类对象较之子类对象占的内存空间小,如果将子类对象赋值给父类对象,只可能将子类中继承的那部分赋值给基类。不会发生内存的越界访问。因此,系统允许赋值。
(2)父类对象不能赋值给子类对象
因为子类对象较之父类对象占的内存空间大,如果将父类对象赋值给子类对象,可能会将不属于父类的那部分数据(或者其他程序的数据)赋值给子类,访问越界,导致系统崩溃。
(3) 父类的指针/引用可以指向子类对象
即下面这种格式合法
int main()
{
B* p;
p = new D;
p->_b = 10 ;
delete [] p;
return 0 ;
}
(4)子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
int main()
{
D* p;
p = new B;
p->_b = 10 ;
delete [] p;
return 0 ;
}
因为子类包含有父类,把子指针赋给父指针时其实只是把子类中父类部分的地址赋给了父类指针而已,而反之,则会发生内存访问越界,原因和上1、2相同。。