本文正在参加「金石计划 . 瓜分6万现金大奖」
什么是多态
同种消息不同的对象接受后产生不同的状态,知道是这个东西就行,不懂也没有什么问题,看后文就可以。
多态的定义及实现
多态是类继承时,对象去调用同一个对象产生不同的行为
- 要构成多态的条件有两个
虚函数的重写 基类的对象或引用调用虚函数
虚函数的重写
- 什么是虚函数?
类的成员函数加上关键字
virtual就变成了虚函数。
- 虚函数重写的条件
是虚函数,且函数名,返回值的类型,参数类型相同(三同) 三同,但是只有父类写
virtual也构成重写 特殊情况:
- 其他条件相同,返回值的类型为父子对象或指针类型也构成重写——这个也叫做协变
- 析构函数的重新:虽然父子的析构函数名不一样,但是在编译看来是相同的,因为它都编译器统一处理为
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;
}
运行上面的代码:
我们就会发现,1正常释放,2只是释放了基类的,没有释放父类的,这就会造成内存泄漏。 当我们写成虚函数
virtual ~teacher(),构成多态之后,就可以全部正常的对子类释放(调用子类的析构函数时,先析构子类,再析构父类):
C++11中的 override和final
final:修饰虚函数,表示该函数不能被重写override:检查派生类中虚哈四年有没有被重写,没有被重写就会报错
抽象类
包含纯虚函数的类,叫做抽象类。 纯虚函数——虚函数后面加上一个
=0抽象类就是抽象,即**不能实例化出来对象。**派生类继承了也不能实例化出来对象,必要要进行重写,才能实例化出来对象。
class Base
{
public:
virtual void print() = 0;
};
class Exten :public Base
{
};
int main()
{
Base a;
Exten b;
return 0;
}
上面代码肯定会报错,
如果想让派生类
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;
实现多态后,派生类中的虚函数表
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;
}
通过上面的代码和调试信息可以看出,在派生类中,虚表中的
studen类中的。
- 那么多态的特性是怎么实现的
还是上面的代码,测试不一样
int main()
{
teacher a;
student b;
teacher& x = a;
x.print();
teacher& y = b;
y.print();
return 0;
}
运行结果:
- 分析
x调用
动态绑定,静态绑定
- 静态绑定:
编译的时候就确定地址,比如:函数重载,模板
- 动态绑定
运行的时候去找地址,比如多态
显然上述的代码就是动态绑定,在程序运行起来之后,去找print的地址。
要想观察这个调用print是什么方式的,需要看一下汇编代码。
单继承虚函数表
上面那个代码就是单继承,但是上面那个代码中,派生类没有写自己的虚函数,只是不继承的虚函数重写了。我们知道只要是虚函数都会放在虚函数表中,但是vs的窗口不能显示出来。
class student :public teacher
{
public:
virtual void print()
{
cout << "student void print()" << endl;
}
virtual void f2()
{
cout << "student void f2()" << endl;
}
};
我们看不见派生类中的f2函数,但是它确实咋虚函数表里面,下面我们写一个程序把它打印出来。 想打印出来它,就要先取到他的地址,然后还要知道它是什么类型?
- 取到它的地址 直接取对象的地址就可以,虚表的指针都放在对象的第一个位置
- 什么类型的?
虚表的指针它是一个函数指针数组指针,什么意思呢?——它是一个指针,它指向一个数组,数组的每个元素都是一个函数指针。
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)));
从打印的结果上看,就可以证明上面我说的了。 当我们调换派生类中
f2的位置的时候也是打印相同的结果;说明虚表中先继承基类的虚函数然后再放自己的虚函数。基类的虚函数是按声明的顺序储存在虚表中。
多继承虚函数表
我们思考派生类中没有重写的虚函数是单独放在一个虚表中,还是放在哪个继承的虚表中 下面我们用代码测试一下
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;
}
直接看结果:
可以看出多继承有多个虚表,子类没有重写的函数放在第一个虚表中
面试常见的问题
inline函数可以是虚函数吗?- 静态成员可以是虚函数吗?
- 构造函数,拷贝构造,赋值运算符的重载可以是虚函数吗?
- 析构函数可以是虚函数吗?
- 对象访问普通函数快还是虚函数快
- 虚函数表在什么阶段产生的,存在哪里?
inline可以是虚函数,inline只是建议编译器把函数当作内联函数,但是,内联函数在编译的时候就展开了,没有函数栈帧的开辟,而虚函数在要在运行的时候去虚函数表中去早该函数的地址。- 静态的成员不能是虚函数,静态成员没有
*this指针,静态函数只能用类域的方式调用,而虚函数的调用需要在虚函数表在中调用。- 构造函数和拷贝构造函数不能是虚函数。因为虚函数是放在虚函数表中,而虚表指针是在构造函数初始化列表中初始化的。赋值运算符的重载是可以是虚函数的
- 析构函数可以是虚函数,虽然析构函数的函数名不一样,但是在编译器看来,都被处理为
destructor,上文有解释为什么要把析构函数写成虚函数。- 如果是普通的函数,那么是一样快的,如果构成多态,普通函数快
- 虚函数表在编译阶段就生成了,存在内存中的代码段