仮想関数とポリモーフィズム
仮想関数
C++ における仮想関数 (Virtual Function) は、基底クラスで使用される特別な関数であり、基底クラスで仮想関数として宣言した後、派生クラスで再定義することもできます。仮想関数はポリモーフィック機能を実装し、基本クラスのポインターまたは参照、および動的バインディングを通じて派生クラス内の同じ名前の関数にアクセスできます。
仮想関数の定義形式は以下のとおりです。
class Base{
public:
virtual void func() {
// function body
}
};
上記のコードでは、func()
関数は仮想関数として宣言されています。派生クラスでは、この関数を再定義して多態性を実現できます。
基本クラスのポインターまたは参照を使用して仮想関数を呼び出す場合、プログラムは実行時に現在のポインターまたは参照が指すオブジェクトの型を決定し、関数の呼び出しアドレスを動的にバインドします。したがって、ポインターまたは参照が派生クラス オブジェクトを指す場合、派生クラスの関数が呼び出されます。
以下は、仮想関数の使用法を示す簡単な例です。
#include <iostream>
using namespace std;
class Shape {
public:
virtual float area() {
cout << "Parent class area :" << endl;
return 0;
}
};
class Rectangle : public Shape {
public:
float area() {
cout << "Rectangle class area :" << endl;
return (width * height);
}
private:
int width;
int height;
public:
Rectangle(int w, int h) {
width = w;
height = h;
}
};
class Triangle : public Shape {
public:
float area() {
cout << "Triangle class area :" << endl;
return (0.5 * base * height);
}
private:
int base;
int height;
public:
Triangle(int b, int h) {
base = b;
height = h;
}
};
int main() {
Shape* shape;
Rectangle rec(10, 7);
Triangle tri(10, 5);
shape = &rec;
shape->area();
shape = &tri;
shape->area();
}
上記のコードでは、Shape
は基本クラス、Rectangle
は派生クラスTriangle
です。Shape
のarea()
関数は仮想として宣言されているため、派生クラスでオーバーロードできます。main()
functionでは、 Shape
type のポインタ変数が宣言されておりshape
、これを介して派生クラス内の同じ名前の関数にアクセスします。shape
オブジェクトを指す場合はRectangle
関数 in がRectangle
呼び出され、オブジェクトを指すarea()
場合は関数 in が呼び出されます。shape
Triangle
Triangle
area()
クラスの定義において、virtualキーワードが前に付いているメンバ関数は仮想関数です。
class base {
virtual int get() ;
};
int base::get()
{
}
virtual 关键字只用在类定义里的函数声明中,
関数本体を記述するときは、次のようにする必要はありません。
多態性表現
オブジェクト指向プログラミングでは、ポリモーフィズム (Polymorphism) とは、同じ関数またはメソッドが異なるタイプのパラメーターを受け入れたり、異なるタイプの結果を返したりできることを意味します。ポリモーフィズムは、オブジェクト指向プログラミングの 3 つの主要な特性 (カプセル化、継承、ポリモーフィズム) の 1 つです。
C++ には、ポリモーフィズムの主な形式が 2 つあります。
- 関数のオーバーロード
C++ では、関数のオーバーロード (関数オーバーロード) もポリモーフィズムの一種です。複数のパラメーターの型またはパラメーターの数が異なる関数には同じ関数名を使用でき、コンパイラーは関数のパラメーターの型と数に基づいてどの関数を呼び出すかを決定します。例えば:
void add(int a, int b) {
cout << "调用的是int类型加法函数:" << a + b << endl;
}
void add(double a, double b) {
cout << "调用的是double类型加法函数:" << a + b << endl;
}
int main() {
add(1, 2); // 调用的是int类型加法函数:3
add(1.5, 2.6); // 调用的是double类型加法函数:4.1
return 0;
}
上記のコードでは、add()
関数はint
、double
型および のパラメータに対して 2 回オーバーロードされています。main()
functionでは、渡されたパラメータの型に応じて、コンパイラが対応する関数を自動的に選択して呼び出します。
2. 仮想関数
仮想関数の概念と使用法は以前に紹介しましたが、仮想関数は実行時ポリモーフィズムを実現します。基本クラスのポインターまたは参照、および動的バインディングを介して、派生クラス内の同じ名前の関数にアクセスします。
例えば:
#include <iostream>
using namespace std;
class Shape {
public:
virtual float area() {
cout << "Parent class area :" << endl;
return 0;
}
};
class Rectangle : public Shape {
public:
float area() {
cout << "Rectangle class area :" << endl;
return (width * height);
}
private:
int width;
int height;
public:
Rectangle(int w, int h) {
width = w;
height = h;
}
};
class Triangle : public Shape {
public:
float area() {
cout << "Triangle class area :" << endl;
return (0.5 * base * height);
}
private:
int base;
int height;
public:
Triangle(int b, int h) {
base = b;
height = h;
}
};
int main() {
Shape* shape;
Rectangle rec(10, 7);
Triangle tri(10, 5);
shape = &rec;
shape->area();
shape = &tri;
shape->area();
}
上記のコードでは、Shape
は基本クラス、Rectangle
は派生クラスTriangle
です。Shape
のarea()
関数は仮想として宣言されているため、派生クラスでオーバーロードできます。main()
functionでは、 Shape
type のポインタ変数が宣言されておりshape
、これを介して派生クラス内の同じ名前の関数にアクセスします。shape
オブジェクトを指す場合はRectangle
関数 in がRectangle
呼び出され、オブジェクトを指すarea()
場合は関数 in が呼び出されます。これは実行時ポリモーフィズムのパフォーマンスです。shape
Triangle
Triangle
area()
派生クラスのオブジェクトには基本クラス参照を割り当てることができます
基本クラス参照を介して、基本クラスと派生クラスの両方で同じ名前の仮想関数を呼び出す場合:
(1) 参照が基本クラスのオブジェクトを参照している場合、基本クラスの仮想関数が呼び出されます。
(2) 参照が派生クラスのオブジェクトを参照している場合、派生クラスの仮想関数が呼び出されます。この仕組みは「ポリモーフィズム」とも呼ばれます。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-p8d9yz08-1687264779367)(2023-06-20-20-11-19.png) )]
ポリモーフィズムの役割
オブジェクト指向プログラミングでポリモーフィズムを使用すると、プログラムのスケーラビリティが向上します。つまり、プログラムを変更したり機能を追加したりする必要がある場合に、変更や追加するコードが少なくなります。
ポリモーフィズムを利用したゲームプログラムの例
[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-ZqkStrLm-1687264779368)(2023-06-20-20-13-01. png)] [外部リンク
画像 転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-FdVlSDLM-1687264779369)(2023-06-20-20-13) -30.png)]
基本的な考え方:
各モンスター クラスの Attack、FightBack、Hurted メンバー関数を作成します。
攻撃機能は攻撃アクションを表現し、特定のモンスターを攻撃し、攻撃したモンスターを呼び出します。
Hurted関数は、攻撃されたモンスターのライフ値を減らすと同時に、攻撃されたモンスターのFightBackメンバー関数を呼び出して、攻撃されたモンスターの反撃を受けるために使用されます。
Hurted 機能は自身の体力を減らし、傷害アクションを実行します。
FightBack メンバー関数は反撃アクションを表し、反撃されたオブジェクトの Hurted メンバー関数を呼び出して、
反撃されたオブジェクトを負傷させます。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-lFtMMvOU-1687264779369)(2023-06-20-20-14-16.png) )]
非ポリモーフィックな実装方法
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-yQQ01rGL-1687264779369)(2023-06-20-20-14-39.png) )]
コード例
以下は、ポリモーフィズムを使用したゲーム プログラムの例です。このプログラムは、それぞれが独自の攻撃と防御を持つさまざまな種類のキャラクターを含む単純な RPG ゲームをシミュレートします。具体的な実装は以下の通りです。
#include <iostream>
#include <string>
using namespace std;
// 角色基类
class Character {
protected:
string name;
int attack;
int defense;
public:
Character(string name, int attack, int defense) {
this->name = name;
this->attack = attack;
this->defense = defense;
}
virtual int getAttack() { return attack; } // 获取攻击力
virtual int getDefense() { return defense; } // 获取防御力
virtual void attackTarget(Character* target) {} // 攻击目标
};
// 具体角色类:战士
class Warrior : public Character {
public:
Warrior(string name, int attack, int defense) : Character(name, attack, defense) {}
int getAttack() { return attack * 2; } // 攻击力加倍
void attackTarget(Character* target) {
int damage = getAttack() - target->getDefense();
damage = max(damage, 0);
cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
}
};
// 具体角色类:法师
class Mage : public Character {
public:
Mage(string name, int attack, int defense) : Character(name, attack, defense) {}
int getDefense() { return defense / 2; } // 防御力减半
void attackTarget(Character* target) {
int damage = getAttack() - target->getDefense();
damage = max(damage, 0);
cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
}
};
// 游戏主程序
int main() {
Warrior warrior("战士", 50, 30);
Mage mage("法师", 40, 40);
Character* player = &warrior;
Character* enemy = &mage;
cout << player->name << " 攻击 " << enemy->name << ":" << endl;
player->attackTarget(enemy);
cout << enemy->name << " 攻击 " << player->name << ":" << endl;
enemy->attackTarget(player);
return 0;
}
上記のコードでは、キャラクターの基本的な属性と動作はキャラクター基本クラスCharacter
で、getAttack()
と のgetDefense()
関数はそれぞれキャラクターの攻撃力と防御力を取得する仮想関数です。特定のロール クラスWarrior
および はMage
ロール基本クラスから継承されCharacter
、仮想関数getAttack()
および はgetDefense()
、攻撃ターゲットの特定の動作を定義します。
ゲームのメイン プログラムでは、最初に戦士と魔術師のキャラクターが作成され、次に基本クラス ポインターを通じてキャラクターの属性と動作にアクセスします。戦士が魔術師を攻撃するときは戦士クラスのattackTarget()
関数、魔術師が戦士を攻撃するときはattackTarget()
魔術師クラスの関数を呼び出します。これら 2 つの関数は両方とも仮想関数であり、再定義されているため、実際には派生クラス内の関数を呼び出し、実行時ポリモーフィズムを実現します。
!!!ポリモーフィック プログラムの例をさらに紹介します。!!
ジオメトリハンドラ
ジオメトリ ハンドラー: 複数のジオメトリのパラメータを入力し、
出力を領域ごとに並べ替える必要があります。エクスポート時に形状を指定します。
入力:
最初の行は幾何学的図形の数 n (100 を超えない) で、その下に n 行があり、各行は文字 c で始まります。
c が 'R' の場合、それは長方形を表し、この行の後に長方形の幅と高さである 2 つの整数が続きます。
c が「C」の場合、円を表し、その後にその半径を表す整数が続きます。
c が「T」の場合、三角形を表し、この線の後に 3 つの辺の長さを表す 3 つの整数が続きます。
出力:
各幾何形状の種類と面積を面積の小さい順に出力します。1 行に 1 つのジオメトリ。出力形式は次のとおりです。
形状名: エリア
教科書的なコード例
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{
public:
virtual double Area() = 0; //纯虚函数
virtual void PrintInfo() = 0;
};
class CRectangle:public CShape
{
public:
int w,h;
virtual double Area();
virtual void PrintInfo();
};
class CCircle:public CShape {
public:
int r;
virtual double Area();
virtual void PrintInfo();
};
class CTriangle:public CShape {
public:
int a,b,c;
virtual double Area();
virtual void PrintInfo();
};
double CRectangle::Area() {
return w * h;
}
void CRectangle::PrintInfo() {
cout << "Rectangle:" << Area() << endl;
}
double CCircle::Area() {
return 3.14 * r * r ;
}
void CCircle::PrintInfo() {
cout << "Circle:" << Area() << endl;
}
double CTriangle::Area() {
double p = ( a + b + c) / 2.0;
return sqrt(p * ( p - a)*(p- b)*(p - c));
}
void CTriangle::PrintInfo() {
cout << "Triangle:" << Area() << endl;
}
CShape * pShapes[100];
int MyCompare(const void * s1, const void * s2);
int main()
{
int i; int n;
CRectangle * pr; CCircle * pc; CTriangle * pt;
cin >> n;
for( i = 0;i < n;i ++ ) {
char c;
cin >> c;
switch(c) {
case 'R':
pr = new CRectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr;
break;
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo();
return 0;
}
int MyCompare(const void * s1, const void * s2)
{
double a1,a2;
CShape * * p1 ; // s1,s2 是 void * ,不可写 “* s1”来取得s1指向的内容
CShape * * p2;
p1 = ( CShape * * ) s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape *
p2 = ( CShape * * ) s2; // 故 p1,p2都是指向指针的指针,类型为 CShape **
a1 = (*p1)->Area(); // * p1 的类型是 Cshape * ,是基类指针,故此句为多态
a2 = (*p2)->Area();
if( a1 < a2 )
return -1;
else if ( a2 < a1 )
return 1;
else
return 0;
}
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo();
return 0;
}
五角形などの新しい幾何学的形状を追加する場合は、CShape から CPentagon を派生し、メインの switch ステートメントに case を追加するだけで、残りは変更されません。
基本クラスのポインター配列を使用してさまざまな派生クラス オブジェクトへのポインターを格納し、その配列をトラバースして各派生クラス オブジェクトに対してさまざまな操作を実行するのが非常に一般的です。
下面是一个利用 C++ 实现的几何形体处理程序,可以输入若干个几何形体的参数,并按照面积排序输出各个几何形体的种类及面积:
```c++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const double pi = 3.14159265358979323846;
// 几何形体基类
class Shape {
public:
virtual double getArea() = 0; // 获取面积
virtual string getName() = 0; // 获取名称
};
// 具体几何形体类:矩形
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) {
width = w;
height = h;
}
double getArea() {
return width * height; }
string getName() {
return "矩形"; }
};
// 具体几何形体类:圆形
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) {
radius = r; }
double getArea() {
return pi * radius * radius; }
string getName() {
return "圆形"; }
};
// 具体几何形体类:三角形
class Triangle : public Shape {
private:
double a, b, c;
public:
Triangle(double aa, double bb, double cc) {
a = aa;
b = bb;
c = cc;
}
double getArea() {
double p = (a + b + c) / 2;
return sqrt(p * (p - a) * (p - b) * (p - c));
}
string getName() {
return "三角形"; }
};
// 比较函数,用于排序
bool cmp(Shape* s1, Shape* s2) {
return s1->getArea() < s2->getArea(); }
// 主程序
int main() {
int n;
cin >> n;
vector<Shape*> shapes;
for (int i = 0; i < n; i++) {
char c;
cin >> c;
if (c == 'R') {
double w, h;
cin >> w >> h;
shapes.push_back(new Rectangle(w, h));
}
else if (c == 'C') {
double r;
cin >> r;
shapes.push_back(new Circle(r));
}
else if (c == 'T') {
double a, b, c;
cin >> a >> b >> c;
shapes.push_back(new Triangle(a, b, c));
}
}
sort(shapes.begin(), shapes.end(), cmp);
for (int i = 0; i < n; i++) {
cout << shapes[i]->getName() << ":" << shapes[i]->getArea() << endl;
delete shapes[i];
}
return 0;
}
```
在上述代码中,几何形体基类 `Shape` 定义了接口函数 `getArea()` 和 `getName()`,分别用于获取几何形体的面积和名称。具体的几何形体类 `Rectangle`、`Circle` 和 `Triangle` 继承自 `Shape`,并实现了这两个接口函数。
在主程序中,首先输入几何形体的数目 `n`,然后根据每个几何形体的名称和参数创建相应的对象,并添加到 `shapes` 向量中。最后利用 `sort()` 函数和比较函数 `cmp()` 对 `shapes` 向量进行排序,按照面积从小到大排序。最后遍历 `shapes` 向量,输出每个几何形体的名称和面积,并释放相应的内存。
コンストラクターとデストラクターでの仮想関数の呼び出し
コンストラクターおよびデストラクターでの仮想関数の呼び出しはポリモーフィズムではありません。呼び出される関数が独自のクラスまたは基本クラスで定義された関数であることはコンパイル時に判断でき、独自のクラス関数を呼び出すか派生クラス関数を呼び出すかを決定するのは実行時まで待たれません。
コンストラクターとデストラクターで仮想関数を呼び出すことは危険な行為です。仮想関数の呼び出しは実行時に決定され、コンストラクターとデストラクターではオブジェクトの状態が完全に初期化されていないか、破棄されている可能性があり、その時点で仮想関数を呼び出します。関数は未定義の動作を引き起こす可能性があります。
具体的には、コンストラクターで仮想関数を呼び出すと、次の問題が発生する可能性があります。
- オブジェクトが完全に初期化されていない可能性があり、この時点で仮想関数を呼び出すと、初期化されていないメンバー変数または無効なポインターにアクセスし、プログラムがクラッシュしたり、未定義の動作が発生したりする可能性があります。
- オブジェクトの動的タイプがまだ決定されていない可能性があります。この時点で、仮想関数を呼び出すと、派生クラスの実装ではなく、デフォルトで基本クラスの実装が使用され、プログラムの誤った動作が発生します。
デストラクターで仮想関数を呼び出すと、次の問題が発生する可能性があります。 - オブジェクトの動的タイプが破壊されている可能性があり、この時点で仮想関数を呼び出すと、プログラムがクラッシュするか、未定義の動作が発生する可能性があります。
- 仮想関数を呼び出すと、仮想関数テーブルの検索と呼び出しがトリガーされる可能性があり、オブジェクトの破棄のプロセス中に仮想関数テーブルと仮想関数ポインタが破棄され、プログラムの未定義の動作が発生する可能性があります。
したがって、コンストラクターやデストラクターで仮想関数を呼び出さないようにするか、コンストラクターで初期化リストを使用してメンバー変数を初期化するか、デストラクターで単純なリソース解放操作のみを実行するなど、他の解決策を採用する必要があります。本当にコンストラクターまたはデストラクターで仮想関数を呼び出す必要がある場合は、オブジェクトの状態が完全に初期化されているか、破棄されていないことを確認し、仮想関数の実装が可能かどうかに注意を払う必要があります。この状況に正しく対処してください。
ポリモーフィック実装の鍵 - 仮想関数テーブル
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-M8dnzr3H-1687264779370)(2023-06-20-20-22-42.png) )] ポリモーフィズムは、
オブジェクト プログラミングの重要な機能の 1 つであり、その鍵となるのは、ランタイム バインディングを通じて関数呼び出しの動的なディスパッチを実現することです。C++ では、ポリモーフィズムを実現する一般的な方法は仮想関数を使用することであり、重要なのは、仮想関数テーブル (vtable) を使用してクラスの仮想関数ポインターを格納し、動的バインディングを実現することです。
仮想関数テーブルは、クラスの仮想関数ポインタを格納するためにコンパイラによって自動的に生成される静的変数です。仮想関数を含むクラスごとに、コンパイラはコンパイル時にその仮想関数テーブルを生成し、それをクラスのメタデータとしてプログラムのデータ セグメントに格納します。仮想関数テーブルの各エントリは、宣言されたのと同じ順序での仮想関数へのポインタです。したがって、クラスのオブジェクトが作成されると、仮想関数テーブルのアドレスがオブジェクトの先頭に格納され、オブジェクトの仮想関数ポインタ (vptr) になります。
基本クラス型のポインターまたは参照を使用して仮想関数を呼び出す場合、オブジェクトの実際の型に従って仮想関数テーブルが検索され、対応する仮想関数が仮想関数ポインターを通じて呼び出されます。具体的には、コンパイラは、オブジェクトの仮想関数ポインタ (vptr) が指す仮想関数テーブル内の対応する仮想関数を検索し、関数ポインタを介して対応する関数を呼び出します。したがって、仮想関数の呼び出しは実行時に動的に決定されるため、ポリモーフィズムの目的は達成されます。
仮想関数を使用する場合は、次の条件を満たす必要があることに注意してください。
- 仮想関数はクラスのメンバー関数である必要があります。
- 仮想関数は、基本クラスで仮想関数として宣言し、派生クラスでオーバーライドまたは実装する必要があります。
- 仮想関数のパラメータと戻り値の型は、基本クラスの仮想関数とまったく同じである必要があります。
- 仮想関数テーブルのサイズと順序は、クラスの継承関係に対応する必要があります。
- 動的バインディングをトリガーするには、基本クラス型のポインターまたは参照を通じて仮想関数呼び出しを行う必要があります。
仮想関数テーブルの実現により、C++ はポリモーフィズムの機能を実現し、プログラムの柔軟性と拡張性を高めます。ただし、仮想関数テーブルの実装により、プログラムのオーバーヘッドと複雑さが増加するため、プログラムの設計および実装時に慎重に検討する必要があることに注意してください。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-9zSU9uGz-1687264779370)(2023-06-20-20-24-16.png) )]
仮想デストラクタ
基本クラスのポインターを介して派生クラス オブジェクトを削除する場合、通常は基本クラスのデストラクターのみが呼び出されます。
ただし、派生クラスのオブジェクトを削除する場合は、最初に派生クラスのデストラクターを呼び出し、次に基本クラスのデストラクターを呼び出す必要があります。
解決策: 基本クラスのデストラクターを仮想として宣言します。派生クラスのデストラクターは仮想として宣言できます。
基本クラスのポインターを介して派生クラス オブジェクトを削除する場合、最初に派生クラスのデストラクターが呼び出され、次に基本クラスのデストラクターが呼び出されます。
一般に、クラスが仮想関数を定義する場合、デストラクターも仮想関数として定義する必要があります。あるいは、基本クラスとして使用することを目的としたクラスでも、デストラクターを仮想関数として定義する必要があります。
注: 仮想関数はコンストラクターとして使用できません。
仮想デストラクターとは、デストラクターの前にキーワード「virtual」を追加して多態性デストラクターを実装することを指します。
基本クラスのポインターまたは参照を使用して派生クラスのオブジェクトを削除する場合、基本クラスのデストラクターが仮想関数でない場合は、派生クラスのデストラクターではなく、基本クラスのデストラクターのみが呼び出されます。これにより、派生クラスに割り当てられたメモリ リソースが正しく解放されなくなり、メモリ リークが発生します。
基底クラスのデストラクタを仮想関数として宣言することで、オブジェクトの破棄時に動的バインディングを実現し、派生クラスのデストラクタを呼び出すことができます。このようにして、派生クラスに割り当てられたメモリ リソースを正しく解放することができ、メモリ リークの問題を回避できます。
例えば:
class Base {
public:
virtual ~Base() { // 声明为虚函数
std::cout << "Base::~Base()" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override { // 实现派生类的析构函数
std::cout << "Derived::~Derived()" << std::endl;
}
};
int main() {
Base *p = new Derived();
delete p; // 动态绑定,调用派生类的析构函数
return 0;
}
上記のコードでは、基本クラスのデストラクターは仮想関数として宣言され、派生クラスのデストラクターは実装されて基本クラスの仮想デストラクターをオーバーライドします。派生クラスのオブジェクトを削除する場合は、基本クラスのポインタを介してオブジェクトを削除し、動的バインディング機構を使用して派生クラスのデストラクターの呼び出しを実現します。
仮想デストラクタは、基底クラスのポインタが派生クラスのオブジェクトを削除する際に発生する可能性のあるメモリリーク問題を解決するものであり、ポリモーフィズムを実現する鍵となります。オブジェクト継承システムを設計および実装する場合、デストラクタは可能な限り仮想関数として宣言する必要があります。
派生クラスがデストラクターを明示的に提供しない場合、コンパイラーはデフォルトのデストラクターを自動的に生成することに注意してください。このデフォルトのデストラクターも、基本クラスの仮想デストラクターを継承するため、仮想として宣言されます。
さらに、仮想デストラクターは、基本クラスのポインターまたは参照が派生クラスのオブジェクトを削除する場合にのみ適用できることに注意してください。オブジェクトの直接名を削除に使用した場合、オブジェクト自体のデストラクターのみが呼び出され、動的バインディングはトリガーされず、派生クラスのデストラクターも呼び出されません。
例えば:
class Base {
public:
virtual ~Base() { // 声明为虚函数
std::cout << "Base::~Base()" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override { // 实现派生类的析构函数
std::cout << "Derived::~Derived()" << std::endl;
}
};
int main() {
Derived d;
Base *p = &d;
p->~Base(); // 不会触发动态绑定,只会调用 Base 的析构函数
return 0;
}
上記のコードでは、オブジェクト d のデストラクターは派生クラス Derived のデストラクターであり、オブジェクト d が削除されると自動的に呼び出されます。ただし、基底クラス ポインターを使用してオブジェクトのデストラクターを直接呼び出すと、動的バインディングはトリガーされず、基底クラスのデストラクターのみが呼び出され、派生クラスのデストラクターが正しく呼び出されなくなります。
したがって、仮想デストラクターを使用する場合は、派生クラスのデストラクターが正しく呼び出され、メモリ リークを回避できるように、基本クラスのポインターまたは参照を使用して派生クラス オブジェクトを削除することに注意する必要があります。
さらに、派生クラスのデストラクターが、動的に割り当てられたメモリの解放やファイルのクローズなどの特別なリソース解放操作を実行する必要がある場合、基本クラスのデストラクターをデストラクター内で明示的に呼び出す必要があることに注意してください。派生クラスの仮想デストラクター。この方法によってのみ、派生クラスのオブジェクトが破棄されるときに、派生クラスのデストラクターが最初に呼び出され、次に基本クラスのデストラクターが呼び出されることが保証され、リソース リークの問題が回避されます。
例えば:
class Base {
public:
virtual ~Base() {
std::cout << "Base::~Base()" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
data = new int[10];
}
~Derived() override {
delete[] data; // 释放动态分配的内存
std::cout << "Derived::~Derived()" << std::endl;
// 显式调用基类的虚析构函数
// ensure the base class's destructor is executed
Base::~Base();
}
private:
int *data;
};
int main() {
Base *p = new Derived();
delete p;
return 0;
}
上記のコードでは、派生クラス Derived がコンストラクターで int 配列のメモリ空間を割り当て、デストラクターでメモリ空間を解放します。同時に、基本クラスの仮想デストラクターが派生クラスのデストラクター内で明示的に呼び出され、オブジェクトが破棄されるときに、派生クラスのデストラクターが最初に呼び出され、次に基本クラスのデストラクターが呼び出されます。これにより、リソース リークの問題が回避されます。
つまり、仮想デストラクターはポリモーフィズムとメモリ リークの回避の鍵です。基底クラスのポインターまたは参照を使用して派生クラス オブジェクトを削除すること、および基底クラスの仮想デストラクターをデストラクターのデストラクターで明示的に呼び出すことに注意する必要があります。派生クラス。リソースが適切に解放されることを保証する関数。
純粋仮想関数と抽象クラス
純粋仮想関数を含むクラスは抽象クラスと呼ばれます
抽象クラスは、新しいクラスを派生するための基本クラスとしてのみ使用でき、独立した抽象クラス オブジェクトを作成することはできません。
抽象クラスのポインタおよび参照は、抽象クラスから派生したクラスのオブジェクトを指すことができます。
A a ; // 間違い、A は抽象クラス、オブジェクトは作成できません
A * pa ; // わかりました、抽象クラスのポインタと参照は定義できます
pa = new A ; // エラー、A は抽象クラス、オブジェクトです作成できません
純粋仮想関数は、抽象クラスのメンバ関数内で呼び出すことができますが、コンストラクタやデストラクタ内では呼び出すことができません。
クラスが抽象クラスから派生した場合、そのクラスが基本クラス内のすべての純粋仮想関数を実装している場合に限り、そのクラスは非抽象クラスになることができます。
純粋仮想関数と抽象クラスは、インターフェイスとポリモーフィズムを実装するための C++ の重要な機能です。
純粋仮想関数とは、関数宣言の最後に「= 0」で指定した関数であり、実装されていない仮想関数です。純粋仮想関数の役割は、インターフェイスを実装すること、つまり、特定の実装を提供せずにインターフェイスを定義することです。純粋仮想関数には特定の実装がないため、このクラスのオブジェクトを直接作成することはできず、関数を使用する前に派生クラスに実装する必要があります。例えば:
class Shape {
public:
virtual double area() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
double area() override { // 实现纯虚函数
return 3.14 * radius * radius;
}
private:
double radius;
};
抽象クラスとは、直接インスタンス化することはできませんが、他のクラスを派生するための基本クラスとしてのみ使用できる純粋な仮想関数を含むクラスを指します。抽象クラスの役割は、特定の実装を提供せずに一連のインターフェイスを定義することです。抽象クラスには純粋仮想関数が含まれるため、すべての純粋仮想関数は使用する前に派生クラスに実装する必要があります。例えば:
class Shape {
public:
virtual double area() = 0; // 纯虚函数,使得 Shape 变成了抽象类
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
double area() override {
return 3.14 * radius * radius;
}
void draw() override {
// 绘制圆形
}
private:
double radius;
};
抽象クラスには非純粋仮想関数を含めることができますが、純粋仮想関数を含むクラスは抽象クラスでなければならないことに注意してください。純粋仮想関数を含むクラスは直接インスタンス化できないため、他のクラスを派生するための基本クラスとして使用し、インターフェイスの定義を提供する必要があります。
純粋仮想関数と抽象クラスの役割は、インターフェイスとポリモーフィズムを実装して、プログラムをより柔軟で拡張可能にすることです。実際のプログラム設計では、抽象クラスまたは純粋仮想関数を使用してインターフェイスを定義できるため、プログラム設計がよりモジュール化され、保守しやすくなります。