C++之多态 虚函数表

多态

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

需要区分一下:1、菱形虚拟继承,是在继承方式前面加上virtual

class Person {};
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};

2、而多态是单继承中,某个函数接口前面加上virtual

class Person {
public:
    //虚函数
	virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
class Student : public Person {
public:
    //虚函数的重写
	virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};

多态调用

普通调用:跟要调用这个函数的对象类型有关;

多态调用:跟指针或者引用指向的对象有关。

//引用
void func(Person& p)//普通调用的话,都是调用Person类所指向的函数,这里是多态
{
	p.BuyTicket();//跟指针或者引用指向的对象不同调用不同的虚函数
}
int main()
{
	Person adult;
	Student stu;
    //两个不同类对象调用同一个函数,看一下多态的实验结果
	func(adult);//引用指向的对象是Person类,故输出 adult-买票-全价
	func(stu);//引用指向的对象是Student类,故输出 Student-买票-半价
	return 0;
}
------------------------------------
//指针
void func(Person* p)//多态
{
	p.BuyTicket();//跟指针或者引用指向的对象不同调用不同的虚函数
}
int main()
{
	Person adult;
	Student stu;
	func(&adult);//输出 adult-买票-全价
	func(&stu);//输出 Student-买票-半价
	return 0;
}

所以多态是根据指针/引用指向对象的类型来决定调用某个重写的虚函数。如果函数变成void func(Person p);【不是引用也不是指针】就不是多态了。

多态的构成条件

  1. 必须通过父类的指针或者引用调用虚函数;【父类对象不可以】
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全票" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生票" << endl;
	}
};

void func(Person& p)//对于该函数,看到的都是父类对象,子类对象中则为父类对象那一部分切割或者切片出来的,但是多态,可以让p根据指向或者引用的对象来调用不同的重写函虚数
{
	p.BuyTicket();
}

int main()
{
	Person adult;
	Student stu;
    //两个不同类对象调用同一个函数
	func(adult);
	func(stu);
	return 0;
}

注意:切割(切片)是子类对象可以赋值给父类对象/指针/引用。就是拿着子类对象中父类的那一部分,调用父类的拷贝构造再生成一个父类。

虚函数

即被virtual修饰的类成员函数称为虚函数。子类的虚函数可以不加virtual。

即三同【函数名、参数、返回值】函数满足多态的条件为:1、父类虚函数,子类虚函数;2、父类虚函数,子类该同名函数不加virtual。

注意:父类不加virtual,子类加virtual;父类和子类都不加virtual;这两种情况都不满足多态的条件。

可以写为虚函数的有:析构函数【很推荐】、内联函数【写成虚函数后,就不是内联函数了】

不可以是虚函数的有:友元函数、构造函数、static类型的成员函数;子类不一定要重新定义父类的虚函数,视情况而定。

虚函数的重写-三同

虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名参数列表完全相同),称子类的虚函数重写了父类的虚函数。

例外1:协变

三同中,返回值不同,但要求返回值必须是父类/子类(包括自己和其他类)的指针或引用

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用;或者父类虚函数和子类虚函数均返回父类对象的指针或应用;或者父类虚函数返回其他父类的指针或引用,子类虚函数返回其他子类对象的指针或引用;或者父类虚函数和子类虚函数均返回其他父类对象的指针或应用

例外2:析构函数建议重写

三同中,看似函数名不同,实际上相同,均为destructor。如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

//类A的对象析构会先调用~A
class A
{
public:
	~A()
	{
		delete[] _a;
		cout << "A析构";
	}
protected:
	int* _a = new int[10];
};
//类B的对象析构会先调用~B,再自动调用~A
class B:public A
{
public:
	~B()
	{
		delete[] _b;
		cout << "B析构";
	}
protected:
	int* _b = new int[10];
};
int main()
{
    A* ptr1 = new A;
	A* ptr2 = new B;
	//这是普通调用!根据参数类型来调用,类型都是A*
	delete ptr1;//调用A的析构函数
	delete ptr2;//调用A的析构函数
    //存在内存泄漏问题
    ----------
    virtual ~A() {};//将A的析构函数变成虚函数,B的也加上
    //此时是多态调用!根据指针所指向的类型来调用
    delete ptr1;//调用A的析构函数
	delete ptr2;//调用B的析构函数,再调用A的析构函数
    return 0;
}

