目录
一.继承的概念及定义
1.1 继承的概念
1.2继承定义
1.2.1 继承定义的格式
1.2.2 继承方式
二. 基类和派生类对象的赋值转化
三. 继承中的作用域
四.派生类的默认成员函数
五.继承和友元
六.继承与静态成员
七.复杂的菱形继承和菱形虚拟继承
7.1菱形继承的问题
7.2 解决办法
7.3 虚拟继承实现原理
八.继承与组合
8.1 什么是组合
8.2 继承和组合的区别
九:总结
发现问题:当我们编写一个类时,发现这个类与类外一个类的成员变量和成员方法相似,并且具有一定的包含关系时,我们编写的这两个类会有很多相似的地方。
比如:
继承机制是面向对象程序设计使代码可以复用的重要手段。它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生的类称为派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承使类设计层次的复用。
简单来说就是:继承就是一个类复用了另外一个类的成员函数和成员变量。就好像在这个类里编写了另外一个类的成员。
来一个例子简单了解一下:
用上面的例子:
对应关系为:
基类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类里不可以见 | 在派生类里不可以见 | 在派生类里不可以见 |
总结:
- 基类的私有成员在派生类中不可以见,基类的其它成员在派生类中的访问方式是:继承方式和基类该成员访问限定符范围那个小,就是什么访问方式,等价于min(继承方式,基类该成员访问限定符)。范围从大到小:public > protected > private
- 基类的private成员,无论以什么继承方式继承,在派生类里是不可以访问的。但是,派生类还是继承了基类,只是在语法上限制了派生类在类里或者类外对基类私有成员的访问。
- 基类protected成员,通过public或者protected继承方式继承的派生类,该成员变成了派生类的protected成员,只能在类里访问,不能在类外访问。但是基类的private成员,派生类不可见,在继承中体现了两者的区别。可以看出保护成员限定符因继承才出现的。
- 使用关键字class定义的类的默认继承方式是private,使用关键字struct定义的类的默认继承方式是public,但是,最后显示写出继承方式。
- 在实际运用中一般使用public继承,很少用protected和private继承。因为protected和private继承下来的成员只能在派生类中使用,实用性不强。
演示和说明:
图示为:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
void Test()
{
Student s;
// 1.子类对象可以赋值给父类对象/指针/引用
Person p1 = s;
Person* pp = &s;
Person& rp = s;
//2.基类对象不能赋值给派生类对象,会报错
s = p1;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
//前提,指向基类指针指向派生类
pp = &s;
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
//基类指针指向基类的话
pp = &p1;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题,
//由于基类中没有_NO成员,访问会越界
ps2->_No = 10;
}
注意:在实际的继承体系同最好不要定义同名成员。
我们知道在类中有6个默认成员函数,即使我们不写,编译器会帮我们自动生成。
我们主要考虑前四个默认成员函数。
隐式调用基类的构造函数
显示调用基类构造函数
总结:
派生类中的基类成员部分只能调用基类的成员函数。
不显示调用,基类构造函数时默认构造函数
显示调用,基类构造函数是默认构造函数或者不是默认构造函数。如果不是默认构造函数,必须显示调用。
注意:在派生类里调用基类的赋值重载函数要加类作用域限定符。
原因:因为是成员变量在栈里开辟空间,基类成员先构造, 派生类成员后构造。析构时,按照栈先入后出性质,所以,派生类成员先析构,基类成员后析构。
总结:
默认成员函数基类成员部分掉基类的默认成员函数,派生类部分掉派生类的默认成员函数。
最好是都显示调用基类默认成员函数。
赋值操作符要加类的作用域,因为隐藏。
派生类先调用基类的构造,再调用派生类的构造。
派生类对象析构,先调用派生类的析构,再调用基类的析构。
析构不用显示调用,会自己调用。
基类友元的关系,派生类不能继承下来。也就是说基类的友元不能访问派生类的私有和保护成员。
class Student;//声明
class Person
{
public:
friend void Print(Person& p, Student& s);//友元
protected:
string _name = "tom";
};
class Student :public Person
{
public:
//friend void Print(Person& p, Student& s);解决
protected:
int _age = 10;
};
void Print(Person& p, Student& s){
cout << p._name << endl;
//cout << s._age << endl;//报错,友元不能继承,不能访问派生类的保护成员
}
int main()
{
Person p;
Student s;
Print(p, s);
system("pause");
return 0;
}
基类定义了一个静态成员,在继承体系中,只有这一个成员。也就是说,无论派生了多少派生类,操作的这个静态成员都是一个成员。
单继承:一个派生类只有一个字节的父类
多继承:一个子类有两个或者两个以上的的基类
菱形继承:多继承的特殊情况,有一个共同的基类。
更复杂一点可以是这样:
上面说明了在类Student和Teacher里都各自有一份基类Person的成员,然后又全部继承到了派生类Assistant中,导致Assistant中有了两份Person的成员int _id。
解决:
上面说明了了菱形继承的两个问题:
虚拟继承,是用关键字vritual修饰中间类。
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
class C :public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;//输出20
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
system("pause");
return 0;
}
当不带virtual关键字时:查看内存
这里也表现了数据冗余的问题,有两份_a。
带virtual关键字
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;//输出24
d.B::_a = 1;
d.C::_a = 1;
d._b = 3;
d._c = 4;
d._d = 5;
system("pause");
return 0;
保存偏移量的表为虚基表,共同的基类A叫虚基类。
抽象一点:
这样解决了数据二义性的问题。但是反而空间增加了,所实现了增加了两个指针,但是这种情况只是放在这是增加了,在其它情况,它可能减少了空间。
如果A的成员变量是一个数值:
class A
{
public:
int _a[10000];
};
//class B :virtual public A
class B
{
public:
int _b;
};
//class C :virtual public A
class C
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;//输出24
system("pause");
return 0;
}
不加virtual输出:
加virtual:
总结虚继承的原理:
B继承A,实际A的成员保存在最后面。B类对象中,还保存了一个指针,指针指向一个虚基表。虚基表里保存的是指针位置到实际保存A成员中第一个成员位置的偏移量。
C继承A,实际A的成员保存在最后面。C类对象中,还保存了一个指针,指针指向一个虚基表。虚基表里保存的是指针位置到实际保存A成员中第一个成员位置的偏移量。
D继承B和C,首先会继承B和C属于自己的成员,还会继承B和C的指针(但是指针变量内容不一样,因为虚基表不一样)。两指针指向的是实际保存各自指针位置到A成员的第一个成员的位置的偏移量。
共同基类A的成员保存在最下面。
注意共同基类的内容被继承下来了,也占据对象的空间。只是保存在最后,并且是一份(解决数据二义性和冗余问题)。
//继承
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
//组合
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
多继承的特殊情况,派生类有多个基类,派生类的多个基类最后会有共同的基类。
造成数据的二义性和冗余。
派生类的基类用关键字virtual修饰。,共同的基类不修饰。
使用偏移量。中间的基类,里保存指针,指向的空间内容为,中间类保存指针位置,到共同基类成员位置的偏移量。共同基类成员只有一份。
继承复用一定是派生类复用基类的一种方法,基类的内部细节对子类可见,一定程度上破坏了基类的封装性,增加了基类和派生类之间的耦合度。
组合是通过在一个类里定义另外一个类对象,来实现类的复用。对象的内部细节对于类来说不可见,组合类之间没有很强的依赖关系,耦合度低。
当只能用继承时,用继承,只能用组合时,用组合,组合和继承都可以用,优先使用组合。