free(malloc(sizeof(MRM)));

虚無・アクセラレーション

C言語でポリモーフィズム

C言語にはオブジェクト指向に関する機能はないですが,ポリモーフィズムを実現する方法をいくつか紹介します.

まずC言語でオブジェクトの継承を表現します.

以下のサンプルにおいて,Personオブジェクトは「名前」という情報を持っています. StudentTeacherPersonを継承しており,Studentには学年,Teacherには教えてる教科名を持たせます.

new_studentnew_teacherStudentTeacherのコンストラクタです.

#include <stdio.h>
#include <stdlib.h>

typedef struct Person {
    char *name;
} Person;

typedef struct Student {
    struct Person base;
    int grade;
} Student;

typedef struct Teacher {
    struct Person base;
    char *subject;
} Teacher;

Student *new_student(char *name, int grade) {
    Student *n = malloc(sizeof(Student));

    ((Person *)n)->name = name;
    n->grade = grade;

    return n;
}

Teacher *new_teacher(char *name, char *subject) {
    Teacher *n = malloc(sizeof(Teacher));

    ((Person *)n)->name = name;
    n->subject = subject;

    return n;
}

int main(void) {
    Student *john = new_student("john", 4);
    Teacher *garin = new_teacher("garin", "English");

    printf("%s %s\n", ((Person *)john)->name, ((Person *)garin)->name);
    printf("%s\n", john == &john->base ? "john == &john->base" : "john != &john->base");
}

実行結果

john garin
john == &john->base

以上のように,構造体(子オブジェクト)の一番最初のメンバに継承させたいオブジェクト(親オブジェクト)の変数を持ってくれば継承が実現できます.

子オブジェクトへのポインタの値 == 親オブジェクトへのポインタの値が常に成り立つため(ここではjohn == &john->base),親オブジェクトへのアップキャストが可能になります. f:id:admarimoin:20191206013521p:plain

自己紹介をさせよう

 先生と生徒で合計6人いる教室の全員に自己紹介をさせます.

教室にいる人は先生と生徒なので(つまり人間),自己紹介させるプログラムは以下のようになるはずです.

void introduce(Person *person);

int main(void) {
    Person *class_member[6] = {
        (Person *)new_student("john", 4),
        (Person *)new_teacher("garin", "English"),
        (Person *)new_student("takashi", 1),
        (Person *)new_teacher("hoge", "math"),
        (Person *)new_student("fuga", 6),
        (Person *)new_student("piyo", 2)
    };

    for(int i = 0; i < 6; i++) {
        introduce(class_member[i]);
    }

    return 0;
}

ここからintroduce関数を2つのやり方で実装していきます.

Person構造体に生徒か先生かの情報を持たせる

生徒か先生かという情報を定義して,

enum PersonType {
    TYPE_STUDENT,
    TYPE_TEACHER,
};

Person構造体のメンバに追加します.

typedef struct Person {
    char *name;
    enum PersonType type;
} Person;

そして,各コンストラクタでtypeの初期化を行わせます.

Student *new_student(char *name, int grade) {
    Student *n = malloc(sizeof(Student));

    ((Person *)n)->name = name;
    ((Person *)n)->type = TYPE_STUDENT;    // here
    n->grade = grade;

    return n;
}

Teacher *new_teacher(char *name, char *subject) {
    Teacher *n = malloc(sizeof(Teacher));

    ((Person *)n)->name = name;
    ((Person *)n)->type = TYPE_TEACHER;    // here
    n->subject = subject;

    return n;
}

introduceでは素直に生徒か先生かを見て出力を変えればOKです.

void introduce(Person *person) {
    printf("My name is %s. ", person->name);

    switch(person->type) {
    case TYPE_STUDENT:
        printf("%dth student.\n", ((Student *)person)->grade);
        break;
    case TYPE_TEACHER:
        printf("%s teacher.\n", ((Teacher *)person)->subject);
        break;
    default:
        puts("space alien");
    }
}

出力結果

My name is john. 4th student.
My name is garin. English teacher.
My name is takashi. 1th student.
My name is hoge. math teacher.
My name is fuga. 6th student.
My name is piyo. 2th student.

②関数ポインタを使う

 Person構造体に関数ポインタをもたせます.introduceは引数にPerson *を取り,戻り値はvoidなので,

typedef struct Person {
    char *name;
    void (*introduce)(struct Person *);
} Person;

となります.

次に,先生と生徒にそれぞれ自己紹介させる関数を実装します.

void intro_student(Person *student) {
    printf("My name is %s. %dth student.\n", student->name, ((Student *)student)->grade);
} 

void intro_teacher(Person *teacher) {
    printf("My name is %s. %s teacher.\n", teacher->name, ((Teacher *)teacher)->subject);
}

そして,各コンストラクタでintroduceを初期化します.

Student *new_student(char *name, int grade) {
    Student *n = malloc(sizeof(Student));

    ((Person *)n)->name = name;
    ((Person *)n)->introduce = intro_student;    // here

    n->grade = grade;

    return n;
}

Teacher *new_teacher(char *name, char *subject) {
    Teacher *n = malloc(sizeof(Teacher));

    ((Person *)n)->name = name;
    ((Person *)n)->introduce = intro_teacher;    // here

    n->subject = subject;

    return n;
}

この変更に伴い,main関数でのintroduceの呼び出しは,

    for(int i = 0; i < 6; i++) {
        class_member[i]->introduce(class_member[i]);
    }

のようになります.

出力結果

My name is john. 4th student.
My name is garin. English teacher.
My name is takashi. 1th student.
My name is hoge. math teacher.
My name is fuga. 6th student.
My name is piyo. 2th student.

C言語はいいぞ.

いかがでしたか?