C でカプセル化、継承、ポリモーフィズムを実装する方法を理解する#
1. イントロダクション#
カプセル化、継承、ポリモーフィズムはオブジェクト指向の重要な特性であり、これらをデザインパターンと組み合わせることで、コードを高い内部結合度と低い耦合度にすることができ、コードの安全性、可読性、保守性を向上させることができます。では、手続き型のプログラミング言語である C 言語でもカプセル化、継承、ポリモーフィズムを実現し、コードの品質を向上させることはできるのでしょうか?答えは明らかです。Linux カーネルのコード学習プロセスで注意深く分析すると、C 言語を使用してカプセル化、継承、ポリモーフィズムを広範に実現していることがわかります。この記事では、C 言語を使用してこれらのオブジェクト指向の特性を実装する方法について解説します。これにより、コードを読むための準備をし、高品質のコードを書くことができるようになります。
2. カプセル化#
カプセル化は、抽象的な事物の属性とそれに対応する操作メソッドをクラスにまとめて、内部のメソッドを使用して内部の状態を変更することです。カプセル化の本質は、情報を隠蔽するプロセスであり、オブジェクトの内部状態が外部から直接アクセスや変更されないようにします。
カプセル化の利点は次のとおりです。
- コードの安全性が向上し、データは規定された方法でのみアクセスできるため、誤操作や不正なアクセスを防ぐことができます。
- コードの再利用性が向上し、同じまたは類似のデータ型を複数回使用することができます。
- コードの保守性が向上し、データ型が変更された場合には、1 か所だけ変更すれば済みます。
C 言語にはクラスの概念はありませんが、構造体を使用して物事をカプセル化することができます。カプセル化の重要な意味は、関数(メソッド)とデータ(プロパティ)を一緒にまとめることであり、データ(プロパティ)とデータ(プロパティ)を一緒にまとめることです。これにより、単純な構造体ポインタを使用してすべてのデータにアクセスし、すべての関数を操作することができます。
以下はカプセル化の具体的な例です。
#include <stdio.h>
typedef struct human
{
int age;
char sex;
void (*set_age)(struct human *p, int age);
int (*get_age)(struct human *p);
void (*set_sex)(struct human *p, char sex);
char (*get_sex)(struct human *p);
} Human;
void set_age(Human *p, int age)
{
p->age = age;
}
int get_age(Human *p)
{
return p->age;
}
void set_sex(Human *p, char sex)
{
p->sex = sex;
}
char get_sex(Human *p)
{
return p->sex;
}
int main()
{
Human p;
p.set_age = set_age;
p.set_age(&p, 18);
p.set_sex = set_sex;
p.set_sex(&p, 'M');
p.get_age = get_age;
printf("age: %d\n", p.get_age(&p));
p.get_sex = get_sex;
printf("sex: %c\n", p.get_sex(&p));
return 0;
}
上記のコードでは、"human" という構造体を定義しています。"age" と "sex" のメンバーと、"set_age"、"get_age"、"set_sex"、"get_sex" の関数を実装しています。これがカプセル化です。構造体のデータと関数によって、human のプロパティとメソッドの操作が実現され、また、構造体内のデータの変更は構造体内の操作関数を介してのみ行われます。
3. 継承#
継承は、既存のクラス(親クラスまたは基底クラス)を基にして新しいクラス(子クラスまたは派生クラス)を作成することです。子クラスまたは派生クラスは親クラスのデータと関数にアクセスすることができ、コードの重複を避けることができます。子クラスは独自のプロパティとデータを追加することもできます。
継承の利点は次のとおりです。
- コードの再利用性が向上し、同じコードを繰り返し書く必要がありません。
- コードの拡張性が向上し、既存のクラスを基に微調整することができます。
- コードの可読性が向上し、継承によりコードがより簡潔になります。
C 言語では、構造体のネストを使用してクラスの継承を実現することができます(ここでは単一の継承についてのみ考慮しています)。ただし、親クラスの構造体参照は子クラスの構造体メンバーの最初の位置に配置する必要があります。これにより、データのアクセスやキャストに問題がないようにします。
以下は継承の具体的な例です。
#include <stdio.h>
#include <stdlib.h>
typedef struct human {
int age;
char sex;
} Human;
typedef struct person{
Human human;
char *name;
} Person;
Person* create_person(int age, char sex, char *name) {
Person* cperson = (Person*) malloc(sizeof(Person));
cperson->human.age = age;
cperson->human.sex = sex;
cperson->name = name;
return cperson;
}
int main() {
Person* cperson = create_person(18, 'w', "lucy");
printf("(%d, %c) - name: %s\n", cperson->human.age, cperson->human.sex, cperson->name);
free(cperson);
return 0;
}
上記のコードでは、"Human" と "Person" の 2 つの構造体を定義しています。"Person" は "Human" 構造体と "name" メンバーを含んでいます。"create_person ()" 関数を使用して "Person" 型の構造体を構築し、その中の "Human" メンバーと "name" メンバーに値を設定しています。継承の特性を他の場所で使用する必要がある場合は、同様のネスト構造体の方法を使用して実装することができます。
4. ポリモーフィズム#
ポリモーフィズムは、オブジェクト指向プログラミングの中心的な概念であり、異なるオブジェクト上で同じ操作を実行することができるようにします。
ポリモーフィズムの利点は次のとおりです。
- コードの拡張性が向上し、異なるタイプのオブジェクトをサポートすることでプログラムがより柔軟になります。
- コードの保守性が向上し、タイプが変更された場合には、対応するクラスのみを変更すれば済みます。
- コードの可読性が向上し、ポリモーフィズムによりコードがより簡潔になります。
C 言語では、関数ポインタを使用してポリモーフィズムを実現することができます。異なる機能を持つ関数は同じ関数名で宣言することができ、これにより、同じ関数名で異なる機能の関数を呼び出すことができます。
以下はポリモーフィズムの具体的な例です。
#include <stdio.h>
typedef struct shape {
void (*draw)(struct shape*);
} Shape;
typedef struct {
int x;
int y;
int radius;
Shape base;
} Circle;
typedef struct {
int x1;
int y1;
int x2;
int y2;
Shape base;
} Line;
void drawCircle(Shape* shape) {
Circle* circle = (Circle*) shape;
printf("Circle at (%d, %d) with radius %d\n", circle->x, circle->y, circle->radius);
}
void drawLine(Shape* shape) {
Line* line = (Line*) shape;
printf("Line from (%d, %d) to (%d, %d)\n", line->x1, line->y1, line->x2, line->y2);
}
int main() {
int i;
Circle circle = {
.x = 1,
.y = 5,
.radius = 10,
.base = { .draw = drawCircle }
};
Line line = {
.x1 = 2,
.y1 = 3,
.x2 = 7,
.y2 = 9,
.base = { .draw = drawLine }
};
/* //上述两段代码与下面操作是一致的
Circle circle;
circle.x = 1;
circle.y = 5;
circle.radius = 10;
circle.base.draw = drawCircle;
Line line;
line.x1 = 2;
line.y1 = 3;
line.x2 = 7;
line.y2 = 9;
line.base.draw = drawLine;
*/
Shape* shapes[2];
shapes[0] = (Shape*)&(circle.base);
shapes[1] = (Shape*)&(line.base);
for (i = 0; i < 2; i++) {
shapes[i]->draw(shapes[i]);
}
return 0;
}
上記の例では、"Shape" 構造体とそのメンバー関数 "draw" を定義しています。さらに、"Circle" と "Line" の 2 つの "Shape" から派生した構造体を定義し、それぞれ独自の "draw" 関数を実装しています。
"Shape" 構造体の中の "draw" 関数を関数ポインタとして宣言することで、"draw" 関数内で異なるサブ構造体のタイプを動的に処理することができます。
"typedef" を使用して構造体を "Shape" という名前で命名し、"Circle" と "Line" 構造体で使用するために使用しています。最後に、"Circle" と "Line" オブジェクトを作成し、それらを "Shape*" 型の配列に格納しています。ループを使用して配列を反復処理し、各形状オブジェクトの独自の "draw" 関数を呼び出して処理することで、ポリモーフィズムの効果を実現しています。