多态是面向对象编程的核心
文章目录
面向对象是使用多态性获得对系统中每个源代码依赖项的绝对控制的能力
面向对象语言之前的多态性是手动的并且有风险,因为工程师必须明确地使用指向函数的指针。然而,面向对象编程语言使多态更容易、更安全,使工程师能够最大限度地发挥多态的好处。
面向对象语言中的多态性为工程师提供了强大的能力来构建一个对扩展开放的更灵活的系统。尤其是在频繁更改的易变系统中,依赖倒置有助于打破耦合。
高内聚、低耦合是程序设计的目标,而多态是高内聚、低耦合的基础。
目录
一、多态与静态绑定
在编程语言和类型论中,多态指为不同的数据类型提供统一的接口。多态意味着调用成员函数时,会根据调用成员函数对象的类型来执行不同的函数。
对于面向对象来说,由于虚函数被继承后需要在子类中重写函数体,(若子类不重写,则无法创建对象;若祖父类中有虚函数,父类重写了,子类未重写,访问时则会调用父类重写的函数,具体可见上一篇继承)当访问虚函数时则访问创建对象中被重写的函数,即使是父类指针使用子类初始化也是如此。
class A
{
public:
virtual void print()=0;
virtual void Print()
{ cout << "A"<<endl;}
};
class B:public A
{
public:
void print()
{ cout<< "B"<<endl; }
void Print()
{ cout << "B"<<endl;}
};
int main()
{
A* pA = new B;
pA->print();//输出B
pA->Print();//输出B
delete pA;
}
静态绑定:将名称绑定到一个固定的函数定义,然后每次调用该名称时执行该定义,这个也是一种常态的执行方式。
静态绑定缺点:调用函数时只根据对象的类型调用函数,在使用指针访问时并不会因为更换指向对象而更换函数。
class A
{
public:
A(int _a):a(_a){}
void print() const
{ cout<< "A为:" << a << endl;}
protected:
int a;
};
class B:public A
{
public:
B(int _b):A(a){}
void print() const
{ cout<< "B为:" << a << endl;}
};
class C:public B
{
public:
C(int _c):B(a) {}
void print() const
{ cout<< "C为:" << a << endl;}
};
int main()
{
A a(1);
a.print();
B b(2);
b.print();
C c(3);
c.print();
//输出:
//A为:1
//B为:2
//C为:3
A* pA = new A;
pA = &a;
pA->print();
pA = &b;
pA->print();
pA = &c;
pA->print();
//输出:
//A为:1
//A为:2
//A为:3
}
可以看出当指针访问时,虽然参数使用的是子类参数,但是输出函数还是父类,与之前提到的父类指针使用子类初始化情况一样,其实就是继承中函数同名问题。
二、虚函数与动态绑定
将需要动态绑定的函数全部设置成虚函数,子类和父类都要设置成虚函数,否则之后继承的子类若忘记重写函数,则直接调用之前没有设置成虚函数的父类中的函数(类的继承特性)。
class A
{
public:
virtual void Print()
{
cout << "A" << endl;
}
};
class B :public A
{
public:
void Print()
{
cout << "B" << endl;
}
};
class C :public B
{
public:
};
class D :public B
{
public:
virtual void Print()
{
cout << "D" << endl;
}
};
class E :public D
{
public:
};
int main()
{
A* pA = new C;
pA->Print();//输出B
D d;
B* pB = new B;
pB = &d;
pB->Print();//输出D,因为在A类中已经声明了Print是虚函数,只要重写Print()就能调用D类中Print()函数
E e;
e.print();//输出D
}
三、多态的应用场景
多态的两个应用场景:
1.函数
2.存储进入Collections
多态与Collections:Collections可以存储值类型(但是值类型不能满足多态),可以存储指针类型,不可以存储引用类型。
函数:
class classroom
{
public:
classroom() = default;
classroom(string name,int age):name(name),age(age){}
virtual void print()
{
cout << "class"<<endl;
}
protected:
string name;
int age;
};
class boy:public classroom
{
public:
boy(string name,int age):classroom(name,age){}
virtual void print()
{
cout << "boy:"<<name<<"年龄:"<<age<<endl;
}
};
class girl:public classroom
{
public:
girl(string name,int age):classroom(name,age){}
virtual void print()
{
cout << "girl:"<<name<<"年龄:"<<age<<endl;
}
};
void print(classroom * a)
{
a->print();
}
int main()
{
boy b("张三",18);
girl g("小芳",19);
classroom* a;
a = &b;
print(a);
a = &g;
print(a);
//输出:
//boy:张三年龄:18
//girl:小芳年龄:19
}
Collections:
集合值类型无法实现多态,指针类型才可以
class classroom
{
public:
classroom() = default;
classroom(string name, int age) :name(name), age(age) {}
virtual void print()
{
cout << "class" ;
}
protected:
string name;
int age;
};
class boy :public classroom
{
public:
boy(string name, int age):classroom(name,age) {}
virtual void print()
{
cout << "boy:" << name << "年龄:" << age << endl;
}
};
class girl :public classroom
{
public:
girl(string name, int age):classroom(name,age) {}
virtual void print()
{
cout << "girl:" << name << "年龄:" << age << endl;
}
};
int main()
{
classroom c;
boy b("张三",18);
girl g("小芳",19);
classroom s[] = {c,b,g};//值类型数组
classroom * s_ptr[] = { &c,&b,&g };//指针类型
for (auto S : s)
{
S.print();//输出为classclassclass
}
for (auto S_ptr : s_ptr)
{
S_ptr->print();//输出:
//class
//boy:张三年龄:18
//girl:小芳年龄:19
}
}
四、多态对象的大小
class A
{
public:
void print()
{ cout<<"A"<<endl; }
};
class B
{
public:
virtual void print()
{ cout<<"B"<<endl; }
};
int main()
{
A a;
B b;
cout << sizeof(a) << endl;//1个字节
cout << sizeof(b) << endl;//8个字节(64位)4个字节(32位)
return 1;
}
1.成员函数不占类空间,存放在代码区,1个字节大小并不是函数成员的所占的内存,而是用来标记类的内存地址的。
2.虚函数不占类的内存空间,8个字节大小(32位4个字节)的内存占用是因为生成了虚函数表,类中会产生一个指向虚函数表的指针(_vfptr),这个8(4)字节大小的是指针占用的内存。
虚函数表是实现多态的关键,虚函数里面存储所有重写的函数的函数指针,通过访问虚函数表我们可以找到其重写的函数
typedef void(*fun)();//给函数指针改名为fun方便下面书写
int main()
{
A a;
B b;
int* p = (int*)&b;//取虚函数表地址,要在x86下运行因为x64下指针大小为8个字节,int储存不下
cout << *p << endl;//输出虚函数表地址
fun f = (fun)*(int*)*p;//对虚函数表首个元素解引用转地址再强转最后赋给函数指针变量f
f();//调用函数f看是什么函数
return 0;
}
根据输出很明显可以得到虚函数表第一个函数指针指向的就是B类中重写的函数Print()
虚函数表:
多态的类都拥有自己的虚函数表,若派生类没有自己的虚函数则虚函数表都来自与基类,若有多个基类,那么有几个基类中含有虚函数,派生类中就有几个虚函数表。
例:派生类E继承于基类A、B、C、D,其中A、B、C中拥有虚函数,我们可以在VS中监视一下E类。
可以看出E类中产生了3个虚函数指针指向三个虚函数表,所以占内存因为24字节(12字节),虚函数表在编译时期就已经生成了存储在常量区,所以不占用类中内存。
五、Override和Final
Override:
为了避免派生类重写继承的虚函数产生错误,可以利用override关键词进行检查,若继承同名虚函数不存在或参数不一致则会报错,可以在所有继承需要重写的虚函数后添加此关键词避免出错。
class A
{
public:
virtual void school(){}
virtual int classroom(){return 1;}
virtual void boy(int a){}
virtual void girl(int x){}
};
class B:public A
{
public:
virtual void School() override {}//无法通过编译,因为S大写后函数名改变,继承的同名虚函数中没有School()
virtual void classroom()override {}//无法通过编译,与继承的虚函数返回类型不同
virtual void boy(string a)override {}//无法通过编译,与继承的虚函数参数不同
virtual void girl(int x)override {}//可以通过编译
};
Final:
当在基类的成员虚函数中有你不想让派生类重写的函数,那么在此函数后添加关键词final,派生类就无法重写此函数
class A
{
public:
virtual void fun() final {}
};
class B:public A
{
public:
virtual void fun() {}//无法通过编译,因为基类中fun()函数已经声明为final
};
六、函数重载与多态
函数重载:可以将语义、功能相近的多个函数共用一个函数名,但函数参数的类型、顺序、个数必须有一项不同才能重载(必要条件),函数返回值可以不同可以相同。
学习到这里时,up主视频PPT中有一句是:
多态对象不能调用子类重载函数,但可以调用父类重载函数
我个人觉得有一点需要补充的,多态对象是不能调用的是子类直接重载的父类的函数,但是子类可以调用子类重写的父类重载函数。
若子类直接重载从父类继承而来的虚函数,重载的函数指针并不会存放到虚函数表中,因为其并不符合虚函数重写规则如果在后面加override会直接报错,具体可以看上面一节,所以多态无法调用,只能调用未重写的继承来的重载函数,有些拗口,我们可以直接看代码。
class A
{
public:
virtual void fun() {}
virtual void fun(int x)
{
cout << "A" << endl;
}
};
class B :public A
{
public:
virtual void fun() {}
virtual void fun(int x,int y) //这相当于对继承来的virtual void fun(int x)进行了重载,多态无法调用
{
cout << "B" << endl;
}
};
int main()
{
A* pA = new B;
pA->fun(1,2);//无法通过编译,因为多态无法调用子类中不在虚函数表中的函数
pA->fun(1);//可以通过编译,输出是A。因为访问的是虚函数表中未被重写的函数即父类的函数
delete pA;
return 0;
}
根据虚函数表的知识,我们只需要将继承来的父类重载函数进行重写,就可以使多态调用。
class A
{
public:
virtual void fun() {}
virtual void fun(int x)
{
cout << "A" << endl;
}
};
class B :public A
{
public:
virtual void fun() {}
virtual void fun(int x) override
{
cout << "B" << endl;
}
};
int main()
{
A* pA = new B;
pA->fun(1);//输出B
delete pA;
return 0;
}
七、析构函数和多态
父类指针被子类对象初始化时析构会出现问题,子类对象析构时只调用父类析构函数,并不会调用子类析构函数,需要将父类析构函数写为虚函数,具体代码解释可以看上一篇关于C++继承。
八、Dynamic_cast类型转换
多态缺点:无法调用子类中其他函数(不是继承来重写的虚函数)。
Dynamic_cast是一种将基类指针强转成子类指针的方法,但是必须是多态的,即至少有一个虚函数,用到的场景很少。
class A
{
public:
virtual void name()
{
cout << "张三" << endl;
}
};
class B :public A
{
public:
virtual void name()
{
cout << "小芳" << endl;
}
void age()
{
cout << "18" << endl;
}
};
int main()
{
A* a = new B;
B* b = dynamic_cast<B*>(a);
b->age();
return 0;
}
个人觉得对于简单的单继承关系没啥用有点多余,会用就行。
九、typeid()操作符
typeid()可以在运行时获得类型名称,typeid()是运算符不是函数
typeid()可以在复杂派生环境中,当只能获得基类指针是来判断多态派生类是否是你期望的派生类。
class A
{
public:
virtual void name()
{
cout << "张三" << endl;
}
};
class B :public A
{
public:
virtual void name()
{
cout << "小芳" << endl;
}
void age()
{
cout << "18" << endl;
}
};
void juge(A* a)
{
if (typeid(*a) == typeid(B))
{
cout << "correct!" << endl;
}
}
int main()
{
A* a = new B;
cout << typeid(a).name() << endl;
cout << typeid(*a).name() << endl;
juge(a);
return 0;
}
十、纯虚函数与抽象类
纯虚函数:没有函数体的函数
抽象类:存在纯虚函数的类就是抽象类,抽象类无法实例化创建对象
抽象类的派生函数必须重写纯虚函数,否则无法通过编译。
代码示例看上篇继承中的抽象类
十一、接口式的抽象类
C++中不存在接口的关键字,但是可以使用抽象类模拟接口
简单例子:设计一个接口A,A是计算的抽象方法。B、C是具体功能,B的功能是两数相乘,C的功能是两数相加。
class A
{
public:
virtual void count(int x,int y) = 0;
};
class B :public A
{
public:
virtual void count(int x, int y) override
{
cout << "x * y = " << x * y << endl;
}
};
class C :public A
{
public:
virtual void count(int x, int y) override
{
cout << "x + y = " << x + y << endl;
}
};
int main()
{
A* b = new B;
A* c = new C;
b->count(1, 2);
c->count(2, 3);
return 0;
}