こんにちは
「C言語によるオブジェクト指向プログラミング入門」を読んだレビュー,感想,備忘録的なのです。
C言語しか知らない7年前(2015年)に購入して,本棚で冬眠させていました。
(授業がC言語だったのに,この本を買った翌週にPythonやらされて萎えた思い出。)
積み本消化も兼ねて...
Amazon
初版の物理本で読みました。
対象読者
本書の「はじめに」にも詳しく書かれている。
・昔からC言語を使用していて,C++などの知識がない人。
・学生時代にオブジェクト指向型の言語を学んだが,実務でC言語しか使えなくて八方塞がりの人。
個人的に下記をプラス。
・C++やJavaなどのオブジェクト指向型言語で書けばオブジェクト指向になると思っている人。
・C言語はオブジェクト指向が出来ないと思っている人。
・ライブラリ開発に関して知りたい人。
・プログラミングの基礎は学んでいるので入門書以外でC言語を学びたい人。
学生時代の課題は「C言語でよろしく」な環境の人にもオススメ。
あと,組み込みメインの人(むしろこの人たちが対象な気がする)。
あくまでC言語でオブジェクト指向を学ぶのが題材なので, オブジェクト指向そのものを学びたい人は別の書籍が良いかもしれません。
ライブラリ開発についても触れているので,ライブラリ開発に興味がある人にもおススメ。
-> 題材はC言語ですが,基本的な考え方はどの言語でも同じはず。
-> gccのコンパイルオプションやMakefileの詳細についても書かれてる。
-> 静的リンク,動的リンクもサンプルを通して理解できる。
C言語のキャストやポインタをあまり理解していない人にもおススメかも。
-> リファレンスカウンタの話題などがあるので勉強になるはず。
MacやWindowsでなくLinuxベースで解説されている。
-> シグナル処理にも触れてる。
メモ
ざっくり。
のはずが。。。
はじめに
C++やJavaなどのオブジェクト指向プログラミング言語を説明して,
C言語でオブジェクト指向プログラミングをする理由を説明してる。
-> 組み込み系は資産やコンパイラなどの関係からC言語をよく使ってるよね的な話。
-> C++に移行することなしに,オブジェクト指向を実現したい需要がままある。
1章でオブジェクト指向の簡単な説明。
2章で実用的なライブラリ作成の説明。
3章以降でC言語によるオブジェクト指向の説明。
より良いプログラムを書くためには,オブジェクト指向のほかに構造化プログラミングやライブラリ作成についても知る必要があるね。
第1章 オブジェクト指向とは
規模の小さいプログラムなら雑に設計しても全体を把握できるけど,規模が大きくなると修正や機能拡張が指数的に困難になるよね。
このとき,ソースコードを管理する(gitとかではない)手法の1つに「オブジェクト指向プログラミング」がある。
オブジェクト指向前夜 - 構造化プログラミング
・オブジェクト指向が普及する前に主流だった開発手法で「はじめにプログラムの全体構造を考え,段階を追って細分化していく」考え方。wikipedia
・プログラマのメイン作業であるプログラミングの作業分割は,大きく2つに分けて「処理別」と「機能別」が考えられる。
・ゲームの初期化を考えると,画面やサウンドの初期化など,機能別に細分化していくことが「段階的詳細化」と呼ばれる。
・構造化プログラミングの3要素は「連接」・「選択」・「反復」で,合わせて「基本制御構造」と呼ばれる。
-> 全プログラム(アルゴリズム)は3つの基本制御構造に分解でき,これを「構造化定理」と呼ぶ。
つまり構造化プログラムとは,目的の処理に対して「段階的詳細化」を行い,「基本制御構造」のみで記述できるようになるまで分解していくことになる。
-> 基本構造が順番に並んでいるため,どの部分がどの構造にあたるか明確で理解しやすい。
鬼ごっこゲームのソースコードを題材に,ディレクトリやファイル分割についての考え方が書かれている。
-> 「ネットワーク部分担当ね」と「全機能の初期化部分を担当ね」と言われたら後者は混乱しやすいよね。
オブジェクト指向プログラミング
オブジェクト指向の書籍には「オブジェクト指向の概念を抽象的に説明している内容」と「C++やJavaなどを利用してオブジェクト指向で書く内容」が多いように思える。
-> 前者は「プロジェクトマネージャ」寄り。「委譲」とか「動的束縛」とか説明してるよね。
-> 後者は「プログラマ」向けの本だが,「オブジェクト指向の本」というよりは「プログラミング言語の解説書」に近いものが多い気がする。
オブジェクト指向という考え方は「プログラミングパラダイム」の話題なので,本来はプログラミング言語に依存しない。
-> C言語やアセンブリでも理屈的には実現可能なはず。
-> classなどの「オブジェクト指向」をサポートする言語でないとオブジェクト指向が出来ない訳ではない。
-> 本来は言語に対して考えるのでなく「このプログラムはオブジェクト指向で書かれている」というように「プログラムに対して」言及する。
オブジェクト指向対応言語は下記機能でオブジェクト指向プログラミングを支援する。
(あくまで「支援」で,どうプログラムを書くかは本人次第。)
1. オブジェクト指向的な書き方を支援する文法機能。
-> classとか。
2. 文法がオブジェクト指向に寄っているので,自然とオブジェクト指向に近づきやすい。
-> オブジェクト指向非対応言語だと好きに書けるのでオブジェクト指向になりにくい傾向がある。
JavaとC言語の比較。
Javaの場合
-> 言語仕様に強制力があり,意識しなくてもある程度は奇麗に見える。
・ファイル名はクラス名と(基本的には)一致。
・1ファイルには(基本的には)1つのpublicなクラスのみ。
C言語の場合
-> 統一的な書き方がないのでやりたい放題できる。
・書き方が自由なので,複数人開発の場合はポリシーを設定しとくのが良い。
・大規模開発の場合は,コーディング規約などを決めるコストがかかる(強制ではないが…)。
オブジェクト指向対応の言語との違い。
1. オブジェクト指向で書こうとしたときに,冗長な部分をカバーしてくれる。
-> 継承とか。
2. 文法が自然とオブジェクト指向になりやすいようになっている。
C言語でオブジェクト指向をするときに最も困難なことは「開発メンバー全員がきちんと趣旨を理解している」こと,つまり「教育」。
・カプセル化してあるのにメンバ変数を無暗に書き換えるなどされては意味がない。
・きちんと理解していないと「この部分はオブジェクト指向だが,あの部分は違う」などのキメラ化が進む。
オブジェクト指向を1言で言うと「分類の仕方」となる。
(以下,本書の鬼ごっこプログラムのファイル分割の例。)
構造化プログラミングの場合
・init.c: 各種初期化ルーチン
display, sound, keyboard, playerなど。
・game_main.c: ゲームメインルーチン
player_move, rabit_move, catch_monsterなど。
・done.c: 各種終了処理のルーチン
display_done, sound_done, player_doneなど。
オブジェクト指向の場合
・display.c: ディスプレイ関連
display_init, display_doneなど。
・sound.c: サウンド関連
sound_init, sound_doneなど。
・player.c: プレイヤー関連
player_init, player_move, player_doneなど。
オブジェクト指向の最大の特徴は下記の2つ。
1. オブジェクト単位に分類するので,コード保守がオブジェクト単位。
2. オブジェクト単位に分類するので,機能(オブジェクト)を動的に切り離しやすい。
オブジェクト指向の大きな効果は「プログラム中のデータをオブジェクト単位であつけるので,直感的に理解しやすいプログラムが書ける」ようになること。
第2章 ライブラリの作成
プログラム開発の大部分はライブラリ開発で,オブジェクト指向においては「クラスライブラリ開発が開発工数の大部分を占める」ことになる。
この章ではオブジェクト指向の準備としてライブラリ作成について触れる。
-> ライブラリの速度性や誤差,チューニング,共有ライブラリについても触れているので結構本格的。
-> マクローリン展開によるsin, cosの実装方法についても解説されているので興味があれば読んでみて。
高速三角関数ライブラリ
sin関数, cos関数を題材としてライブラリ作成について触れる。
-> 数学ライブラリに含まれるsin, cosは高い精度まで計算している。
反面,ゲームなどの精度より速度を重視する場合は独自高速化されていることが多い。
-> 配列に予め計算したものを詰め込む(LUTかな)版と線形補完版を作成する。
条件的な奴
・与える角度の有効数字が3桁程度なので,戻り値も3桁程度にする。
・必要メモリサイズは32ビットマシンでsizeof(int)*360=720バイトの想定。
sin関数
・sinX = sin(180-X)
・sinX = -sin(180+X)
の定理を使うので,第1象限の0から90度の領域分の配列で第2~第4象限までカバーでき,
メモリも約1/4に節約できるが速度が犠牲になるので360の配列を1周分として,コンパイル設定で1/4周分を切り替えられるようにする。
cos関数
・cosX = sin(90+X)
の定理を利用する。
角度intで360個のsinを値を前もって計算して配列に押し込む版
-> 数学ライブラリを利用して計算するのとマクローリン展開で独自計算があるが,独自計算の方を利用する。
・初期化処理について。
-> sin関数, cos関数の呼び出し時に初期化済みかのチェックを行う。
-> 初期化を行うsin関数と初期化を行わないsin関数を作成し,関数ポインタで動的に切り替えると初期化判定コストが安くなる。
typedef double (*sin_func_t)(const int); static sin_func_t fastSindFunc = FastSindInit; static double FastSindNoInit(const int degree) { return (sin(degree * (M_PI / 180.0))); } static double FastSindInit(const int degree) { // ここで初期化処理。 fastSindFunc = FastSindNoInit; return FastSindNoInit(degree); } static fastSind(const int degree) { return fastSindFunc((degree)); }
ライブラリの構成
fastsin - include - fastsin.h
- configure.h
- init.h
- init.c
- fastsin.c
- Makefile
・パブリックヘッダファイル
ユーザがライブラリ利用時にインクルードするための外部に公開するヘッダファイルのこと。
-> fastsin.h が該当。
-> ライブラリ仕様書の役割もあるため,ライブラリ関数のプロトタイプ宣言やマクロ,構造体の定義などを行う必要がある。
-> ライブラリ中で内部的に利用している関数などは含めてはいけない(fastsin.hに書かれているもののみユーザは利用できる)。
一度公開したものは安易に仕様変更してはいけない。
-> パブリックヘッダファイルの変更はライブラリ仕様変更とほぼ同義。
・設定用ヘッダファイル
ライブラリのビルド時設定など,ライブラリ中のソースコード全体から参照されるような定義を行う。
-> configure.h が該当。
-> 設定可能なパラメータ類などはこのヘッダにまとめておくとよい。
・プライベートヘッダファイル
ライブラリ内部の閉じた環境で利用されるもので,ライブラリファイルの分割時に出来たヘッダファイルのこと。
-> init.h が該当(init.cはユーザに対して公開するものではないため)。
-> configure.h も該当する。
・ライブラリ本体
ライブラリの実装が書かれているファイル。
-> fastsin.cとinit.c が該当。
・Makefile
ライブラリのビルドとインストールに使用されるmakeコマンドに必要なファイル。
-> ライブラリのビルド方法とインストール方法のマニュアル的な役割も持つ。
ライブラリのリスト
・fastsin.h
パブリックヘッダファイルでユーザに公開されるので,コメントを丁寧に入れる。
-> 多重インクルード対策を忘れずに。
・configure.h
マクロ定数などの定義はこのファイル。
-> ユーザも設定できるため,コメントはなるべく入れる。
-> make時に設定できるようにしておく。
-> #error でパラメータ設定ミスを通知してあげられるといいね。
#ifndef SHORT_INT_ARRAY // #define SHORT_INT_ARRAY #endif #ifndef FINE_DEGREE #define FINE_DEGREE(360*1) #endif
とあるとき,
$ make CFLAGS="-DSHORT_INT_ARRAY -DFINE_DEGREE=3600"
や
$ gcc -c fastsin.c -DSHORT_INT_ARRAY -DFINE_DEGREE=3600
などでコンパイル時に定義できる。
・init.h/init.c
ライブラリ実装者が利用するものなので裁量はおまかせ。
-> #includeの<>でくくったファイル名は,/usr/includeなどから検索される。
-> ダブルクォーテーションの場合は,カレントディレクトリから検索され,無ければ標準ヘッダの格納場所を検索。
sin(0)とsin(360)は同じ結果だが,剰余計算は一般的には時間がかかるのでメモリを1つ余分に確保する方を選択していた。
-> 高速化とメモリはトレードオフ関係になりやすい気がする。
・fastsin.c
マクロであっても公開する必要がないものは.cファイルの先頭部分に定義している。
インライン関数にすることで,関数内処理が関数呼び出し箇所に直接埋め込まれるので高速。
-> 関数コール時のスタック操作コストが無くなり高速化する。
-> 複数箇所から呼び出されている場合は,同じ処理が該当箇所に埋め込まれるためオブジェクトサイズが増大する。
また,キャッシュの利用効率が悪化するため,逆に低速になる場合もある。
-> インライン関数内で定義しているローカル変数用にスタックは消費されることもある。
-> 長期運用してく過程でいろいろな場所で呼び出されるようになるとボトルネックになる可能性があるので,濫用は注意。
・Makefile
Makefile上で定義したパラメータは,そのMakefileが利用される場合のみ有効。
-> Makefileのあるディレクトリ内のみで有効なので,ディレクトリ外からのインクルードなどによりヘッダファイル内の定義の整合性が取れなくなる可能性がある。
また,構造体のサイズが変わったことによるメモリ破壊など,わかりにくいバグの原因になることがあるので注意。
がっつり紹介されているので気になったら読んでね。
ライブラリアーカイブ
ライブラリはコンパイル済みのオブジェクトファイル形式で提供することが可能。
-> ライブラリが巨大化するにつれ,オブジェクトファイルの数が増大するため,コンパイル済みのオブジェクトファイルを全て提供するのは現実的でない。
通常,いくつかのオブジェクトファイルを1つにまとめたアーカイブ形式で提供する。
-> Unixでは/usr/lib や /usr/local/lib をみると libc.aやlibcurse.aなどのファイルが大量に存在している。
アーカイブ形式でまとめられたライブラリを「ライブラリアーカイブ」と呼ぶ。
ライブラリのインストール
install: all mkdir -p $(INCLUDE_DIR) cp $(INCS) $(INCLUDE_DIR) cp $(TARGET) $(LIBRARY_DIR) uninstall: rm $(LIBRARY_DIR)/$(TARGET) rm -fR $(INCLUDE_DIR)
$ make installを実行することで,ヘッダファイルをINSTALL_DIR,INCLUDE_DIRで指定したディレクトリにインストールする。
また,libfastsin.aをINSTALL_DIR, LIBRARY_DIRで指定したディレクトリにインストールする。
-> /usr/local 以下の適切なディレクトリにヘッダファイルfastsin.hとライブラリlibfastsin.aがインストールされる。
#include <fastsin/fastsin.h>
を行い,リンク時に-lfastsinオプションを使用することでfastsinライブラリを使用できるようになる。
コンパイラによっては/usr/local 以下がヘッダファイルやライブラリの検索対象に含まれていない場合もある。
$ gcc -I/usr/local/include ...
のように-Iオプションで検索対象に含めることができる。
$ gcc -L/usr/local/include ...
のように-Lオプションでライブラリの検索対象を追加できる。
Unixは一度インストールしたものは削除しないという考え方があるため,ソフトウェアによってはアンインストール用のターゲットが存在しない場合もある。
-> ユーザの立場で考えるとアンインストール用のターゲットは用意しておくのが良い。
init.h などのプライベートヘッダファイルは$ make install でインストールされるべきではない。
-> ユーザが必要とするもののみインストール(公開)する」という考え方。
評価
sin関数,cos関数の性能評価。
Makefileの活用法なども解説されているが,基本的には数学よりの話なので軽く流す。
-> コンパイルオプションに最適化設定についても言及されている(結果込み)。
性能評価時はキャッシングの効果や最適化,ページング,ホットスポットなどを考慮する必要がある。
-> キャッシング効果は絶大。
-> ページングは,仮想メモリ機能により特定サイズ単位でメモリ上のデータをハードディスク上に退避・復旧する動作。
-> ホットスポットは,処理時間の大部分を占めている箇所。
雑に結果だけ抜粋。
・fastsind: 1.40秒
・fastsinf: 4.76秒
・数学ライブラリsin: 12.70秒
-> 本書では誤差についても書かれている。
高速化のヒント
1. 配列サイズを360->512にして,剰余演算子をマスク計算に置き換えてアクセス。
2. 初期化用関数を作成し,明示的に初期化して初期化済みフラグ参照を無くす。
3. 「角度は負の値にならない」を前提とすることでfast_sinf()のifを減らす。
4. 条件分岐を減らす。
結構詳細に書かれているので興味があれば。
-> 沼に片足突っ込むレベルで書かれてた。
共有ライブラリ
作成したライブラリは一般的に「静的ライブラリ」と呼ばれる。
-> 共有ライブラリで配布されるのが一般的。
-> 共有ライブラリはハードディスク,メモリなどの資源に対して高効率。
静的ライブラリは,実行形式の作成時に本体がリンクされ,実行時に結合される。
-> 静的リンク。
-> 実行形式に埋め込まれるためファイルサイズが大きくなる。
共有ライブラリは,実行形式のリンク時にどの共有ライブラリがリンクされるかという情報が埋め込まれる。
-> 実行時に,実行形式とは別に共有ライブラリがロードされる。
-> ハードディスク的には共有ライブラリ本体が1で十分なので,ディスクページ節約となる。
-> 実行時に仮想メモリシステムによって,テキスト領域などの共有できる部分は複数プロセス間で共有されるため,実行時メモリサイズも節約できる。
-> 実行時に動的リンクされるため動的ライブラリとも呼ばれる。
動的ライブラリと共有ライブラリは厳密には意味が異なる。
-> 動的ライブラリは,ハードディスク上のサイズを節約する。
-> 共有ライブラリは,使用メモリを節約する。
-> 共有ライブラリの実現には動的リンクが不可欠で,実際には似た意味で使われる。
共有ライブラリはOSに依存する部分が多いため注意。
-> .dllとか.soってコト!?
Cファイルのコンパイル時には「位置独立コード」を作成する必要がある。
-> $ gcc -c fastsin.c -fpic -O -Wall の-fpicのこと。
共有ライブラリのコンパイル方法,Makefile,インストールについて詳細が書かれているが長いので省略。
まとめ
・ランダムアクセスして性能評価したけど,値が順次参照する用途だと速いかもね。
-> グラフィックスの円描画時の座表計算など。
・メモリに値を確保しているから,ページングが発生すると遅くなるかも。
・精度,メモリ量,速度のトレードオフがあるよ。
-> ユーザが調整できることも大切。
-> ハッシュを利用すると速度と使用メモリ量のトレードオフを調整できるよ。
第3章 オブジェクト指向に必要な概念とC言語による実装
「抽象化」,「カプセル化」,「継承」,「多様性」の4つに加え,「動的なオブジェクトの生成」についても説明される。
-> C言語はオブジェクト指向のサポートがないので,実装者がオブジェクト指向を意識して書く必要がある。
-> プログラムの題材はシューティングゲーム(CUIベースなので面倒なことはない)。
その1. 抽象化
プレイヤーや弾丸などのあるひとまとりのデータのことを「オブジェクト」と呼ぶ。
-> 利用者が実際の処理を気にすることなくオブジェクトを扱えるようにすることを「抽象化」と呼ぶ。
新しく作成した抽象型のことを「クラス」と呼ぶ。
-> 本書では構造体をクラスみたいに扱っている。
-> 変数に相当するクラス実体を「オブジェクト」と呼ぶ。
-> 実体は「インスタンス」と呼ぶ。
-> メモが雑で訳わかんなくなりそうなので,C++のクラスをイメージしておけばよい。
その2. カプセル化
オブジェクト指向プログラミングでは,クラスはソースコードを構成する1単位として扱う。
-> 他クラスとはっきり独立させる必要がある。
-> あるオブジェクトを操作したい場合は,勝手にメンバを弄るのでなく,オブジェクトに「命令」を送信する。
-> オブジェクトに送信する命令を「メッセージ」と呼ぶ。
オブジェクトの持つメンバ変数は隠ぺいする。
-> 直接弄るのでなく,メッセージを介して操作する。
-> オブジェクトの独立性を高めることを「カプセル化」と呼ぶ。
-> C言語でいうところの「構造体」によってオブジェクトを抽象化して,構造体操作用の関数(メソッド)を追加したもの。
オブジェクトの独立性を高めることが「カプセル化」の目的である。
-> 内部メンバを隠蔽することが目的ではない。
クラス定義をライブラリ化したものは「クラスライブラリ」と呼ばれる。
-> オブジェクト指向プログラミングは,ひたすらクラスライブラリを作成していくこととも考えられる。
その3. 動的オブジェクトの生成
C言語でクラスを作成するときのイメージ↓
-> classがないのでこんな感じになるはず。
#ifndef PLAYER_H_INCLUDE #define PLAYER_H_INCLUDE typedef struct Player_ * Player; int PlayerGetX(Player* const player); int PlayerGetY(Player* const player); int PlayerGetVX(Player* const player); int PlayerGetVY(Player* const player); void PlayerInit(Player* const player, const int x, const int y, const int vx, const int vy); void PlayerMove(Player* const player); void PlayerDone(Player* const player); #endif // PLAYER_H_INCLUDE
相互インクルードは意識しよう。
-> 後に解説されるみたい。
Player.cで構造体の定義を行い,Player.hでは構造体へのポインタを定義してクラスの使用時にポインタを利用する。
-> C言語では未定義の構造体へのポインタを定義できることを利用する。
-> C言語での隠蔽実装的な奴だと思う。
未定義の構造体へのポインタ代わりにvoid*を利用できるが,コンパイラの型チェックを捨てることになるので非推奨。
-> 将来的にポインタがらみのバグに悩まされるかもよ...
その4. 継承
シューティングゲームに弾丸を継承した誘導ミサイルを実装することを考える。
-> C言語には継承機能はないので,リストを利用して継承を再現している。
構造体の先頭に別の構造体を配置して,ある構造体の仕組みを継承させる。
-> リストを継承した場合,リンクリストを操作する関数の追加は,struct listに関するものだけ作成すればよい。
-> PersonやCarはstruct list構造体を継承していると考えられる。
-> 構造体の先頭に別の構造体を配置する手法は,C言語ではよく使われる。
typedef struct Person_ { struct list list; const char* m_name; int m_bloodType; } Person; typedef struct Car_ { struct list list; const char* m_name; int m_type; } Car;
Person* p; Person person; insertList((struct list*)p, (struct list*)(&person));
外側の構造体へのポインタで,内側の構造体へアクセスすることで,struct TypeBをstruct TypeAに継承させている可能性があることに注意。
struct Type A { struct Type B { int value; } }; struct TypeA* type_a =; ((struct TypeB*)type_a)->value = 0;
実体ごとにサイズを変更できるような場合は注意。
-> コメントで構造を明記しておくのが良い。
struct String { int length; char string[1]; }; void Create(char *string) { struct string* stringObject; ... stringObject = (struct string*)malloc(size); ... }
誘導ミサイルの場合はこんな感じ。
typedef struct Missile_ { Shot shot; // 継承 int m_quick; Player m_target; } Missile;
プライベートヘッダを作成しても文法的にアクセスを抑止できない点に注意。
-> ShotP.h(PrivateのP)とShot.h(公開用)を用意しても,ShotP.hをインクルードされてしまうことは防げない。
-> コーディング規約は大事だね。
コンストラクタとデストラクタに相当する関数は意識して作成する必要がある。
-> 文法的に機能がないため。
has-a関係とis-a関係
-> 動作的には同じだが,考え方に大きな違いがあるので注意。
ShotGetStruct(&(missile[i]->Shot)); // has-A ShotGetStruct((Shot)(missile[i])); // is-a
その5. 多態性
キャストでShotクラスを継承したMissileクラスを実現し,Shotクラスのメソッドを利用できた。
-> ShotクラスからMissileクラス操作(逆)はできるだろうか。
Shot shot; MissileMove((Missile)shot);
・C言語の立場
shot変数のメモリ領域はShot構造体の分しか確保されていない。
-> コンパイルは通るが,不正メモリアクセスとなる。
-> Segmentation faultは起こらず,関係ないメモリ領域を参照するはず。
・オブジェクト指向の立場
MissileはShotの1種だが,ShotはMissileの1種ではない。
Shot p; if (key == SHOT) p = CreateShot(); else p = (Shot)CreateMissile();
リストなどでオブジェクトを一般化して扱おうとするとダウンキャストのような問題が発生する。
-> C++では仮想関数があるので問題ない。
-> C言語では仮想関数の仕組みを実装する必要がある。
-> 関数ポインタをうまく利用する。
typedef struct Shot_ { int x, y; void (*moveMethod)(struct Shot_* shot); void (*destroyMethod)(struct Shot_* shot); } Shot; void ShotMove(Shot shot) { (*(shot->moveMethod))(shot); } int main() { ... // 弾丸だろうがミサイルだろうが同じ関数で処理できる。 for (p = listHead; p != NULL; P = p->next) { ShotMove(p->shot); } ... }
関数ポインタは使い方によってはプログラムがきれいになるが,関数呼び出し部分を見ただけでは実装が見えない。
-> デバッグが面倒(動作させないとわかりにくい)。
-> 適切な動作を行ってくれるのでクラス間の違いを意識する必要がない(ShotMove)。
まとめ
オブジェクト指向考え方の基礎は「モノ」を「モノ」として扱うこと。
その他の話題
その他の話題にしては濃い内容。
・バックポインタ
ほかのオブジェクトから参照されているにも関わらずオブジェクトを消去すると。存在しないオブジェクトへの参照が発生する。
-> 対処法1: オブジェクトにバックポインタを持たせる。
-> 対処法2: オブジェクトにリファレンスカウンタを持たせる。
参照先から参照元を逆引きするようなポインタを「バックポインタ」と呼ぶ。
-> 削除するオブジェクトへの参照を潰しておく機能を削除オブジェクトのメソッドに組み込んでおく。
・リファレンスカウンタ
バックポインタによる対処だとオブジェクト間の参照構造が複雑になった場合にデッドロックの原因になりやすい。
-> バグ原因を発見してもオブジェクト消去のタイミングを調整する必要があり,修正が困難になりやすい。
オブジェクトにカウンタを持たせ,カウンタを増減させる方式を「リファレンスカウンタ」と呼ぶ。
-> 参照先のオブジェクトのカウンタを増やし,参照関係が無くなったときにカウンタを減らす。
-> オブジェクトを削除する際はカウンタが0になっているか確認する。
-> 削除時にカウンタが残っている場合は未動作フラグなどを立てて動作しないようにしておく。
実装が簡単で動作が確実なため,オブジェクト指向以外でもよく使用される。
・どこまでをクラスにするのか
オブジェクトが相互にメッセージをやり取りするように実装する。
突き詰めるとmain.cの記述内容は下記の通りかな。
・コマンドライン引数の処理
・シグナルの設定
・すべてのオブジェクトの親となるオブジェクトの生成と消去
どこまでをオブジェクト指向で書くかは設計者の自由。
-> 闇雲に全てをクラス化しようとしても複雑になるだけ。
(一生完成しない神システムになるかもしれないしね)
-> 複数の書き方を組み合わせて,適所で応用する力のほうがずっと重要。
・シグナル処理について
Unix環境で動作するアプリケーションは,大体シグナル処理を入れる。
-> Ctr-cやkill -INT [pid]などでプロセスにシグナル送信して強制終了。
-> 何も考えずにexit()しても,メモリ解放はOSが適切に行ってくれると思うし。
組み込み機器用のOSは貧弱な場合が多いため,アプリ側で責任をもってメモリ解放する必要が多い。
また,きちんとした終了処理を行いたい場合も多々あるはず。
・終了処理を必要とするライブラリを使用しているため(cursesライブラリなど)。
・OSが貧弱でメモリ解放処理はアプリ側に一任されている。
シグナル処理には2つ方法がある。
1. シグナルハンドラ内部で全親のオブジェクトを消去。
2. シグナル受信時にフラグを立て,プログラム中ではフラグを見てbreakする。
ハンドラ内部でメモリ解放する。
void signalHandler(int value) { if (objectがNULLでない) DestroyAllObject(); exit(1); } int main(void) { signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); CreateObject(); gameMainLoop(); exit(0); }
オブジェクト生成中にシグナルハンドラが呼ばれた場合は?
-> メモリ確保中なのでオブジェクト消去が行われず,メモリ解放されない。
-> malloc()内部でシグナルが発生するかも。
-> シグナル処理の根源的な問題。
シグナル処理は「どこで呼ばれるかわからない」を意識する必要がある。
-> フラグを利用した方法が一般的に利用されている。
フラグを利用したシグナル処理。
-> グローバル変数のフラグをシグナルハンドラ内で立て,フラグが立っている場合は終了させる。
-> いつシグナルが発行されるか意識する必要がない。
-> 対処漏れが発見しやすい。
int terminated = 0; void signalHandler(int value) { terminated = 1; } int main(void) { signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); while(true) { if (terminated) break; } exit(0); }
シグナル処理の難しさはタイミング依存のバグが多いこと。
-> 大体再現できない。
・構造体の先頭メンバは本当に先頭にあるのか?
Shotクラスを継承したMissileクラスは,Missile構造体の先頭にShot構造体を置いていた。
-> 構造体の先頭メンバは先頭に配置されていることを前提としている。
-> 構造体の先頭に空白領域を作るコンパイラだとヤバいが,大体大丈夫なはず。
構造体の定義時のメモリ配置方法は,Application Binary Interface(ABI)という仕様で決められている。
-> 異なるコンパイラで作成したオブジェクトファイル同士でも相互リンクできるようにするため。
CPU毎に仕様は変わるが,構造体の先頭メンバはメモリ上で先頭に置かれること,構造体のメンバのメモリ配置はメンバ定義順に一致することは多くのABIで保証されているよう。
・is-aとhas-aの関係
C言語では完全な隠ぺいが出来ない。
場合によってはis-aよりhas-aの方がよい場合もある。
-> プライベートヘッダの意味はなくなるが,実装は楽。
時と場合に応じて臨機応変に対応すべし。
・相互インクルードの問題
相互インクルードの対策は下記が主流。
1. 各種ファイルのインクルードやポインタ定義の順番を調整。
2. typedefs.hのようなファイルを作成し,ポインタ定義など,よく使われそうな型を定義する。
3. 相互インクルードする際に最初に定義が必要なものはインクルードガードの外側に出しておく(インクルードとインクルードで挟むやり方)。
だいたい1か3のやり方が採用されやすい。
-> 個人的には1がいいかな。
第4章 オブジェクト指向プログラミングの実装例(設計編)
実際のプログラミング時に発生しやすい相互インクルード,隠蔽性,メモリ開放漏れ,エラー処理などについても解説されてる。
クラス作成の際のオブジェクト間関係についての考え方も載せてあった。
大体知っているので流し見。
デバッグの考慮
#defineマクロでデバッグレベル別に機能を定義する。
・デバッグレベル低ではマクロの中身が空など。
第5章 オブジェクト指向プログラミングの実装例(開発編)
鬼ごっこゲームを題材として,実際にどんな感じでプログラミングするかが解説されている。
基本的には今までの総まとめなのでメモは割愛。
まとめ
予想していたよりかなり面白い内容でした。
C言語を扱うことは中々無いですが,C++でも通ずる内容だったので良かった。
C言語メインの人は読んでおくといいかも。