前言:
多态按字面的意思就是多种形态。
同一个名称的方法,在不同的实例上可以有不同的不同表现形式或形态,这便是多态。在C++中,我们可以通过重写父类中的方法实现多态,虚函数的重写在C++的多态中也扮演着重要角色。
多态
一、重写(overwirte)
在C++中,子类可以继承父类的代码,在此基础上重写(overwirte)某些父类的方法来实现与父类有所不同的功能。
重写的方法需要和被重写的方法函数名相同,参数类型和返回值类型也要相同。
例如:
#include<iostream>
using namespace std;
class A
{
public:
void print(int x)
{
cout << x<< endl;
}
};
class B:public A
{
public:
void print(int x)
{
cout << 2*x << endl;
}
};
int main()
{
A a = A();
B b = B();
a.print(1);
b.print(1);
}
输出结果为:
1
2
除了成员方法(又称成员函数)可以重写外,成员变量也可以被重写。
但我们要知道,子类的成员变量重写后其实会新建一个名称相同变量,在子类的方法和外部的代码中只能访问到这个新建的变量,但父类中原有的被重写的变量会依然存在,仍然可以通过其父类的方法访问和修改它,但子类中的方法和外部的代码只能访问到新建的那个变量。
举个例:
#include <iostream>
using namespace std;
class A
{
public:
int m=1;
A()
{
printf("A.m=%d\n", this->m);
}
void Aprint()
{
printf("%d\n", this->m);
}
void AsetM(int m)
{
this->m = m;//this指针可以让你访问类的成员变量m,而不是形式参数m
}
};
class B :public A
{
public:
int m=2;
B()
{
m = 2;
printf("B.m=%d\n", this->m);
}
void Bprint()
{
printf("%d\n", this->m);
}
};
int main()
{
B b;
b.Aprint();
b.Bprint();
printf("%d\n\n", b.m);
b.m = 3;
b.Aprint();
b.Bprint();
printf("%d\n\n", b.m);
b.AsetM(4);
b.Aprint();
b.Bprint();
printf("%d\n\n", b.m);
return 0;
}
输出结果为:
A.m=1
B.m=2
1
2
2
1
3
3
4
3
3
同时我们还发现,C++构造子类的实例时,会先调用其父类的构造方法,再调用其自己的构造方法
补充内容:重写和重载的区别:
重载(注意:读chong 载,不是zhong 载,英文名overload)
是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
我们可以通过重载,让同一个名称的成员方法拥有多种不同参数列表,在使用时,可以根据参数的不同调用不同方法,来完成同样的功能。
类的构造函数也可以被重载,使用时可以根据参数类型的不同采用不同的构造方法,但析构函数不能,因为析构函数不能带有任何形式参数。
重载的使用例子:
#include<bits/stdc++.h>
using namespace std;
class A
{
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
};
重载的方法之间是并列或者说水平的关系,而重写是在覆盖父类中的方法,重写和被重写的方法之间是垂直的关系。
二、虚函数和抽象类
虚函数
在C++中,虚函数是指拥有virtual关键字前缀的函数。虚函数在子类被重写后可以通过其父类类型的指针访问到其子类中重写的方法。
假设,我们要开发一个游戏,
里面有法师和剑士两种角色,
并且都具有血量(float blood)、法力值(float mana)两种受保护的属性。
法师攻击时会消耗10点法力值,剑士攻击时不消耗法力值;
法师受到伤害时,会受到完全的伤害,剑士受伤时,因为剑士自带铠甲可以防御掉20%的伤害。
法师的攻击力为100,剑士的攻击为80,两者的血量和法力值都为100。
于是我们的代码可以这样写:
#include<iostream>
using namespace std;
class Character
{
protected:
float blood = 100;
float mana = 100;
public:
virtual void attack(Character* c)
{
c->getHurt(100);
}
virtual void getHurt(float hurt_blood)//注意一定要使用虚函数,否则其父类指针只能访问父类中的getHurt方法,不能访问你重写的方法
{
blood -= hurt_blood;
cout << "Character getHurt" << endl;
}
void print()
{
cout << blood << " " << mana << endl;
}
};
class Mage :public Character
{
public:
void attack(Character* c)//因为法师攻击时会消耗法力值,与父类的攻击方式不同,所以需要重写attack方法
{
c->getHurt(100);//对角色c造成伤害
mana -= 10;
}
};
class Swordsman :public Character
{
public:
void getHurt(float hurt_blood)//因为剑士受攻击时只受到80%的伤害,和父类的受伤方式不同,所以需要重写getHurt方法
{
cout << "Swordsmam getHurt" << endl;
blood -= hurt_blood * 0.80;
}
};
int main()
{
Mage* m = new Mage();//C++中可以通过new关键字在程序运行的过程中动态创建新对象
Swordsman* s = new Swordsman();
m->attack(s);
s->print();
}
输出结果:
Swordsmam getHurt
20 100
如果我们去掉上面代码中的virtual关键字,输出结果就会变成:
Character getHurt
0 100
可见,在这里,父类指针Character* c只访问到了父类中的getHurt()方法去接受伤害,所以剑士被扣了100血,并没有按照我们重写的getHurt()去接受伤害,但我们给父类的getHurt()方法添加virtual关键字后,父类指针Character* c才能访问到子类重写出来的getHurt()方法,才能实现剑士可以减少20%的伤害的效果。
纯虚函数
如果在虚函数的末尾增加=0,那么它就会成为纯虚函数
virtual void getHurt(float hurt_blood)=0;
纯虚函数具有以下特点:
(1)纯虚函数没有函数体;
(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
(3)这是一个声明语句,最后有分号。
(4)拥有至少一个纯虚函数的类叫做抽象类,抽象类自身不能被直接实例化,但它的子类在重写父类中 的纯虚函数方法后就能该子类就能实例化。
抽象类
在软件开发中,我们发现有些对象具有一些相同的方法,但这种方法无法脱离该对象而单独存在,比如圆和正方形,他们都属于几何图形,我们要计算他们的面积,那么编程时中就得为他们提供一个计算面积的方法作为获取其面积的接口,外部的代码就可以提供这个接口获取这个图形的面积。而这个接口本身可以算作一类事物,但它肯定是不能脱离图形类的实例而单独存在的(或者说能被实例化的)。
所以C++中为我们提供了抽象类(又称接口类)的功能,让我们能专门写这样的接口类。
不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。
纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
那么在C++中我们就可以这样写:
#include<iostream>
using namespace std;
class Shape
{
};
class AreaInterface//面积接口类
{
public:
virtual double getArea() = 0;//纯虚函数
};
class Square :public Shape, public AreaInterface//正方形类继承形状类和面积接口类
{
public:
double a;// 表示正方形的边长
double getArea()//子类必须实现父类中的纯虚函数方法才能被实例化,这就能强制程序员必须为这个类提供这个接口
{
return a * a;
}
};
class Circle :public Shape, public AreaInterface
{
public:
double r;//表示圆的半径
double getArea()
{
return 3.1415926 * r * r;
}
};
int main()
{
Square s = Square();
s.a = 10;
Circle c = Circle();
c.r = 5;
AreaInterface* a = &s;
cout << s.getArea() << endl;
cout << c.getArea() << endl;
cout << a->getArea() << endl;
// ↑面积接口指针a也可以访问到它指向的a的getArea方法,因为正方形是面积接口的子类
}
输出结果:
100
78.5398
100
override关键字
override关键字是C++11标准中新增的帮助你重写父类中的虚函数的语法糖。
在没有override关键字时,如果你重写的方法与父类方法的参数、名称、属性不一样,编译器并不会报错,而是会把它当成新的函数,程序就会违背你的意愿,如下:
class Base
{
public:
virtual void Show(int x); // 虚函数
};
class Derived : public Base
{
public:
virtual void Show(float x);
// 你重写的方法形式参数为float x和父类方法参数不一样,编译器会把它当成新的函数,不会覆盖父类中的Show函数
};
如果你要保证你正确重写了父类中的方法,你可以在你重写的方法后加上override关键字,编译器就会检查你写的重写的方法的形式参数表、类型、属性是否和父类方法相同,如果不同就会报错提醒你去改正
class Base
{
public:
virtual void Show(int x); // 虚函数
};
class Derived : public Base
{
public:
virtual void Show(float x) override;
// 你重写的方法和父类方法形式参数、属性或返回值类型不一样(父类是int x,子类是float x),但是你加了override关键字此时编译器就会报错提示你去改正它,保证你重写的方法与父类的形式参数表、类型、属性相同。
};
如果你要重写出纯虚函数,则=0要添加到override关键字后,如下:
class Base
{
public:
virtual void Show(int x); // 纯虚函数
};
class Derived : public Base
{
public:
virtual void Show(int x) override=0;// 在=0之前
};
final关键字
final关键字也是C++11中新增的语法糖帮助你防止父类方法被重写,也可以防止父类被继承。
如果你在写父类的方法时,你希望子类不能重写修改这个方法,那么你可以为其添加final关键字,那么在子类重写这个方法时,编译器就会报错,如下:
class Base {
public:
virtual void Show(int x) final; // 拥有final关键词
};
class Derived : public Base {
public:
virtual void Show(int x) override;
// 因为被重写的方法拥有final关键字,此时编译器就会报错提示你不能重写
};
如果你把final关键字写在父类类名后面,那么这个父类就不能被继承,如下:
class A final
{
};
class B :A//编译器会在这里报错,提示你有final关键字的父类不能被继承
{
}
静态
静态成员变量
在C++中,对象的内存包含了其成员变量,不同的对象,占用的内存区域是不同的,这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。例如有两个相同类型的对象 a、b,它们都有一个成员变量 name_,那么修改 a.name_ 的值不会影响 b.name_ 的值。
但是我们在实际编程的时候,有的时候希望某一类的对象拥有一些共享的数据,而不是每个对象的实例都存一份数据。
所以在C++,提供了静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰,例如:
#include <stdio.h>
class A
{
public:
static int b; //静态变量只能在类中声明,但不能定义初始值,只能在类外定义初始值
};
int A::b = 3; //定义了静态成员变量,同时初始化。也可以写"int A::a;",即不给初值,同样可以通过编译
int main()
{
A a[2] = { A(),A() };
printf("%d\n", a[1].b);
a[1].b = 5;
printf("%d\n", a[2].b);
return 0;
}
输出结果:
3
5
static 成员变量属于该类所有对象共有的,不属于某个具体的对象,即使创建多个对象,也只分配一份内存,静态成员变量在这个类的第一个实例创建后,其在内存空间中的位置是固定或者说静态不变的(也就是静态存储),该类的所有实例使用的都会是这份内存中的数据。
所以上面的代码,我们修改静态成员变量a[1].b=5后,a[2].b也会等于5,因为它们都使用的同一段内存区域。
静态方法(又称静态成员函数)
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。
静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
例如:
#include<iostream>
using namespace std;
class Math
{
public:
static double plus(double a, double b)
{
return a + b;
}
};
int main()
{
cout << Math::plus(2, 3) << endl;//不用实例化就能访问plus函数
}
输出结果:
5