分析此处delete行为【使用 delete 释放 C++ 类对象的内存时,将在释放该对象的内存之前调用该对象的析构 函数(如果该对象具有析构函数)】:1、使用指针调用析构函数;2、operator delete(ptr);

建议:父类的虚构函数无脑加virtual,即可构成重写,避免内存泄漏。

如何实现一个不能被继承的类?

  1. 父类的构造函数设成私有成员函数【C++98的方法】
  2. 在类定义时+final【C++11的方法】class Person final {};此时Person称为最终类。

final和override

final:修饰父类虚函数,表示该虚函数不能再被重写【既可以修饰-不能被继承,也可以修饰父类虚函数–不能被重写】

override: 检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错。

再次区分重写(覆盖)、隐藏(重定义)、重载

隐藏(重定义):父类和子类的函数,函数名(1同)即可;

重写(覆盖):父类和子类的虚函数,返回值、函数名、参数列表(3同)即可;

重载:同一个作用域,函数名相同(1同),返回值和参数列表不同

在这里插入图片描述

抽象类(纯虚函数)

包含纯虚函数的类叫做抽象类(也叫接口类)。【在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。】

  • 抽象类不能实例化出对象子类继承后也不能实例化出对象
  • 只有重写/实现纯虚函数,派生类才能实例化出对象(此时子类对象中也包含了父类对象)。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
  • override是检查重写,而抽象类是强制重写。
  • 纯虚函数可以有函数体

如果一个类在实际生活中没有具体的对象,可以考虑将这个类定义为抽象类。

接口继承和实现继承

  1. 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  2. 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

牢记:多态是接口继承,子类用的是父类的虚函数接口!!

//情况1:构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
	virtual void test(){ func();}//this->func()
    //这里是父类指针this(类型为A*)调用该虚函数,且该虚函数已经完成重写,此时符合多态调用
    //多态调用是看实际指向对象的类型,p是子类对象的指针B*,故调用的是B类的func函数
    //因为多态调用是接口继承,故使用的是A类的func接口(用A类的缺省参数)
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();//输出结果是B->1
	return 0;
}
//情况2:不构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
	virtual void test(){ func();}//this->func,func是B*类型,所以不构成多态【多态要求是父类指针/引用调用】
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();//输出结果是B->0
	return 0;
}
//情况3:不构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
	virtual void test(){ func();}//this->func,func是B*类型,所以不构成多态【多态要求是父类指针/引用调用】
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->func();//输出结果是B->0
	return 0;
}
//情况4:编译报错
class A
{
public:
	virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}//A没有缺省参数,会报错
	virtual void test(){ func();}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->func();//编译报错
	return 0;
}

类的大小–虚函数表

单继承的虚表指针

先给出结论:

当父类定义了虚函数时,在子类进行继承的时候会将父类的虚函数表也给继承下来所以那一些虚函数在子类中也是virtual类型的,如果要对父类中的虚函数进行重写时或添加虚函数,顺序是:

  1. 先将父类的虚函数列表复制过来
  2. 重写虚函数时是把从父类继承过来的虚函数表中对应的虚函数进行相应的替换。
  3. 如果子类自己要添加自己的虚函数,则是把添加的虚函数加到从父类继承过来虚函数表的尾部。

注意:

  1. 严格说是同一个的类的不同对象都有各自的虚函数表,只是指向相同的虚函数,虚函数是共用的
  2. 子类和父类中的虚函数表中没重写的虚函数也是共用的,不是共有虚表哦!

先看一道笔试题

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
//sizeof(Base) = 8;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
};
//sizeof(Base) = 4;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
    char _c;//内存对齐
};
//sizeof(Base) = 12;

除了private成员,还多了一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为该类内的虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。本质是函数指针数组。不是虚函数就不会进虚基表。

在这里插入图片描述

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。【VS2013版本下是这样】

