この記事を見ているあなたは、仕事で「C言語でオブジェクト指向プログラミングをしろ!」と言われたのか、
はたまた「オブジェクト指向プログミングを学びたいが、そのために他の言語を最初から学び直すのは面倒」だとか、
さまざまな理由で「C言語でオブジェクト指向プログラミング」というヘンテコなことをする羽目になっているのではないでしょうか。
この記事では、そんなあなたのために「C言語でオブジェクト指向プログラミングをするためにはどうすればよいのか?ということを紹介します。
この記事の内容は以下の通りです。
- オブジェクト指向プログラミングの簡単な説明
- C言語でクラスを実装する方法
- C言語でメソッドを実装する方法
以下の記事では、AWSを無料で勉強する方法をまとめているので、あわせて参考にしてください。
上記の記事でも紹介していますが、侍テラコヤでは解説動画が無料で視聴できるので、こちらも見てみるといいですよ!
オブジェクト指向プログラミングとは
まずはオブジェクト指向について簡単に説明します。
しかしながら、すでにご存知かもしれませんがC言語にはオブジェクト指向プログラミングの機能はありません。
オブジェクト指向プログラミングの機能がついたプログラミング言語としては、C++、Python、JavaScriptがあります。
ではそもそも、「オブジェクト指向」ってどういう意味なのでしょうか。
オブジェクト指向の意味
言葉の意味で言えば、「オブジェクト」というのは「物」や「物体」を表す英単語で、「指向」というのは「ある方向や目的に向かうこと」という意味らしいです。
つまり、「オブジェクト指向」というのは、「物(オブジェクト)を目的とすること」的な意味になります。
余計分かりにくくなりましたね…。
まずは「オブジェクト」について簡単に説明しましょう。
オブジェクトは「物」や「物体」と言いいましたが、ここでは「変数や関数」を1つにまとめたものだと思って下さい。
変数や関数を「オブジェクト」というグループに分ける、と考えた方が分かりやすいかもしれません。
今までの普通のプログラミングでは、例えば「変数xに2をかける」とか、「関数fを引数aで呼び出す」とか変数や関数なんかを目的として(指向して)何かをしてきたと思います。
その「変数」とか「関数」の代わりに「オブジェクト」を目的として何か操作をすることをオブジェクト指向と言います。
オブジェクト指向のイメージを表したのが下の図です。
例えば、「オブジェクトAの変数Xに5を足す」とか「オブジェクトAの関数fを呼び出す」なんてことをします。
ここで、みなさん構造体を思い出して欲しいです。
構造体では、変数をグルーピングし、そのメンバ変数は構造体を介して(構造体名.メンバ変数)の形式で値を書き換えたりしますよね。
これが、オブジェクト指向のイメージです。
あとで説明しますが、実際にC言語でオブジェクトを再現すると構造体を使うことになります。
では実際にプログラミングでオブジェクトを表現するにはどうするのか?を見ていきましょう。
そのためには、「オブジェクトの元」になるクラスについて説明します。
クラスとインスタンス
クラスとはオブジェクトの元となるものです。
クラスという型のようなものを作って、その型を使ってオブジェクトを作ります。
これもC言語の構造体を連想するとよいです。
まず、構造体の型を定義して、その型の構造体変数を宣言しますよね。
クラスとオブジェクトの関係は、この構造体の型と変数の関係に似ています。
ちなみに、オブジェクトはインスタンスとも呼ばれ、実際はインスタンスと呼ぶことの方が多いですね。
なので以降は、ここでもインスタンスと呼ぶことにします。
C言語でクラスとインスタンスを実装するには?
では、実際にC言語でクラスを実装する方法を見ていきましょう。
と言っても、先ほどから何度も言っているように、C言語のクラスは構造体で表します。
「構造体の型=クラス」で、「構造体変数=オブジェクト(インスタンス)」の関係です。
/* 構造体の型定義=クラス定義 */
struct sample_t {
int age;
int weight;
char name[126];
}
/* 実際に使う構造体変数の宣言 */
struct sample_t sample; // sample_t型の構造体sampleを宣言 = インスタンス生成
ちなみに、構造体では変数を「宣言する」と言いますが、インスタンスは「作る」とか「生成」と言います。
クラスが設計書で、オブジェクトがその設計書をもとに作られた実体、というイメージをするといいでしょう。
クラス変数とインスタンス変数
クラスが構造体の型定義、インスタンスが実際の構造体変数ということがイメージできたと思います。
次に、「クラス変数」と「インスタンス変数」について説明しましょう。
クラス変数というのは、同じクラスから生成されたインスタンスみんなが共通の値を持つ変数のことです。
クラス間でのグローバル変数のようなイメージです。
同じクラスからできたインスタンスは、どのインスタンスもクラス変数を自由に変更でき、誰かが変更したクラス変数は他のインスタンスもその変更が反映されます。
一方でインスタンス変数というのは、そのインスタンス内部でのみ持つ変数です。
同じクラスから生成されるので、すべてのインスタンスが同じ名前のインスタンス変数を持ちますが、値がインスタンスによって変わります。
これは構造体のメンバ変数と同じですね。
C言語でクラス変数とインスタンス変数を実装するには?
C言語でインスタンス変数を実装するのは簡単です。
最初のように普通に構造体変数を宣言すれば、その構造体のメンバ変数はそれぞれの構造体変数で値が違うので、それはインスタンス変数ということになります。
では、全ての構造体変数で共通の値を持つクラス変数をどうやって定義するのか?
これはstaticな変数とポインタを組み合わせることで実現できますが、かなりややこしくなるので説明は一旦割愛します。
メソッドとは
先ほどの例は、クラスに変数だけが定義されているパターンでした。
しかし実際のオブジェクト指向プログラミングでは、変数だけでなく「メソッド」を同時に定義します。
メソッドとは、C言語でいう関数のことです。
ただし、ただの関数ではなく、クラスで定義された関数をメソッドと呼びます。
それはつまりどういうことか?
C言語の実装例と一緒に見ていきましょう。
C言語でメソッドを実装するには?
では、実際にC言語でメソッドを実装する方法を見ていきましょう。
メソッドの再現はかなりややこしいです。完全に理解できなくても、そういう風にすればいいんだな〜くらいに思って頂いてOKです!
C言語のクラスは構造体で実装しましたよね。
なので、メソッド=関数も構造体のメンバとして定義したいのですが、残念ながら構造体ではメンバ変数に関数を定義することができません。
メンバ変数に定義できるのは、その名の通り「変数」であって「関数」ではないですからね。
ではどうするか?
ここで登場するのが「関数へのポインタ」です。
ポインタは変数ですよね。
なので、関数へのポインタは構造体のメンバ変数にすることが可能です。
構造体のメンバ変数に関数へのポインタを定義することで、擬似的に構造体に関数を定義するという寸法です。
実装例を載せます。
#include
/* 構造体の型定義=クラス定義 */
struct sample_t {
int age;
int weight;
char name[126];
int (*_get_age)(struct sample_t *); //関数へのポインタをメンバ変数に定義
};
/* 実際にメソッドの中身にしたい関数の宣言 */
int get_age(struct sample_t *self){
return self->age; //メンバ変数ageの値を返す関数
}
int main(void){
/* 実際に使う構造体変数の宣言 */
struct sample_t sample; // sample_t型の構造体sampleを宣言 = インスタンス生成
sample._get_age = get_age; //_get_ageメソッドにget_age関数を紐付ける
sample.age = 20; //sampleインスタンスのage変数に20を代入
int age;
age = sample._get_age(&sample); //sampleインスタンスの_get_ageメソッドを呼び出す
printf("age is %d\n", age); //"age is 20"が出力される
}
C言語ではオブジェクト指向の機能がないので、メソッドを実装しようとするとこのように複雑になってしまいます。
一つずつ説明するので頑張って理解してみましょう!
まずはこの部分です。
/* 構造体の型定義=クラス定義 */
struct sample_t {
int age;
int weight;
char name[126];
int (*_get_age)(struct sample_t *); //関数へのポインタをメンバ変数に定義
};
ここでは関数へのポインタを構造体のメンバ変数として定義します。
実装例の場合だと、戻り値がint型でstruct sample_t型の構造体へのポインタを引数とする関数へのポインタ_get_ageをメンバ変数に加えています。
かなりややこしいですね…。
関数へのポインタはこのように、「戻り値 (*ポインタ名)(引数)」で宣言できます。
また、引数で構造体へのポインタを取っていますが、これは呼び出した構造体自分自身のアドレスです。
つまり、インスタンスをいくつか生成した時(sample_t型の構造体変数をいくつか宣言した時)でも、そのメソッドを呼び出したい変数のポインタを引数として渡してあげます。
オブジェクト指向が搭載された言語では、引数として自分のポインタを渡さなくても勝手に自分の変数は覚えておいてくれるんですが…。
次に、メソッドとしたい関数(get_age)を定義しています。
/* 実際にメソッドの中身にしたい関数の宣言 */
int get_age(struct sample_t *self){
return self->age; //メンバ変数ageの値を返す関数
}
この関数は引数の構造体のage変数の値を返す関数です。
そんな関数作らなくても、普通にsample.ageとかでage変数の値は分かるんじゃない?と思われるかもしれません。
確かにそれは可能ですが、オブジェクト指向プログラミングではインスタンス変数はメソッド経由で値を取得したり編集したりすることが多いです。
これはオブェクト指向プログラミングの3大要素と呼ばれる要素の1つである「カプセル化」に関係するのですが、その話は次回したいと思います。
話は戻りまして、このget_age関数を、構造体の関数ポインタ_get_ageに割り当てているのが以下の部分です。
sample._get_age = get_age; //_get_ageメソッドにget_age関数を紐付ける
「関数へのポインタ = 関数名」とすることで、関数へのポインタに、関数名の関数が紐づけられます。
最後に、以下の部分の説明をします。
age = sample._get_age(&sample); //sampleインスタンスの_get_ageメソッドを呼び出す
さきほどsample._get_ageはget_ageと紐付けられたため、sample._get_age関数を呼び出すとget_age関数を呼び出すことと等価です。
なので、ここでは変数ageに、引数として渡しているインスタンスsampleのメンバ変数ageが代入されます。
まとめ
この記事ではC言語でオブジェクト指向プログラミングを行う方法を紹介しました。
オブジェクト指向の機能をC言語で実装するためにどうすればよいのかを改めてまとめます。
オブジェクト指向 | C言語 |
---|---|
クラス | 構造体定義 |
インスタンス | 構造体変数(実体) |
インスタンス変数 | 構造体のメンバ変数 |
メソッド | 関数へのポインタ |
一部は、関数ポインタなど少し難易度が高いものもあります。
以下の記事では、AWSを無料で勉強する方法をまとめているので、あわせて参考にしてください。
上記の記事でも紹介していますが、侍テラコヤでは解説動画が無料で視聴できるので、こちらも見てみるといいですよ!
コメント
コメント一覧 (2件)
それ、抽象データ型(ADT)の言語であって、オブジェクト指向とは言いません。
オブジェクト指向には確かに厳密な定義はないけれど、クラスベースのオブジェクト指向にはざっくりした合意があって、(1) 抽象データ型の機能を持つ(クラスからインスタンスが作れる、クラスの持つ変数や関数の実装をカプセル化できる)、(2) クラス継承の機能を持つ、(3) 動的束縛によるポリモーフィズムを実装している、の3つとされています。
歴史的には、C++がこの3つを備えるに至った時点で「クラスベースのオブジェクト指向」についての合意が固まり、Javaがその後に続いて世界的に認知されるようになった、という感じですね。
コメントありがとうございます!
確かにオブジェクト指向の3つの特性については承知しております…!
今回は特製の一部のみに絞って解説したもので、初心者に説明することを重視しており厳密ではない点、ご留意いただければと思います。
今後も時間があればカプセル化や継承、ポリモーフィズムについてもCで”それらしく”実装する方法も記事にできればと思っております。
今後ともよろしくお願いたします。