C++继承和多态
继承
继承的本质:
- 代码的复用。
- 在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了。
类和类的关系:a part of … …一部分关系
继承:a kind of … …一种的关系
继承引入了一些概念:基类/父类 派生类/子类
类的私有属性只有当前类的方法或者友元才能访问,继承后的子类不管哪种方式都无法访问。
基类成员的访问限定,在派生类里面是不可能超过继承方式的。
总结:
- 外部只能访问对象的public的成员,protected和private的成员无法直接访问。
- 在继承结构中,派生类从基类可以继承过来private的成员,但是派成类无法直接访问(私有成员在派生类中不可见)。
-
protected和private的区别?在基类中定义的成员,想在派生类访问,但是不想被外部访问,那么在基类中,把相关成员定义成protected保护的;如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成private私有的。
**默认的继承方式是什么?**要看派生类是用class定义的,还是struct定义的?class定义派成类,默认继承方式就是private私有的。struct定义派生类,默认继承方式就是public公有的。
派生类的构造过程
派生类怎么初始化从基类继承来的成员变量呢?
- 派生类从基类可以继承来所有的成员(变量和方法),除过构造函数和析构函数(因为构造函数和析构函数只有在当前类中才有作用)。通过调用基类相应的构造函数来初始化,派生类的构造函数和析构函数,负责初始化和清除派生类部分。派生类从基类继承来的成员的初始化和清理由基类的构造函数和析构函数来负责。
- 派生类和基类的构造和析构顺序?一般情况下基类的初始化在派生类构造函数的初始化列表中初始化,所以先调用基类的构造函数,在调用派生类的构造函数,来初始化自己的成员。出作用域时,先调用派生类的析构函数,在调用基类的析构函数。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data) :ma(data) { cout << "Base(int)" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int ma;
private:
};
class Derive:public Base
{
public:
/*
“Base”: 没有合适的默认构造函数可用
Derive(int data) :ma(data), mb(data) { cout << "Derive()" << endl; }
*/
Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
protected:
private:
int mb;
};
int main()
{
/*//先调用基类的构造,在调用子类的构造,出作用时先调用子类的析构,在调用父类的析构
Base(int)
Derive()
~Derive()
~Base()
*/
Derive d(20);
return 0;
}
重载、隐藏和覆盖
重载关系:一组函数要重载,必须处于同一作用域中;而且函数名字相同,参数列表不同。Base类中的两个show函数就是重载的关系。
隐藏(作用域的隐藏)关系:在继承结构当中,派生类的同名成员,把基类的同名函数给隐藏调用了。Derive类中的show方法隐藏了Base类中的show方法和show(int)方法。
覆盖关系:基类和派生类的方法,返回值、函数名以及参数列表都相同,而且基类的方法都是虚函数,那么派生类的方法就会自动处理成虚函数,它们直接称为覆盖关系。具体查看虚函数章节内容的动态绑定。
把继承结构,也就说成从上(基类)到下(派生类)结构。
基类对象 -> 派生类对象 N
派生类对象 -> 基类对象 Y
基类指针(引用) -> 派生类对象 N
派生类指针(引用) -> 基类对象 Y
在继承结构中,进行上下的类型转换,默认只支持从下到上类型的转化,也就是派生类转化成基类。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data=10) :ma(data) { cout << "Base(int)" << endl; }
~Base() { cout << "~Base()" << endl; }
void show() { cout << "Base::show()" << endl; }
void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
private:
};
class Derive :public Base
{
public:
Derive(int data=10) :Base(data), mb(data) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
void show() { cout << "Derive::show()" << endl; }
//void show(int) { cout << "Derive::show(int)" << endl; }
protected:
private:
int mb;
};
int main()
{
Derive d;
d.show();
//“Derive::show”: 函数不接受 1 个参数
//d.show(10);//如果Derive中没有重写Base中的void show(int)函数,则会报错
d.Base::show(10);//显示调用父类的带参数的成员函数
Base b(10);
Derive de(20);
// 基类对象b <- 派生类对象de 类型从下往上的转化 Y
b = de;
// 派生类对象de <- 基类对象b 类型从上往下的转化 N
//de = b;
// 基类指针(引用) <- 派生类对象 类型从下往上的转化 Y
Base* p = &de;
p->show();
((Derive*)p)->show();
// 派生类指针(引用) <- 基类对象 类型从上往下的转化 N
//Derive* pd = &b;//存在内存的非法访问
return 0;
}
静态绑定/动态绑定/虚函数
静态绑定
静态编译时期的绑定,也称为静态绑定,编译期间已经确定函数的调用情况。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data = 10) :ma(data) { cout << "Base(int)" << endl; }
~Base() { cout << "~Base()" << endl; }
void show() { cout << "Base::show()" << endl; }
void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
private:
};
class Derive :public Base
{
public:
Derive(int data = 10) :Base(data), mb(data) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
void show() { cout << "Derive::show()" << endl; }
protected:
private:
int mb;
};
int main()
{
Derive d(10);
Base* p = &d;
//静态编译时期的绑定,也称为静态绑定,编译期间已经确定函数的调用情况
p->show();//call Base::show (09F1041h)
p->show(10);//call Base::show (09F1433h)
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
return 0;
}
虚函数 && 动态绑定
一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable(虚函数表),虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。RTTI(run-time type information运行时的类型信息,类型字符串(class Base等))当程序运行时,每一张虚函数表都会加载到程序的.rodata(只读数据)区。
一个类里面定义虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr(虚函数指针),指向相应类型的虚函数表vftable。一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表。
一个类里面虚函数的个数,不影响对象的内存大小(vfptr指针大小还是指针类型大小),影响的是虚函数表的大小。
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法将会自动处理成虚函数。这个派生类这个方法就重写/覆盖了基类的方法,这个派生类的vftable表中的对应虚函数的地址替换基类的虚函数地址。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data = 10) :ma(data) { cout << "Base(int)" << endl; }
~Base() { cout << "~Base()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
private:
};
class Derive :public Base
{
public:
Derive(int data = 10) :Base(data), mb(data) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
void show() { cout << "Derive::show()" << endl; }
protected:
private:
int mb;
};
int main()
{
Derive d(10);
Base* p = &d;
/*
* Base::show();此时是一个虚函数,所以就需要动态绑定了,通过查找对象的vfptr进行动态绑定
mov ecx,dword ptr [p]
mov eax,dword ptr [edx+4]
call eax(虚函数地址),运行时期的动态绑定
*/
p->show();
p->show(10);
//因为有了虚函数,所以添加了虚函数指针,导致多了4个字节
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
cout << typeid(p).name() << endl;
/*
pb的类型:Base -> 有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型, *pb 等价于 Base类型
如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI 类型
pb -> d(vfptr) -> Derive vftable -> class Derive
*/
cout << typeid(*p).name() << endl;
return 0;
}
cl virtual.cpp /d1reportSingleClassLayoutBase
查看当前virtual.cpp文件中Base类的情况。
动态绑定
是不是虚函数的调用一定就是动态绑定? 肯定不是。
在类的构造函数中,调用虚函数,也是静态绑定(构造函数调用其他函数(虚函数),不会发生动态绑定),动态绑定依赖于对象(vfptr -> vftable)。如果不是通过指针或者引用变量来调用虚函数,那就是静态绑定。
对于存在虚函数的基类和派生类关系中:用对象本身去调用虚函数,发生的是静态绑定。不管是基类指针指向基类对象还是派生类对象,都发生动态绑定(必须由指针调用虚函数)。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data) :ma(data) { cout << "Base(int)" << endl; };
virtual ~Base() { cout << "~Base()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
protected:
int ma;
};
class Derive :public Base
{
public:
Derive(int data) :Base(data), ptr(new int(10)) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
virtual void show() { cout << "Derive::show()" << endl; }
private:
int* ptr;
};
int main()
{
Base b(10);
Derive d(20);
//这里的汇编是call Base::show/Derive::show 是静态绑定,用对象本身调用虚函数
b.show();
d.show();
//不管是基类指针指向基类对象还是派生类对象,都发生动态绑定
Base* pb1 = &b;
pb1->show();
Base* pb2 = &d;
pb2->show();
//不管基类引用的是基类的对象还是派生类的对象,都发生动态绑定
Base& rb1 = b;
rb1.show();
Base& rb2 = d;
rb2.show();
//动态绑定
Derive* pd1 = &d;
pd1->show();
Derive& rd1 = d;
rd1.show();
return 0;
}
虚析构函数
虚函数依赖:
- 虚函数能产生地址,并存储在vftable中,其实也就是拥有虚函数的类产生的vftable。
- 对象必须存在(vfptr -> vftable -> 虚函数地址)
那些函数不能实现为虚函数?
- 构造函数。构造函数中(调用任何的函数,都是静态绑定的)调用虚函数,也不会发生静态绑定。
- 静态成员方法。静态方法不依赖于对象。
虚析构函数,析构函数调用的时候,对象是存在的!所以可以实现为虚函数。
基类的析构函数是虚函数,那么派生类的析构函数一定是虚函数。
什么时候把基类的析构函数必须实现为虚函数?
基类的指针(引用)指向堆上new出来的派生类对象的时候,delete pb(基类指针),它调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用,导致内存泄漏
#include<iostream>
using namespace std;
class Base
{
public:
Base(int data) :ma(data) { cout << "Base(int)" << endl; };
virtual ~Base() { cout << "~Base()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
protected:
int ma;
};
class Derive :public Base
{
public:
Derive(int data) :Base(data), ptr(new int(10)) { cout << "Derive()" << endl; }
~Derive() { cout << "~Derive()" << endl; }
virtual void show() { cout << "Derive::show()" << endl; }
private:
int* ptr;
};
int main()
{
Base* b = new Derive(10);
//如果Base中的show不是虚函数,那么就是动态绑定,否则就会发生动态绑定
b->show();
//如果Base中的析构函数不是虚函数,那么就不会调用Derive中的析构函数,造成内存泄漏
delete b;
return 0;
}
抽象类
拥有纯虚函数的类叫做抽象类,抽象类不可以在进行实例化对象了,但是可以定义指针和引用变量。定义纯虚函数的方法virtual void function() = 0;
,如果能进行抽象,那么尽量将函基类设计成抽象类。
基类的作用:
- 让派生类通过继承的方式复用基类的属性和方法。代码复用
- 给所有的派生类保留统一的覆盖/重写接口。虚函数/动态绑定
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
Animal(string name):_name(name){}
virtual void bark() = 0;//纯虚函数,动态绑定和静态绑定问题
protected:
string _name;
private:
};
class Dog:public Animal
{
public:
Dog(string name):Animal(name){}
void bark() { cout << _name << " bark: wang wang!" << endl; }
protected:
private:
};
class Cat :public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << _name << " bark: miao miao!" << endl; }
protected:
private:
};
void bark(Animal* p)
{
p->bark();
}
int main()
{
Cat c("小猫");
Dog d("小狗");
bark(&c);
bark(&d);
return 0;
}
多态
静态多态(编译时期的多态)、动态多态(运行时期的多态)
静态多态:函数重载、模板(函数模板和类模板)
bool compare(int,int){}
bool compare(double,double){}
//调用点
compare(10,20);call compare_int_int 在编译阶段就确定好调用的函数版本
compare(10,20);call compare_double_double
template<typename T>
bool compare(T a,T b){}
//调用点
compare<int>(10,20); -> 实例化一个compare<int>
compare<double>(10.0,20.0); -> 实例化一个compare<double>
动态多态:继承(虚函数)
在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用派生类对象的覆盖方法,称为多态。
多态底层是通过动态绑定来实现的,通过对象的vfptr去寻找vftable,再去调用方法。
继承和多态实际问题
对象的vfptr虚函数指针交换。
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
Animal(string name) :_name(name) {}
virtual void bark() = 0;//纯虚函数,动态绑定和静态绑定问题
protected:
string _name;
private:
};
class Dog :public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout << _name << " bark: wang wang!" << endl; }
protected:
private:
};
class Cat :public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << _name << " bark: miao miao!" << endl; }
protected:
private:
};
int main()
{
Cat* c = new Cat("加菲猫");
Dog* d = new Dog("二哈");
//进行对象指针的强转,其实也就是交换对象第一个元素的值,
//对象元素的分布是(vfptr和对应的属性),所以第一个元素是vfptr
int* pp1 = (int*)c;
int* pp2 = (int*)d;
int temp = pp1[0];
pp1[0] = pp2[0];
pp2[0] = temp;
//-----------------------------------------------------------------
//对象的vfptr在上面操作被交换了
c->bark();
d->bark();
//-----------------------------------------------------------------
return 0;
}
虚函数默认参数不一致的情况。
#include<iostream>
#include<string>
using namespace std;
class Base
{
public:
virtual void show(int i = 10) { cout << "Base::show(),i=" << i << endl; }
};
class Derive:public Base
{
public:
virtual void show(int i = 20) { cout << "Derive::show(),i=" << i << endl; }
};
int main()
{
Base* b = new Derive();
/*
动态绑定只是调用函数,其默认参数其实在编译过程中就已经压栈了,
而压栈的默认参数就是对应类型的参数,Base::show(int i=10)
*/
b->show();//需要动态绑定,运行时根据vftable进行函数调用
delete b;
return 0;
}
函数权限更改后,虚函数调用情况
#include<iostream>
#include<string>
using namespace std;
class Base
{
public:
virtual void show() { cout << "Base::show()" << endl; }
};
class Derive:public Base
{
private:
virtual void show() { cout << "Derive::show()" << endl; }
};
int main()
{
Base* b = new Derive();//Derive中的show方法变为私有的,是否可以访问
/*
成员方法能不能调用,就是说方法的访问权限是不是public的,是在编译阶段就需要确定好的。
最终能调用Derive::show()是在运行时期才确定的
*/
b->show();//需要动态绑定,编译阶段只能看见Base里面的show
delete b;
return 0;
}
虚继承
抽象类:有纯虚函数的类。vfptr/vftable
虚基类:继承过程中使用virtual修饰。vbptr/vbtable
virtual:修饰成员方法是虚函数。可以修饰继承方式,是虚继承。被虚继承的类,称作虚基类。
正常情况下的继承:
class A
{
public:
private:
int ma;
};
class B:public A
{
public:
private:
int mb;
};
class B:public A
改为class B: virtual public A
后,在派生类B中将会出现虚基类指针,并且会将基类的往后移(这里是特殊情况,一般继承的基类内容都在派生类前面)。并且在B的虚函数表中将会出现基类内容偏移的记录情况。
指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。所以这下面就会出现一个很严重的问题,内存释放起始的位置不对:
#include<iostream>
using namespace std;
class A
{
public:
virtual void func() { cout << "A:func()" << endl; }
private:
int ma;
};
class B:virtual public A
{
public:
virtual void func() { cout << "A:func()" << endl; }
private:
int mb;
};
int main()
{
A* p = new B();
p->func();
delete p;
return 0;
}
这里执行了A* p = new B();
,将p指向B的地址,所以会从B中虚基类的部分给A的指针p,所以在delete p;
的时候,就会出现从B对象的中间位置开始释放内存,导致程序崩溃(只有设计new和delete出现才会出现问题,主要是windows的vs studio,linux的g++不会出现问题)。
多继承/菱形继承
多重继承中存在巨大的缺点,就是继承的多个基类中可能存在多个相同的成员属性和成员方法。派生类可能会存在多个重复的基类,这就可以采用虚继承来实现。
多重继承的问题,可以有更多的代码复用。
下方代码展示了一个菱形继承问题,会导致在初始化D对象的时候重复调用相同的A的构造函数,导致内存开辟的资源的消耗。
#include<iostream>
using namespace std;
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;
}
通过将B和C类虚继承A类可以解决A的重复初始化。
class B:virtual public A;
class C:virtual public A;
D(int data) :A(data),B(data),C(data), md(data)
//需要显示调用A的构造函数,要不然没有默认构造函数A就会出错