同种消息不同的对象接受后产生不同的状态,知道是这个东西就行,不懂也没有什么问题,看后文就可以。
多态是类继承时,对象去调用同一个对象产生不同的行为
虚函数的重写
基类的对象或引用调用虚函数
类的成员函数加上关键字
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()
,构成多态之后,就可以全部正常的对子类释放(调用子类的析构函数时,先析构子类,再析构父类):
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调用
y调用
这也是为什么说,指向哪里调哪里——父类的指针或引用指向父类调父类,指向子类调子类。
- 静态绑定:
编译的时候就确定地址,比如:函数重载,模板
- 动态绑定
运行的时候去找地址,比如多态
显然上述的代码就是动态绑定,在程序运行起来之后,去找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
,上文有解释为什么要把析构函数写成虚函数。- 如果是普通的函数,那么是一样快的,如果构成多态,普通函数快
- 虚函数表在编译阶段就生成了,存在内存中的代码段