C++多态

1,687 阅读6分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

什么是多态

同种消息不同的对象接受后产生不同的状态,知道是这个东西就行,不懂也没有什么问题,看后文就可以。

多态的定义及实现

多态是类继承时,对象去调用同一个对象产生不同的行为

  • 要构成多态的条件有两个

虚函数的重写 基类的对象或引用调用虚函数

虚函数的重写

  • 什么是虚函数?

类的成员函数加上关键字virtual就变成了虚函数。

  • 虚函数重写的条件

是虚函数,且函数名,返回值的类型,参数类型相同(三同) 三同,但是只有父类写virtual也构成重写 特殊情况:

  1. 其他条件相同,返回值的类型为父子对象或指针类型也构成重写——这个也叫做协变
  2. 析构函数的重新:虽然父子的析构函数名不一样,但是在编译看来是相同的,因为它都编译器统一处理为destructor。所以析构函数的重写只需要在基类上加上virtual就可以构成重写。

为什么对析构函数进行重写呢?

看下面这段代码:

class teacher
{
public:
	~teacher()
	{
		cout << "~teacher()" << endl;
	}
};
class student:public teacher
{
public:
	~student()
	{
		cout << "~student()" << endl;
	}
};
int main()
{
	teacher* a = new teacher;//1
	delete a;

	teacher* b = new student;//2
	delete b;
	
	return 0;
}

运行上面的代码:image.png 我们就会发现,1正常释放,2只是释放了基类的,没有释放父类的,这就会造成内存泄漏。 当我们写成虚函数virtual ~teacher(),构成多态之后,就可以全部正常的对子类释放(调用子类的析构函数时,先析构子类,再析构父类):image.png

C++11中的 overridefinal

final:修饰虚函数,表示该函数不能被重写 override:检查派生类中虚哈四年有没有被重写,没有被重写就会报错

抽象类

包含纯虚函数的类,叫做抽象类。 纯虚函数——虚函数后面加上一个=0 抽象类就是抽象,即**不能实例化出来对象。**派生类继承了也不能实例化出来对象,必要要进行重写,才能实例化出来对象。

class Base
{
public:
	virtual void print() = 0;
};
class Exten :public Base
{
};
int main()
{
	Base a;
	Exten b;
	return 0;
}

上面代码肯定会报错,image.png 如果想让派生类Exten可以实例化出来对象,必须重写

class Exten :public Base
{
public:
	virtual void print()
	{
		cout << "可以实例化对象" << endl;
	}
};

接口继承和实现继承

虚函数的继承是接口继承,目的是为了重写,接口继承就是函数的声明继承下来,定义不继承,会重写定义。 实现继承:普通函数的继承就是实现继承,包基类中的函数全部继承下来。

多态实现的原理

虚函数表

那些虚函数都放在哪里呢?虚函数放在虚函数表中,所以的虚函数都放在学函数表中 类中有个虚函数表的指针,指向这个表,在vs2019中,这个指针为vfptr

class teacher
{
public:
	virtual void print()
	{
		cout << "void print()" << endl;
	}
};
//main
teacher a;

image.png

实现多态后,派生类中的虚函数表

class teacher
{
public:
	virtual void print()
	{
		cout << "teacher void print()" << endl;
	}
	virtual void f1()
	{
		cout << "teacher void f1()" << endl;
	}
};
class student :public teacher
{
public:
	virtual void print()
	{
		cout << "student void print()" << endl;
	}

};
int main()
{
	teacher a;
	student b;
	
	return 0;
}

image.png

通过上面的代码和调试信息可以看出,在派生类中,虚表中的print被重写成studen类中的。

  • 那么多态的特性是怎么实现的

还是上面的代码,测试不一样

int main()
{
	teacher a;
	student b;

	teacher& x = a;
	x.print();
	
	teacher& y = b;
	y.print();
	return 0;
}

运行结果:

image.png

  • 分析

image.png

x调用print直接去基类的虚表中找 y调用print去派生类的虚表中找,此时的虚表中的print已经重写。 这也是为什么说,指向哪里调哪里——父类的指针或引用指向父类调父类,指向子类调子类。

动态绑定,静态绑定

  • 静态绑定:

