文章目录
- 1. 什么是多态
- 2. 构成多态的条件
- 2.1 虚函数
- 2.2 虚函数的重写
- 2.3 final 和 override关键字
- 2.4 重载、重写、重定义对比
- 3. 虚函数表
- 4. 多态的原理
- 5. 多继承的虚表关系
- 6. 抽象类
当下网络有个热门词汇叫“双标”,意思就是用不同的标准来衡量人或事,这是一个贬义词。而在编程世界中,这种“双标”,我们称之为多态,当然了这里的多态并不是贬义词,而是一种技术实现。
比如说某种商城有会员机制,将用户分为普通用户、普通会员、尊贵会员等
那买同种东西的时候,不同的用户等级会有着不同的价格,这就是一种多态行为
实现多态性的主要构成条件是使用虚函数和继承:
只有类的成员函数才能被定义为虚函数,格式如下:
class A
{
//函数前面加上virtual 表面该成员函数为虚函数
virtual void func() {}
};
当派生类中有一个和基类完全相同的虚函数时,我们称这为虚函数的重写/覆盖
重写有三同,即:返回值类型、函数名、参数列表完全相同
class A
{
public:
//虚函数
virtual void func() const
{
cout << "A->func()" << endl;
}
};
class B :public A
{
public:
//虚函数重写
virtual void func() const
{
cout << "B->func()" << endl;
}
};
//多态调用传引用过去
void Print(const A& p)
{
p.func();
}
int main()
{
Print(A()); //A->func()
Print(B()); //B->func()
return 0;
}
在多态调用中,看的是指向的对象;而普通的函数调用,看的是当前的类型
虚函数的重写,还需注意几点:
虚函数父类必须加上virtual
修饰,子类虚函数重写前面可以不加virtual
,但在实际中,还是建议加上
对于虚函数的重写,我们规定三同,但是有例外——协变
即基类与虚函数返回值类型不同,但是返回值类型必须是构成父子关系指针或者引用(同时是指针 或 同时是引用)
class A
{
public:
//虚函数
virtual A* func() const
{
cout << "A->func()" << endl;
return 0;
}
};
class B :public A
{
public:
//虚函数重写 B和A是父子关系
virtual B* func() const
{
cout << "B->func()" << endl;
return 0;
}
};
void Print(const A& p)
{
p.func();
}
int main()
{
Print(A());
Print(B());
return 0;
}
析构函数的重写,基类和派生类的析构函数名不同
class A
{
public:
//虚函数
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
//虚函数重写
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* a1 = new A;
A* a2 = new B;
delete a1;
delete a2;
return 0;
}
这里的原因是因为编译器对析构函数的名字做了处理,编译后名称统一处理为destructor
,那为什么要将析构函数统一处理称destructor
呢?因为这里要让他们构成重写。如果不构成重写,就好出现类似这样的情况:
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
~B()
{
delete ptr;
cout << "~B()" << endl;
}
protected:
int* ptr;
};
int main()
{
A* a1 = new A;
delete a1;
a1 = new B;
delete a1;
return 0;
}
输出发现,我们这里new
了一个B对象,但是每次都是调用A的析构函数,这显然与我们的意愿不符,我们期望的是这个a1->destructor
形成的是多态调用,所以这样统一处理之后,就可以让他们构成重写
如果不想让这个虚函数被重写,可加上final
关键字修饰
当然了,final
也可以修饰类,让这个类不被继承,一般用于最终的类
如果要检查某个派生类是否重写了基类的某个虚函数,可用override
关键字修饰,如果没有重写,则编译报错
class A
{
public:
virtual void func()
{
cout << "func()" << endl;
}
protected:
int _a;
};
int main()
{
cout << sizeof(A) << endl;
}
这段代码如果不加上virtual
,则输出的是4;但是加上virtual
之后,输出的是16(64位下,指针是8字节,然后内存对齐)
这是因为有了虚函数,这个类里面会多一个虚函数表的指针,这些表里面存的是虚函数的地址
但如果将这个虚函数没有被重写,那么派生类的虚函数表还是指向基类的虚函数;如果重写了,则指向重写的虚函数。
所以多态调用的时候,不管我们传的是基类和派生类,在内存里看到的都是父类;普通调用是在编译的时候就确定了地址,而多态调用时,运行时会到指向对象的虚表找函数的地址
动态绑定与静态绑定:
- 静态绑定:在编译时确定调用哪个函数或方法。这是在编译器根据变量的静态类型(声明类型)来决定调用哪个函数
- 动态绑定:在运行时根据对象的实际类型来确定调用哪个函数或方法。这是通过虚函数(在基类中声明为虚函数,子类进行重写)实现的。动态绑定适用于通过基类指针或引用调用虚函数的情况,确保调用正确的派生类函数
在这里虚表的地址,是存储在哪里的呢?我们通过这段代码来验证
class A
{
public:
virtual void func()
{
cout << "A->func()" << endl;
}
virtual void Func()
{
cout << "A->Func()" << endl;
}
int _a;
};
class B :public A
{
public:
virtual void func()
{
cout << "B->func()" << endl;
}
};
void Print(A a)
{
a.func();
}
int main()
{
A aa;
B bb;
int a = 0;
printf("栈:%p\n", &a);
static int b = 0;
printf("静态区:%p\n", &b);
int* p = new int;
printf("堆:%p\n", p);
const char* str = "hello";
printf("常量区:%p\n", str);
//前四个字节,一定是虚表的地址
printf("虚表a:%p\n", *((int*)&aa));
printf("虚表b:%p\n", *((int*)&bb));
}
输出发现虚表的地址和常量区的地址隔的较近,所以我们可以得出结论:虚表的地址存储在常量区
另外,我们在Vs的监视窗口只能查看3个虚函数的地址,但这不代表这,内存里面只有三个虚函数的地址,我们可通过这段代码进行验证:
class A
{
public:
virtual void func1()
{
cout << "A->func1()" << endl;
}
virtual void func2()
{
cout << "A->func2()" << endl;
}
virtual void func3()
{
cout << "A->func3()" << endl;
}
};
class B :public A
{
virtual void func3()
{
cout << "B->func3()" << endl;
}
virtual void func4()
{
cout << "B->func4()" << endl;
}
};
//函数指针命名
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{
for (size_t i= 0; table[i]!=nullptr ; i++)
{
printf("[%d]:%p->", i, table[i]);
Func_Ptr f = table[i];
f();
}
printf("\n");
}
int main()
{
A a;
B b;
int vft1 = *((int*)&a);
PrintVFT((Func_Ptr*)vft1);
int vft2 = *((int*)&b);
PrintVFT((Func_Ptr*)vft2);
return 0;
}
有了虚表的概念,这我们就能理解,为什么构成多必须是通过基类的指针或引用调用虚函数。因为只有父类的虚表才能既能指向父类,又能指向子类。
那这里还有一个问题就是,为什么必须是指针或引用呢?
class A
{
public:
virtual void func()
{
cout << "A->func()" << endl;
}
virtual void Func()
{
cout << "A->Func()" << endl;
}
int _a;
};
class B :public A
{
public:
virtual void func()
{
cout << "B->func()" << endl;
}
};
void Print(A a)
{
a.func();
}
int main()
{
A a;
a._a = 1;
B b;
b._a = 10;
a = b;
A* pa = &b;
A& ref = b;
}
这段代码调试发现,子类赋值给父类,父类会进行切片,这里值会拷贝过去,但是虚表并不会拷贝;因为如果拷贝了虚表的话,这样父类对象中的虚表指向的是父类还是子类就混淆了
上面讲的内容,包括举得例子都是单继承的,所以就不再赘述。这里我们看一下多继承里面的虚表是怎样的
class A
{
public:
virtual void func1()
{
cout << "A->func1()" << endl;
}
virtual void func2()
{
cout << "A->func2()" << endl;
}
protected:
int _a;
};
class B
{
public:
virtual void func1()
{
cout << "B->func1()" << endl;
}
virtual void func2()
{
cout << "B->func2()" << endl;
}
protected:
int _b;
};
class C :public A, public B
{
public:
virtual void func1()
{
cout << "C->func1()" << endl;
}
virtual void funcC()
{
cout << "C->funcC()" << endl;
}
protected:
int _c;
};
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{
for (size_t i= 0; table[i]!=nullptr ; i++)
{
printf("[%d]:%p->", i, table[i]);
Func_Ptr f = table[i];
f();
}
printf("\n");
}
int main()
{
C c;
cout<<sizeof(c)<<endl;
int vft1 = *((int*)&c);
//int vft2 = *((int*)(char*)&c + sizeof(A));
B* ptr = &c;
int vft2 = *((int*)ptr);
PrintVFT((Func_Ptr*)vft1);
PrintVFT((Func_Ptr*)vft2);
}
通过验证,我们可以发现,C类里面有两张虚表,一张是A的,一张是B的。而C里面的虚函数funcC()
的虚表,是存放在第一张虚表里面
但是,我们这里发现,重写的func1()
函数,明明是一样的,但是地址却不一样,我们这段代码转到汇编代码查看
int main()
{
C c;
A* ptr1 = &c;
B* ptr2 = &c;
ptr1->func1();
ptr2->func1();
return 0;
}
我们发现,ptr1
是直接调用找个func1()
,而ptr2
最终调用的地址和ptr1
是一样的,但是在jump
的,寄存器减了一个8,这个减8正好是c
的地址。ptr1
不用修改是因为正好指向了c
的起始地址,内存不看类型,只看地址
菱形继承这里就不讲了,很混乱~
虚函数后面加上=0
,则这个函数为纯虚函数,包含了纯虚函数的类,叫做抽象类。
抽象类不能实例化出对象,之后继承的派生类也不能实例化对象,只能重写虚函数,派生类才能实例化出对象。这里规定了派生类必须重新虚函数,所以抽象类也叫接口类。
class A
{
public:
virtual void func() = 0;
};
class B :public A
{
public:
virtual void func()
{
cout << "B->func()" << endl;
}
};
class C :public A
{
public:
virtual void func()
{
cout << "C->func()" << endl;
}
};
void Func(A*a)
{
a->func();
}
int main()
{
Func(new B);
Func(new C);
return 0;
}
那么本期的分享就到这里咯,我们下期再见,如果还有下期的话。