今回の内容はファイル分割に関するお話が中心になります。ソースコードが長くなってくると一つのファイルだけで管理するのが難しくなってきます。(皆さんのパソコンもごちゃごちゃしてきたらフォルダを作成して整理をすると思います)他にもファイルを複数に分けられることで、複数人が同時並行で作業を進めることができますよね。
そんな中規模から大規模なプロジェクトを行う際に役に立つのがファイル分割です。チーム開発をする際にはマストな知識なので頑張って勉強しましょう!
前回の復習
前回は構造体、列挙型、共用体についてのお話しをしました。
構造体
構造体というのは複数の値をまとめて管理できるもののことでした。配列との違いは型を統一する必要がない点ですね。そのため配列では『角括弧[]』を用いて要素にアクセスできましたが構造体の場合はアクセスしたいメンバを『.』や『->(アロー演算子)』を用いて指定する必要がありました。
列挙型
列挙型というのは新しい型を作ることができるものことでした。内側では整数値で管理されているため演算をすることができることを学びましたね。
共用体
共用体というのはメンバのデータを2つ以上同時に保持できない構造体のようなものでした。これを使う場面は使用できるメモリが限られている場面ぐらいなので基本的には構造体を使えば大丈夫でしょう。
それぞれの項目について詳しく知りたい方は構造体【C言語講座 #14】を参照して下さいね。
前回の練習問題の解答例
A問題
#include <stdio.h>
typedef struct {
char *name;
int age;
char gender;
} students;
void setStudents(students *t, char *c, int a, char b) {
t->name = c;
t.age = a;
t.gender = b;
return;
}
int main() {
students st;
char ch[] = "takeshi";
int x = 55;
char s = 'm';
setStudents(&st, ch, x, s);
printf("%s, %d, %c\n", st.name, st.age, st->gender);
return 0;
}
解答例
#include <stdio.h>
typedef struct {
char *name;
int age;
char gender;
} students;
void setStudents(students *t, char *c, int a, char b) {
t->name = c;
t->age = a;
t->gender = b;
return;
}
int main() {
students st;
char ch[] = "takeshi";
int x = 55;
char s = 'm';
setStudents(&st, ch, x, s);
printf("%s, %d, %c\n", st.name, st.age, st.gender);
return 0;
}
自作関数内では構造体のポインタを使っていて、main関数の中では普通の構造体を使っています。そのため『.』と『->』の使い方は上記の通りになります。
自作ヘッダーファイル
ここからが今回の記事の内容になります。最初は自作ヘッダーファイルについてのお話です。
ヘッダーファイルというのは以前にも説明していますが簡単に復習しておくと、関数の存在が書かれたファイルのことで、#includeを使ってコンパイル前に読み込まれるファイルのことでしたね。記事の前半ではこのヘッダーファイルを自作してみるというお話になります。ヘッダーファイルを自作することでソースファイルから一部のコードを切り分けることができます。
自作ヘッダーファイルの中にかけるものについて勉強するために、C言語のプログラムがどんなもので構成されているのかを考えてみましょう。
- プリプロセッサ指令
- 構造体や列挙型や共用体
- プロトタイプ宣言(自作関数の宣言)
- グローバル変数
- main関数
- 自作関数
大体のプログラムはこんな感じの項目に分けてあげると四角で囲めると思います。これらのうち自作ヘッダーファイルに書き込むことが多いのは上の3つになります!
試しに以下のtc1501.cのソースコードをtc1502.hとtc1503.cに分けてみます。自作ヘッダーファイルの拡張子は『.h』になるので気をつけて下さいね。
#include <stdio.h>
#define TAX 108
struct fruits {
char *name;
int price;
};
int calc(int pri);
int n = 3;
int main() {
struct fruits f[n];
for(int i = 0; i < n; i++) {
char ch[100];
int x;
scanf("%s %d", ch, &x);
f[i].name = ch;
f[i].price = calc(x);
printf("%s, %d\n", f[i].name, f[i].price);
}
return 0;
}
int calc(int pri) {
pri = pri * TAX / 100;
return pri;
}
実行結果
apple 200 //入力値
apple, 216
orange 300 //入力値
orange, 324
banana 150 //入力値
banana, 162
上記の箇条書きであげたものを全て取り入れたコードです。これをヘッダーファイルに分けたものが下の2つのファイルになります。
#ifndef FRUITS_H_
#define FRUITS_H_
#include <stdio.h>
#define TAX 108
struct fruits {
char *name;
int price;
};
int calc(int pri);
#endif
#include "tc1502.h"
int n = 3;
int main() {
struct fruits f[n];
for(int i = 0; i < n; i++) {
char ch[100];
int x;
scanf("%s %d", ch, &x);
f[i].name = ch;
f[i].price = calc(x);
printf("%s, %d\n", f[i].name, f[i].price);
}
return 0;
}
int calc(int pri) {
pri = pri * TAX / 100;
return pri;
}
実行結果
aaa 100 //入力値
aaa, 108
bbb 200 //入力値
bbb, 216
ccc 300 //入力値
ccc, 324
このように分けることができます。分けた際にそれぞれのファイルに少し書き足されている部分があるのでそれについて説明します。まずはヘッダーファイル(tc1502.h)の方からです。
ヘッダーファイルの一番上と一番下に見慣れないディレクティブが追加されています。これはインクルードガードと呼ばれるもので、2重にインクルードをすることをあらかじめ防ぐ働きをしています。1行目で『FRUITS_H_』という名前の記号定数(シンボル名と呼ばれたりもする)がdefineされているかを調べています。今回使われているのはifndefなので『FRUITS_H_』という記号定数が定義されていない時に#endifまでの処理を実行するようになっています。もちろん2回目以降は実行されないように2行目で#define FRUITS_H_で記号定数を定義しています。(#ifndefと#defineと#endifの行は書く癖をつけておきましょう!)記号定数の名前に関しては何でも大丈夫ですが他のところで使っているものと被らないように『ヘッダーファイル名+H』みたいな自分ルールを設けておくと被らなくて済みますね。
勘違いをしないようにあえて書いておきますがこれらの項目を全てヘッダーファイルに分けなければいけない訳ではなく、あくまでヘッダーファイルに分けられるものとソースコードに書くべきものを説明しただけなのでヘッダーファイルの作り方には個人差があります。例えば#includeはヘッダーファイル内に書く人もいれば、ソースコード側に書く人もいます。ただ2重にインクルードするとコンパイル時にエラーが出てしまうので、どちらか片方だけに書くようにしましょう。
ちなみに#ifndefの他にも#ifdefや#else, #ifなどがあるのですが話が逸れていきそうなので下のコラムで説明しておきますね。
次にソースファイル(tc1503.c)の方を見ていきます。こちらは1行目に自作ヘッダーファイルを読み込んでいるだけなので、わかりやすいですね。自作のヘッダーファイルの場合はファイル名をダブルクォーテーションで囲みます。(コンパイルされたヘッダーファイルを使う場合は自作であっても『<』と『>』で囲みます)
これだけで自作のヘッダーファイルを作ることができます。また自作のヘッダーファイルは使い回すことができます。使う際にはtc1503.cの時と同様のやり方でインクルードしてあげれば大丈夫です。記事の後半で紹介するファイル分割のお話でも登場します!
#ifndef, #ifdef, #endif, #if, #elif, #elseについて
ディレクティブについてはプリプロセッサ指令【C言語講座 #11】で勉強しましたね。その時に説明を飛ばしていたこれらのディレクティブの使い方について説明しておきます。
説明すると言ってもほとんどif文と同じような感じで分岐ができるだけになります。違う点はディレクティブなのでコンパイル前のプリプロセスで処理される点です。それぞれのディレクティブの使い方ですがディレクティブの名前をよく見ていただければ簡単に分かるようになっています。(『if/n/def』, 『if/def』, 『end/if』, 『if』, 『el/if』, 『else』みたいな感じで区切りが見えます)
それぞれの意味は概ね以下のようになっています。
『if』はそのままif。『el』や『else』はelse。『end』は条件分岐の終わりを示している。『n』はnot。『def』はdefine。
『ifndef』はdefine(def)されていない(not)場合(if)に#endifまでの間の処理が実行されます。
『ifdef』はdefine(def)されている場合(if)に#endifまでの間の処理が実行されます。
『if』は条件式とセットで使って条件式が成り立っていれば#endifまでの間の処理が実行されます。成り立っていない時に実行したい処理がある場合は#elseを追記します。
『elif』はelse if と同じで3つ以上に分岐させたい時に使うことになります。
これらより概ね使い方が推測できると思います。一応それぞれのディレクティブの基本形を載せておきます。
#ifndef 記号定数
//記号定数がdefineされていない時に実行される処理
#endif
#ifdef 記号定数
//記号定数がdefineされている時に実行される処理
#endif
#if 条件式1
//条件式1が成り立っている時に実行される処理
#elif 条件式2
//条件式1が成り立っていなくて条件式2が成り立っている時に実行される処理
#else
//条件式1も条件式2も成り立っていない時に実行される処理
#endif
ちなみに以下のように#ifを使って#ifdefと同じ処理をすることもできます。
#if defined(記号定数)
//記号定数がdefineされている時に実行される処理
#endif
#if !defined(記号定数)
//記号定数がdefineされていない時に実行される処理
#endif
ファイル分割
ここからはファイル分割についての話をしていきます。さっきまでの話はヘッダーファイル1つとソースファイル1つに分ける話でしたが、実はソースファイルやヘッダーファイルを複数に分割することができます。基本的な文法は前半の自作ヘッダーファイルの文法が理解できていれば新しく覚える文法はないので気楽に読んでもらえれば大丈夫です。
早速ですがひとつソースコードを見てもらいます。以下のサンプルは1つのヘッダーファイルと2つソースコードからなっています。
#ifndef Calc_H_
#define Calc_H_
int add(int a, int b);
int sub(int a, int b);
#endif
#include <stdio.h>
#include "tc1504.h"
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
#include <stdio.h>
#include "tc1504.h"
int main() {
int x = 10, y = 4;
printf("%d + %dは%dです\n", x, y, add(x, y));
printf("%d - %dは%dです\n", x, y, sub(x, y));
return 0;
}
実行結果
10 + 4は14です
10 - 4は6です
今回のソースコードは自作関数の定義をtc1505.cの方にかいてtc1506.cはmain関数のみになるようにしてみました。こうすることで何千行というコードからmain関数を探す必要がなくなるわけです。main関数で呼び出している関数がどんな処理をするのかが一目でわかるようになっていれば作業効率がさらにアップしますね。
ファイル分割をした際のC言語のプログラムをコンパイルするには、一括してまとめてコンパイルする方法と、分割して1つずつコンパイルを行っていく方法の2通りあります。それぞれのやり方について説明をしていきます。
一括してコンパイルする方法は単純です。例として上のtc1504.h, tc1505.c tc1506.cの3つのファイルからなる場合を用いて説明しておきます。この場合コンパイルする際には以下のように記述することで複数のソースコードからなるプログラムをコンパイルすることができます。
gcc tc1505.c tc1506.c -o tc1505
./tc1505
gcc -o tc1505 tc1505.c tc1506.c
./tc1505
どちらの書き方でも大丈夫ですが私個人としては上の書き方の方が直感的にもあってる気がするので上の書き方しかしてません。
一括してコンパイルする場合はどこか1つでもミスがあるとコンパイルエラーになってエラーをつぶすのが面倒くさいというデメリットがあります。
次は分割コンパイルのやり方について説明します。コンパイル時に『-c』オプションをつけてコンパイルするとオブジェクトファイル(拡張子が『.o』のファイル)が生成されます。これらを用いて再度gccでコンパイルしてあげることで実行ファイルを作ることができます。
gcc -c tc1505.c
gcc -c tc1506.c
gcc tc1505.o tc1506.o -o tc1505
./tc1505
分割コンパイルを行うとソースコードをパーツごとにコンパイルできるのでエラー箇所を特定しやすくなり、バグを短時間で修正するのに向いています。
このように複数のソースファイルが存在する場合は全てをコンパイラに渡してあげるか、オブジェクトファイルを作成してからまとめてコンパイラに渡してあげる必要があります。
ここからは少し話の方向を変えてどういう基準でファイルを分けるべきなのかという話をしていこうと思います(自作関数の数だけソースファイルを作るとかとんでもないことにならないように……)。
最近の開発現場ではオブジェクト指向という開発スタイルが盛んに唱えられています。この背景にあるのはデバイスの性能の向上です。C言語が登場した当時は使えるメモリの容量も限られており、なるべくメモリを節約しながらプログラムを書くのが『正義』とされていました。しかしマシンの性能が良くなるにつれ大きなプログラムが実行できるようになりました。今ままで通りの省メモリ的な考えでプログラムを書くとムダに複雑な処理をしなければならなくなり人間の頭がついていけなくなりました。つまりPCの処理の限界より人間の思考の方が限界になってきたという訳です。
このマシンのスペックが向上した背景によりプログラムを組む際の考え方が『PCファースト』から『人間ファースト』に180度変化しました。これにより人間にとってわかりやすいソースコードを書くのが良いとされるようになりました。
オブジェクト指向というのはその作法のようなものです。残念ながらC言語ではオブジェクト指向は使えませんが、ファイルを分割する際の指標として考え方を流用することはできます。
簡単にオブジェクト指向について述べるならば、『オブジェクト単位で分ける』ことになります。オブジェクトというのは『物』ですね。例えばオンライン決済なんかでお金を払うプログラムを想定しましょう。ここに登場するオブジェクトは『レジ(計算する物)』ですね。オブジェクトをイメージすることで『レジ』に関係するコードをひとまとめにして管理しておこうと考えることができ、コードがどこにあるかわからなくなる問題を解消することができます。また似たようなプログラムを書く際にも、オブジェクト単位で管理をすることでコードの再利用がしやすくなる利点があります。
ファイル分割の話からマシンの進化がもたらした変化をお話しました。ファイル分割の分け方の指針としてオブジェクト指向の考え方は非常にシンプルでわかりやすいので是非参考にしてみて下さい。
まとめ
今回の記事は主にファイルの分割を中心とした話題でお話ししてきました。大きいプログラムを組む際にはファイル分割をすることになるので将来的に必要になってくる知識です。
今回の内容を簡単に振り返っておきましょう。
- 自作ヘッダーファイルを作成する際には二重にインクルードをしないようにifndefディレクティブを用いてインクルードガードを行う癖をつけておこう。
- ファイル分割をする際に、ソースファイルを2つ以上作成する場合はコンパイル時にすべてのソースファイル(またはオブジェクトファイル)をコンパイラに渡す必要がある。
次回はファイルへの読み書きについて勉強していきましょう!本当は標準関数としてまとめて扱うつもりでしたがファイル操作が苦手な人が多そうなのでここだけは集中的に取り扱うことにします。
最後まで記事を見ていただきありがとうございます。また別の記事でお会いできることを祈っております。