编译的时候就确定地址,比如:函数重载,模板

  • 动态绑定

运行的时候去找地址,比如多态

显然上述的代码就是动态绑定,在程序运行起来之后,去找print的地址。 要想观察这个调用print是什么方式的,需要看一下汇编代码。

单继承虚函数表

上面那个代码就是单继承,但是上面那个代码中,派生类没有写自己的虚函数,只是不继承的虚函数重写了。我们知道只要是虚函数都会放在虚函数表中,但是vs的窗口不能显示出来。

class student :public teacher
{
public:
	virtual void print()
	{
		cout << "student void print()" << endl;
	}
	virtual void f2()
	{
		cout << "student void f2()" << endl;
	}
};

image.png

我们看不见派生类中的f2函数,但是它确实咋虚函数表里面,下面我们写一个程序把它打印出来。 想打印出来它,就要先取到他的地址,然后还要知道它是什么类型?

  1. 取到它的地址 直接取对象的地址就可以,虚表的指针都放在对象的第一个位置
  2. 什么类型的?

虚表的指针它是一个函数指针数组指针,什么意思呢?——它是一个指针,它指向一个数组,数组的每个元素都是一个函数指针。

typedef void(*VF)();
void printvf(VF* arr)
{
	for (int i = 0; i < 3; i++)
	{
		printf("%p", arr + i);
		arr[i]();	
	}
}
//main中调用
printvf((VF*)*((int*)(&b)));

image.png

从打印的结果上看,就可以证明上面我说的了。 当我们调换派生类中printf2的位置的时候也是打印相同的结果;说明虚表中先继承基类的虚函数然后再放自己的虚函数。基类的虚函数是按声明的顺序储存在虚表中。

多继承虚函数表

我们思考派生类中没有重写的虚函数是单独放在一个虚表中,还是放在哪个继承的虚表中 下面我们用代码测试一下

class A
{
public:
	virtual void fA()
	{
		cout << "void fA()" << endl;
	}
};
class B:public A
{
public:
	virtual void fB()
	{
		cout << "void fB()" << endl;
	}
};
class C:public A
{
public:
	virtual void fC()
	{
		cout << "void fC()" << endl;
	}
};
class D :public B, public C
{
public:
	virtual void fD()
	{
		cout << "void fD" << endl;
	}
	virtual void fun()
	{
		cout << "void fun" << endl;
	}
};
typedef void(*VF)();
void printvf(VF* arr,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%p", arr + i);
		arr[i]();
	}
}
int main()
{
	cout<<"测试是否在第一个虚表" << endl;
	D d;
	printvf((VF*)*(int*)(&d), 3);//有的话就是3个
	
	cout << "测试是否在第二个虚表" << endl;
	C* c = &d;
	printvf((VF*)*(int*)c, 3);

	return 0;
}

直接看结果:image.png 可以看出多继承有多个虚表,子类没有重写的函数放在第一个虚表中

面试常见的问题

  1. inline函数可以是虚函数吗?
  2. 静态成员可以是虚函数吗?
  3. 构造函数,拷贝构造,赋值运算符的重载可以是虚函数吗?
  4. 析构函数可以是虚函数吗?
  5. 对象访问普通函数快还是虚函数快
  6. 虚函数表在什么阶段产生的,存在哪里?
  1. inline可以是虚函数,inline只是建议编译器把函数当作内联函数,但是,内联函数在编译的时候就展开了,没有函数栈帧的开辟,而虚函数在要在运行的时候去虚函数表中去早该函数的地址。
  2. 静态的成员不能是虚函数,静态成员没有*this指针,静态函数只能用类域的方式调用,而虚函数的调用需要在虚函数表在中调用。
  3. 构造函数和拷贝构造函数不能是虚函数。因为虚函数是放在虚函数表中,而虚表指针是在构造函数初始化列表中初始化的。赋值运算符的重载是可以是虚函数的
  4. 析构函数可以是虚函数,虽然析构函数的函数名不一样,但是在编译器看来,都被处理为destructor,上文有解释为什么要把析构函数写成虚函数。
  5. 如果是普通的函数,那么是一样快的,如果构成多态,普通函数快
  6. 虚函数表在编译阶段就生成了,存在内存中的代码段