引子:接上一回我们讲了继承的分类与六大默认函数,其实继承中的菱形继承是有一个大坑的,我们也要进入多态的学习了。
注意:我学会了,但是讲述上可能有一些不足,希望大家多多包涵
继承复习:
1,继承中的友元函数不能继承!(就比如说,你爸爸的朋友不是你朋友,你朋友不时你爸爸的朋友)
2,继承中的static:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例(可以通过地址来看是否一样)
继承补漏洞(数据冗余与二义性):
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承有数据冗余和二义性的问题
如下图:西红柿,里面就有二个植物的属性,这与我们常识不符
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
关键字:virtual ,虚拟继承可以解决菱形继承的二义性和数据冗余的问题
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承其实是is a的关系,组合其实是has a 的关系,
组合耦合度更低,关联性较低,比较适用与接口,继承耦合度更高,关联性比较强,比较收到父类的较大影响,往往一发而牵全身,故在现实生活,公司里能用组合就用组合,在继承的情况下就使用继承,(其实也是一种"黑箱白盒"的思想),白盒要求更高,黑盒则只要功能实现,故黑盒难度更低,而我们组合本质上就是一种黑盒,实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
多态基础知识:
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
多态的构成条件
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:
即被virtual修饰的类成员函数称为虚函数,虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
抽象类:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
2. 析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
试例代码:
class person
{
public:
//virtual void ticket() = 0
//在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
//类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
//类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
virtual void ticket()
{
cout << "买全票" << endl;
}
virtual ~person()
{
cout << "~person" << endl;
}
};
class children :public person
{
public:
virtual void ticket()
{
cout << "买半票" << endl;
}
~children()
{
cout << "~children" << endl;
}
};
class soldier :public person
{
public:
virtual void ticket()
{
cout << "优先买票" << endl;
}
~soldier()
{
cout << "~soldier" << endl;
}
};
//产生多态的条件(语法规定)
//1,虚函数重写
//2,必须是父类引用调用虚函数或者父类指针
void func1(person &p)
{
p.ticket();
}
void func2(person*p)
{
p->ticket();
}
//协变
//析构函数的重写,编译器在编译的时候,统一变为destructor;
int main()
{
person s1;
children s2;
soldier s3;
func1(s1);
func1(s2);
func2(&s1);
func2(&s2);
func1(s3);
func2(&s3);
//隐藏关系大于重写
s2.ticket();
person* s4 = new soldier;
person* s5 = new person;
person* s6 = new children;
delete s5;
delete s4;
delete s6;
return 0;
}
多态的原理:
我们先看一下以下代码的结果:
//查看虚拟函数的地址-->虚表
//虚函数开辟有一定的空间消耗
//参看各个虚函数的地址,与普通函数的区别
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void func(Base& p)
{
//这是动态的一个(直接在虚表中找到的func)
p.Func1();
//静态编译的func(是在链接产生的符号表!)
p.Func3();
}
//切割父类的那一部分!
int main()
{
Base b;
Base b2;
Base b3;
Derive d;
func(b);
func(d);
int h1=2;
static int h2=3;
int* h3 = new int;
const int h4=1;
printf("栈区%p\n", &h1);
printf("静态区:%p\n", &h2);
printf("堆区:%p\n", &h3);
printf("常量区:%p\n", &h4);
//相关关系关联的成度
printf("%p\n", *((int*)&b));
printf("%p\n", *((int*)&d));
/*printf("%p ", &Base::Func1);
printf("%p ", &Base::Func2);
printf("%p ", &Base::Func3);
printf("%p ", &Derive::Func1);*/
return 0;
}
结果运行如下:
一:虚函数指针存在对象中,有一个void**的指针
二:虚函数的地址相对其他非常相近,都是在代码区,
三,(1),多个虚函数地址如果类型一样,他们的地址都是一样的,就是共享虚表(2),注意取出前四位地址是*((int*)&b);(3),虚函数表,存在常量区
四,继承的虚函数地址没有改变,重写的虚函数地址改变,
多态原理文字解释(虚表本质是函数指针数组):
void->虚函数,存在代码区
void*->虚函数表,存在常量区
void**->虚函数表的指针,存在对象中
看出满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
其实多态本质上是一种动态绑定又称后期绑定。普通函数是静态绑定又称为前期绑定(早绑定),
什么是动态绑定与静态绑定?如下
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载 2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。 3. 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。
二道面试题:
多继承中指针偏移问题?下面说法正确的是(C )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){ Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
下面哪种面向对象的方法可以让你变得富有( ) '
A: 继承 B: 封装 C: 多态 D: 抽象