面向对象的基本特征:
封装
继承
多态
在实现世界中,任何一个概念都不是孤立存在的,都有与之相关的概念,这些概念之间会存在各种关系
面向对象模拟现实世界,我们用类来表示概念,所以,概念之间的关系,就转换成了类与类之间的关系。
例:
计算机
CPU、显卡、内存、硬盘、键盘、鼠标、显示屏幕、主板...
笔记本、台式机、平板、手机...
学生
学号、姓名、年龄、性别、出生年月...
小学生、中学生、大学生、研究生...
汽车
轮胎、发动机、变速箱、车灯、车门...
小汽车、公交车、货车...
类与类之间主要存在两种关系:
组合:has-a, 例如计算机有内存、硬盘等
继承:is-a ,例如笔记本是计算机、大学生是学生等
所谓继承,就是在已有类型的基础上,创建新的类型。
继承可以实现 代码复用、减少代码冗余、提高开发速度等
继承主要用来描述那些非常相似,只有细微差异的类之间的关系。
通过继承联系在一起的类构成一种层次关系,处于层次关系根部的类,称为基础类(Base Class), 简称基类,也称为父类
层次关系中其它的类 或直接 或间接从基类派生而来,这些类称为派生类(Derived Class),也称为子类.
基类负责定义在层次关系中所有类 共同的 属性和行为。
派生类自动获取基类的 特性,并负责定义各派生类自己特性。
派生类的定义:
class 派生类名: 继承方式 基类1, 继承方式 基类2, ...
{
// 派生类新增的成员
};
根据基类的数量,把继承又分成两种:
单继承: 只有一个基类
多重继承: 两个或两个以上的基类
继承方式:决定了基类的成员在派生类内部 和 派生类外部的访问权限。
public 公有继承
private 私有继承
protected 保护继承
class的默认继承方式为private继承,struct的默认继承方式为public继承。
C++中class与Struct的区别?
公有继承:
基类的公有成员,通过公有继承,派生类可直接访问
基类的私有成员,通过公有继承,派生类不可直接访问
基类的保护成员,通过公有继承,派生类内部可直接访问,派生类外部不能直接访问
私有继承:
基类的公有成员,通过私有继承,派生类内部可直接访问,派生类外部不能直接访问
基类的私有成员,通过私有继承,派生类不可直接访问
基类的保护成员,通过私有继承,派生类内部可直接访问,派生类外部不能直接访问
保护继承:
基类的公有成员,通过保护继承,派生类内部可直接访问,派生类外部不能直接访问
基类的私有成员,通过保护继承,派生类不可直接访问
基类的保护成员,通过保护继承,派生类内部可直接访问,派生类外部不能直接访问
[父的私有,子皆不可;公有继承公有部分,子随意;其余皆是子有内部访问,子外部不可问]
派生类与基类的关系
通过测试,可以发现,派生类由两部分组成:
从基类继承来的成员
派生类新增的成员
因为在派生类对象中包含有基类对应的功能,所以,可以把 公有派生类 当成 基类 用。
具体:
1、可以用 派生对象 初始化 基类对象
Derived d; // 派生类对象
Base b = d;// 实例化基类对象
2、可以用 派生类对象 给 基类对象 赋值
b = d;
3、基类指针 可以指向 派生类对象
Base* p = &d;
4、基类引用 可以 绑定 到 派生类对象
Base& r = d;
说明:
虽然 公有派生类 可以当成 基类 用,但是用 派生类 初始化的 基类对象/指针/引用, 都无法访问 派生类自己 新增加的成员,只能访问派生类从基类继承的来的成员。
继承中 构造函数 与 析构函数
一般 构造函数 负责 初始化本类的 成员变量
所以,当初始化一个派生类对象时,得先调用基类的构造函数,以初始化 从基类继承来的成员,再调用成员对象的构造函数
最后再执行派生类自己的构造函数。
如果 基类有默认构造函数,则在 初始化 派生类对象时,可以自动调用基类的默认构造函数
如果基类没有默认构造函数或需要调用基类其它的构造函数,
则只能在 派生类构造函数的初始化列表中 显式的调用基类的构造函数
例:
class Base
{
public:
Base(形参);} ;
class Derived: public Base
{
public:
Derived(形参): Base(形参) {}
} ;
当派生类对象 销毁时,会自动 先调用 派生类的析构函数,然后调用成员对象的析构函数,最后调用 基类的析构函数。
如果对象的作用域相同,则
先构造的,后析构
后构造的,先析构
练习:
如果派生类新增的成员是另一个类类型的对象,那么 构造函数 与 析构函数 的调用顺序又是怎样的?
例:
class A {};
class B {};class C : public B
{
A a;
};
C c;//顺序是B A C 先是父,再成员,最后是本身的构造函数。
继承中的同名成员:
派生类可以在基类的基础上新增成员,那么这些新增加的成员可能会与基类的某些成员同名。
例:
class Base
{
public:
void print()
{
cout << x << endl;
}
private:
int x;
};class Derived: public Base
{
public:
void print()
{
print(); // 调用的哪个print()? 会调用派生类的print函数,形成递归
cout << x << endl; // 访问的又是哪个x? 本身的
cout << y << endl;
}
private:
int x;
int y;
};
当在 派生类内部 或使用 派生类对象,去访问那些同名成员时,默认情况下访问的是派生类自己新增加的成员
也就是说,基类的那些同名成员被隐藏了。
如果需要使用从基类继承来的那些同名成员,指定作用域即可:
void print()
{
Base::print(); // 调用基类的那个同名函数
}
多重继承:
所谓多重继承,指一个派生类拥有多个基类的情况,多重继承下的派生类会继承它所有基类的属性和行为。
例:
class Base1
{
public:
int getX();
private:
int x;
};class Base2
{
public:
int getY();
private:
int y;
};class Derived: public Base1, public Base2
{
};
Derived d; // 对象d有两个成员变量,有两个成员函数
多重继承中 同名成员
多重继承的情况下,多个基类中的成员可能同名,基类中的成员可能与派生类中的成员同名
当用派生类对象去访问某个成员时,首先从派生类的作用域查找该成员,如果找到则使用
如果没找到,则继承从它的所有的基类中同时查找
如果没找到,则继续沿着继承层次结构往上查找,找完整个继承层次都没找到 则报错
如果在同一层次的多个基类中找到了多个同名成员,则产生二义性问题
多重继承中的二义性问题,解决方式与单继承的同名成员情况类似,也只需要指定 作用域 即可:
例:
d.Base1::getX()
如果底层派生类的两个直接基类又继承自同一个基类,则称这种情况为"菱形继承或钻石继承" :
例:
class A {}
class B: public A {}
class C: public A {}
class D: public B, public C {}
上述继承方式,存在两个问题:
1、A中的成员,通过继承关系,到达派生类D的时候,会出现多份。
2、初始化派生类D的对象时,会调用基类的构造函数,A中的成员可能会被初始化多次。
解决方法:虚继承
所谓的虚继承,指在直接基类B和C继承A的时候,在继承方式前面加一个关键字virtual
被继承的A称为虚基类
虚继承使得B和C共享虚基类A的成员。
例:
class A {} // 虚基类
class B: virtual public A {} // 虚继承
class C: virtual public A {} // 虚继承
class D: public B, public C {}
虚基类A的成员存放在派生类D对象的存储空间的最后面
同时,要保证B类和C类能正常访问被共享的A的成员
所以,得记录被共享A的成员距离B和C的偏移量,大多数编译器这些偏移量存放在对象之外,一般把保存这些偏移量的那个数据结构称为 虚基类表(vtable)
也就是偏移量是存储在其它地方的,那么B和C得保存这些偏移量的地址,所以,虚继承的派生类B和C中都会多出一个虚指针,保存偏移量的存储位置。
结论:
当计算底层派生类对象大小时,需要加上虚指针,如果对象成员的大小不一致,还得考虑字节对齐。
在初始化底层派生类D的对象时,需要在初始化列表中调用所有直接基类的构造函数 和 虚基类的构造函数