Предисловие:
Полиморфизм буквально означает несколько форм.
Метод с одним и тем же именем может иметь разные проявления или формы в разных экземплярах, что является полиморфизмом. В C++ мы можем добиться полиморфизма, переписав методы в родительском классе, и переписывание виртуальных функций также играет важную роль в полиморфизме C++.
полиморфизм
1. Перезаписать
В C++ подклассы могут наследовать код родительского класса, и на этом основании некоторые методы родительского класса могут быть перезаписаны для выполнения функций, отличных от родительского класса.
Переопределенный метод должен иметь то же имя функции, что и переопределенный метод, а также должны быть одинаковыми типы параметров и типы возвращаемых значений.
Например:
#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 load, а не zhong load, английское название — перегрузка)
Относится к нескольким функциям с одним и тем же именем, объявленным в одной и той же доступной области с разными списками параметров (разные типы, числа и порядок параметров), вызываемая функция определяется в соответствии со списком параметров, и при перегрузке не учитывается возвращаемый результат. тип функции.
Мы можем позволить методам-членам с одним и тем же именем иметь множество различных списков параметров посредством перегрузки.При их использовании мы можем вызывать разные методы в соответствии с разными параметрами для выполнения одной и той же функции.
Конструктор класса также может быть перегружен, и могут использоваться разные методы построения в соответствии с разными типами параметров, но деструктор не может, потому что деструктор не может иметь никаких формальных параметров.
Пример использования перегрузки:
#include<bits/stdc++.h>
using namespace std;
class A
{
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
};
Перегруженные методы находятся в параллельной или горизонтальной связи, в то время как перезапись заключается в переопределении метода в родительском классе, и между переопределенным методом и переопределенным методом существует вертикальная связь.
2. Виртуальные функции и абстрактные классы
виртуальная функция
В C++ виртуальная функция — это функция с префиксом ключевого слова virtual. После переопределения подкласса виртуальная функция может получить доступ к переопределенному методу в своем подклассе через указатель типа своего суперкласса.
Предположим, мы хотим разработать игру,
Есть две роли, маг и фехтовальщик,
И у обоих есть два защищенных атрибута: плавающая кровь и плавающая мана.
Маг будет потреблять 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(), но после того, как мы добавим ключевое слово virtual в метод getHurt() родительского класса, указатель родительского класса 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 — это синтаксический сахар, добавленный в стандарт 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, как показано ниже:
class Base
{
public:
virtual void Show(int x); // 纯虚函数
};
class Derived : public Base
{
public:
virtual void Show(int x) override=0;// 在=0之前
};
конечное ключевое слово
Ключевое слово 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
Статические переменные-члены являются общими для всех объектов этого класса и не принадлежат конкретному объекту.Даже если создается несколько объектов, выделяется только одна копия памяти.После создания первого экземпляра этого класса статические переменные-члены Хранится в пространстве памяти Место в фиксировано или статически неизменно (то есть статическое хранилище), и все экземпляры этого класса будут использовать данные в этой памяти.
Таким образом, в приведенном выше коде после изменения статической переменной-члена 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