如何实现多态:当父类的指针或者引用指向父类对象时,对应的是父类对象的模型;当父类的指针或引用指向子类对象时,是子类对象的父类对象的切片【如果子类对象重写了虚函数,那么虚函数表会不一样】,以上这两种情况对应的对象模型是一样的,但是虚表不一定相同。多态实际是依靠虚表来实现的,里面虚函数的地址不一样。这里涉及的是动态绑定。

自己写一个代码访问虚表里的虚函数

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int _a;
};

class B: public A
{
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
	void func4() { cout << "B::func4" << endl; }
private:
	int _b;
};

typedef void(*VF_Ptr)();//类中的4个函数声明都是一样的,故可以typedef一样的
void PrintVFTable(VF_Ptr vft[])//传的是函数指针数组
{
	for (int i = 0; vft[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
}

int main()
{
	A a;
	B b;
	PrintVFTable((VF_Ptr*)(*((void**)&a)));//此代码在32/64位平台均可,void**解引用得到void*,那void*在不同平台大小也不一样,故可适应//只要是个二级指针就可以

	//把虚表打印出来
	PrintVFTable((VF_Ptr*)(*((int*)&a)));//仅适合32位平台--指针大小位4自己
	PrintVFTable((VF_Ptr*)(*((int*)&b)));
}

多继承的虚表指针

结论:

  1. 该类继承自几个不同对象就有多少个虚函数指针。

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

//访问第一张虚表
PrintfVFTable((VFPtr*)(*(void**)&d));
//访问第二张虚表
PrintfVFTable((VFPtr*)(*(void**)((char*)&d+sizeof(Base1))));
//访问第二张虚表
Base2* ptr = &d;
PrintfVFTable((VFPtr*)(*(void**)(ptr));

在这里插入图片描述

菱形继承的虚表指针

和多继承下的对象模型差不多,具体看图

菱形虚拟继承的虚表指针

最后继承的那个子类D要重写父类的虚函数,因为菱形虚拟继承只会有一份父类A,而B和C都重写了A中的虚函数,那D对象模型中的A的虚表就不知道应该放谁重写的虚函数,当D重写以后,A的虚表中放D重写的虚函数地址即可。

在这里插入图片描述

虚函数在哪?虚表在哪?

通过代码验证一下

int main()
{
	int a = 0;
    cout << "栈:" << &a << endl;//0083FBC0
    
    int* p = new int;
    cout << "堆:" << p << endl;//00CCA430
    
    const char* str = "aaa";
    cout << "代码段/常量区:" << (void*)str << endl;//00729B78
    
    static int b = 0;
    cout << "静态区/数据段:" << &b << endl;//0072C400
    
    A a;
    cout << "虚表:" << (void*)*((int*)&a) << endl;//00729B34
	return 0;
}

注意:上述地址每次运行都不一样。只需要看地址区间即可,可看到虚表是在代码段/常量区在类对象里的是虚表指针虚表存的是虚函数指针虚函数和普通函数一样的,都是存在代码段的

虚表对同一个类创建的多个对象而言,是共享的,就只有一份,所以在常量区。不同类的虚表不一样。

静态绑定和动态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。编译时的多态性是通过函数重载和模板体实现的

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。根据指针指向的类型去调用具体的函数【父类对象本身或父类对象的切片==>虚函数表不一样】,运行时的多态性是通过虚函数实现的

题目

class A
{
public:
    A ():m_iVal(0){test();}
    virtual void func() { std::cout<<m_iVal<<‘ ’;}
    void test(){func();}
public:
    int m_iVal;
};

class B : public A
{
public:
    B(){test();}
    virtual void func()
    {
        ++m_iVal;
        std::cout<<m_iVal<<‘ ’;
    }
};

int main(int argc ,char* argv[])
{
    A*p = new B;
    p->test();
    return 0;
}
//输出0 1 2
分析:new B时先调用父类A的构造函数,执行test()函数,再调用func()函数
    由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0
    构造完父类后执行子类构造函数,又调用test函数,然后又执行func()
    由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1,
    最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2
class A
{
public: 
    virtual void f()
    {
        cout<<"A::f()"<<endl;
    }
};
class B : public A
{
private:
    virtual void f()
    {
        cout<<"B::f()"<<endl;
    }
};
A* pa = (A*)new B;
pa->f();
//输出B::f()
分析:虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值