C++总结5——继承与多态【转】
(转自:https://blog.****.net/wendy_keeping/article/details/75212860)
一、继承
1.C++的继承
继承有3种形式:私有继承、保护继承、公有继承,默认的继承方式是私有继承。通常使用 public 继承。
不论哪种继承方式,派生类都是显示的继承类基的保护成员变量和函数和公有成员变量和函数,继承方式只是限定在派生类中这两种成员变量的访问方式(即访问权限)。私有的成员变量和函数也被继承到派生类中,但是不能被访问,它是隐藏的,在派生类中不可见。
派生类继承基类,除基类的构造函数和析构函数不外,其他的所有都继承。
继承是“is-a”的关系。派生类是基类的一种,例如:学生继承与人,学生是人的一种,但人不一定都是学生。
私有继承:将基类保护和公有的成员变量作为派生类的私有变量,不能在类外访问;
保护继承:将基类保护和公有的成员变量作为派生类的保护变量,不能在类外访问但是可以被继承;
公有继承:基类的公有成员作为派生类的公有成员,基类的保护成员作为派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
注意:派生类中可以定义和基类同名的成员变量,因为有类的作用域的限定(Base::a Derive::a)
class Base
{
public:
Base(int date):_ma(data) {}
~Base() {}
private:
int _ma;
};
class Derive : public Base
{
public:
Derive(int data1, int data2):Base(data1),_mb(data2) { }
~Derive() {};
};
在派生类的构造函数的初始化成员列表中显示的调用基类的构造函数,用基类的构造函数构造派生类中的基类部分。
2.派生类对象的构造/析构顺序
构造:构造基类成员对象–>构造基类–>构造派生类成员对象–>构造派生类
析构:析构派生类–>析构派生类的成员对象–>析构基类–>析构基类的成员对象
3.基类对象==>赋值给==>派生类对象 (X) 后面派生类的内存无法赋值
派生类对象==>赋值给==>基类对象 (V)将派生类中基类的部分赋值给基类对象
基类指针==>指向==>派生类对象 (V) 该指针智能访问从基类继承的那部分
派生类指针==>指向==>基类对象 (X) 可能非法访问内存
4.虚函数
如果派生类继承了有被vritual关键字修饰的函数的基类,被vritual修饰的函数称为虚函数。派生类可以重写该虚函数。如果派生类重写了该虚函数,那么派生类对象调用该方法时调用的就是派生类自己实现的方法。如果派生类没有重写该方法,则调用基类的方法。
class Base
{
public:
Base(int data):_ma(data) {}
~Base() {}
virtual void Show()//虚函数
{
cout<<"Base::Show()"<<endl;
}
private:
int _ma;
};
class Derive : public Base
{
public:
Derive(int data1, int data2):Base(data1),_mb(data2) { }
~Derive() {};
virtual void Show()//虚函数
{
cout<<"Derive::Show()"<<endl;
}
private:
int _mb;
};
int main()
{
Derive derive(10,20);
derive.Show();//Derive::Show()
return 0;
}
5.纯虚函数
纯虚函数是特殊的虚函数,基类中不能给出这个虚函数的实现方法,派生类必须给出该函数的实现。这种特殊的函数称为纯虚函数,有纯虚函数的类称为抽象类,抽象类不能实例化对象,但是可以定义抽象类的指针或引用,派生类必须重写方法后才能实例化对象。
class Base//抽象类
{
public:
Base(int date):_ma(data) {}
~Base() {}
vritual void Show() = 0;//纯虚函数
private:
int _ma;
};
含有虚函数的类,编译器会在其前4个字节添加一个虚表指针,并为其产生一个虚函数表,虚表指针指向虚函数表。虚函数表在编译时产生,位于.rodata段,属于类所共有,不属于某一对象。若派生类没有从基类中继承虚表指针和虚函数表,则自己产生,虚表指针永远位于对象前4个字节处。如果派生类从基类继承了虚表指针和虚函数表,则派生类不再产生,虚函数都写在继承来的虚函数表中(同名覆盖)。
6.基类的成员方法和派生类的成员方法之间是同名隐藏的关系
基类和派生类继承结构中,函数名、参数列表、返回值都相同,如果基类中的函数是vritual函数,那么派生类中该函数也是vritual函数。如果派生类重新实现了该vritual函数,那么派生类对象调用该方法时调用的就是派生类自己实现的方法。如果派生类没有重写该方法,则调用基类的方法。
7.菱形继承
两个派生类继承同一个基类,而某一个基类同时继承这两个派生类。
class A
{
public:
A(int data):ma(data)
{
cout<<"A()"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int ma;
};
class B : public A
{
public:
B(int data):A(data),mb(data)
{
cout<<"B()"<<endl;
}
~B()
{
cout<<"~B()"<<endl;
}
private:
int mb;
};
class C : public A
{
public:
C(int data):A(data),mc(data)
{
cout<<"C()"<<endl;
}
~C()
{
cout<<"~C()"<<endl;
}
private:
int mc;
};
class D : public B, public C
{
public:
D(int data):B(data),C(data),md(data)
{
cout<<"D()"<<endl;
}
~D()
{
cout<<"~D()"<<endl;
}
private:
int md;
};
int main()
{
D d(10);
return 0;
}
通过命令 cl 文件名 /d1reportSingleClassLayout类名 > 重定向文件名
例:cl 20170717.cpp /d1reportSingleClassLayoutD > log.txt
将20170717.cpp文件中,类D的内存布局重定向到文件log.txt中
类D的内存布局:
d中有两个ma,但是变量ma所处的类的作用域不同。
8.虚继承
虚继承是多重继承中特有的概念,是为了解决多重继承而出现的。但是在C++中,多重继承是不推荐的,并不常用。被虚继承的基类叫做虚基类。
class A
//虚基类
{
public:
A(int data):ma(data)
{
cout<<"A()"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int ma;
};
class B : vritual public A
{
public:
B(int data):A(data),mb(data)
{
cout<<"B()"<<endl;
}
~B()
{
cout<<"~B()"<<endl;
}
private:
int mb;
};
class C : vritual public A
{
public:
C(int data):A(data),mc(data)
{
cout<<"C()"<<endl;
}
~C()
{
cout<<"~C()"<<endl;
}
private:
int mc;
};
class D : public B, public C
{
public:
//虚继承后,ma只有一份。类D中,A可见,因此要在初始化类表中对其进行构造。
//虚基类的数据永远先构造
D(int data):B(data),C(data),A(data),md(data)
{
cout<<"D()"<<endl;
}
~D()
{
cout<<"~D()"<<endl;
}
private:
int md;
};
int main()
{
D d(10);//构造顺序:A(); B(); C(); D();
return 0;
}
类D的内存布局:
二、多态
1.多态的实现机制
C++的多态就是基于继承的,多态的实现就是调用虚函数时发生的同名覆盖。当用基类的指针(或引用)指向派生类的对象时,通过该指针(或引用)调用虚方法是动态联编的过程。先找到对象前4个字节的虚函数指针(vbptr),通过vbptr找到虚函数表,虚函数表里有函数的入口地址。
2.C++的静多态和动态多态
静多态是指函数的重载和模板
动多态是指继承中,虚函数的同名覆盖方法
3.早绑定和晚绑定的区别
早绑定也称静态绑定,是程序在编译时就确定调用的是哪个函数。
汇编指令是 call Base::func()
晚绑定也称动态绑定,是编译的时候才确定调用的是哪个函数。晚绑定基于继承实现,基类的指针(或引用)指向派生类的对象,通过指针(或听引用)访问虚函数时,会调用指针所指向的派生类的方法。
汇编指令如下:
mov ecx,dword ptr[p] 访问虚表指针,将虚表指针放在ecx寄存器中
mov eax,dword ptr[ecx] 将ecx(虚表指针)的值(虚函数表)放在eax寄存器中
call eax 调用函数
在运行过程中,确定了eax寄存器里的值,才能确定调用哪个函数。
4.如果基类的指针(或引用)指向堆上派生类的对象,必须要将基类的析构函数写成虚函数,否则会有内存泄露。
#include <iostream>
using namespace std;
class Base
{
public:
Base(int data):_ma(data)
{
cout<<"Base()"<<endl;
}
~Base()
{
cout<<"~Base()"<<endl;
}
virtual void Show()//虚函数
{
cout<<"Base::Show()"<<endl;
}
private:
int _ma;
};
class Derive : public Base
{
public:
Derive(int data1, int data2):Base(data1),_mb(data2)
{
cout<<"Derive()"<<endl;
}
~Derive()
{
cout<<"~Derive()"<<endl;
};
virtual void Show()//虚函数
{
cout<<"Derive::Show()"<<endl;
}
private:
int _mb;
};
int main()
{
Base *p = new Derive(10,20);
p->Show();
delete p;
return 0;
}
运行结果如下图:
只调用了基类的析构函数,Derive类未析构。原因是:p是Base*类型的直指针,Base的析构函数不是虚函数,编译时已经确定delete p时调用的是Base类的析构函数。
如果将基类的析构函数写成虚析构函数,则调用delete p时,具体调用哪个函数,是运行时才确定的。当程序运行时,发现p所指向的是派生类的对象,故先调用派生类的析构函数,然后调用基类的析构函数
。
一般都将基类的虚构函数定义为虚函数,从而避免内存泄露。
5.常见的面试题
<1>inline函数能否为虚函数? 不能!
<2>构造函数能否为虚函数? 不能!
<3>析构函数能否为虚函数? 可以!
<4>static函数能否为虚函数? 不能!
答案已经公布,原因请自己分析(提示:各种函数的特征)