プログラムを分割する
これまでの章では、処理を一つのソースファイルに記述してきました。しかし、ファイルが肥大化したり、複数のプログラムで共通の関数を使用したい場合など、ソースコードを複数のファイルに分けて管理する必要が生じます。
この章では、ソースファイルの分割とコンパイル方法、ヘッダファイルの作成、関数や変数の共有について説明します。
プリプロセッサ
ソース分割の説明をする前に、プリプロセッサについて説明をします。
プリプロセッサは、C言語のコンパイル前にソースコードに対して実行される前処理を行うプログラムです。
プリプロセッサを使用すると、ソースコードは指定された文字列や数式に置換されたり、他のファイルが取り込まれたり、条件に応じてコンパイルされる部分が変更されることができます。
プリプロセッサは、シャープ記号(#)で始まるディレクティブを使用して操作されます。
プリプロセッサには、以下のような主な機能があります。
– #include ディレクティブ:ヘッダファイルや他のソースファイルをインクルードすることにより、関数や変数の宣言及び定義を共有することが可能です。
例えば、#include <stdio.h>
と書くと、標準入出力関数を使えるようになります。
– #define ディレクティブ:マクロを使用して、定数や数式に名前を割り当てて定義することができます。
たとえば、プログラムに「#define PI 3.14」と記述すると、’PI’という文字列はプログラム内のすべての箇所で’3.14’という文字列に置換されます。’3.14’を’3.14159’に変更する場合は、マクロの定義を変更するだけで済み、プログラム中に数字が直接記述されている場合よりも意味が明確になるという利点があります。
マクロは関数のようにも使用できます。たとえば、#define SQUARE(x) ((x) * (x)) というマクロを定義すると、プログラム内でSQUARE(5)と書かれた部分は((5) * (5))に置き換えられます。
#include <stdio.h>
#define PI 3.14 // 円周率を定義する
#define SQUARE(x) ((x) * (x)) // 二乗を計算する数式を定義する
int main(void) {
double r = 5.0; // 半径
double area = PI * SQUARE(r); // 面積を計算する
printf(“半径 %f の円の面積は %f です\n”, r, area);
return 0;
}
– #if, #ifdef, #ifndef, #else, #elif, #endif ディレクティブ:条件コンパイルは、式やシンボル(マクロの名前)の値に応じてコンパイルされるコードの部分を変更できる機能です。
例えば、#ifdef DEBUG
と書くと、DEBUG というシンボルが定義されているときだけ、その後の部分がコンパイルされます。#endif
で終わります。
#include <stdio.h>
#define DEBUG // DEBUG を定義する
int main(void) {
int a = 10;
int b = 20;
int c = a + b;
#ifdef DEBUG // DEBUG が定義されているとき
printf(“a = %d, b = %d, c = %d\n”, a, b, c); // 変数の値を表示する
#endif
printf(“c の値は %d です\n”, c);
return 0;
}
この例では、DEBUGが定義されている場合に限り、変数の値を表示するコード行がコンパイルされます。
プリプロセッサの機能を活用することで、ソースコードの可読性や再利用性を向上させ、デバッグや移植性の改善が可能です。しかし、過度に使用すると、ソースコードが不必要に複雑になり、予期しないエラーやバグの原因となることもあります。
プリプロセッサの機能は適切に使うことが大切です。
ソースファイルの分割
たとえば、以下のソースプログラムを対象にしてみましょう。
#include <stdio.h>
int sum(int a,int b);
int main(void){
int x = 1;
int y = 2;
int z;
z = sum(x,y);
printf("%d + %d = %d\n",x,y,z);
return 0;
}
int sum(int a,int b){
int s = a + b;
return s;
}
このソースプログラムをmain()側と関数側に分割します。
list 6-1
#ifndef _SUM_H
#define _SUM_H
//sum()のプロトタイプ宣言
int sum(int a,int b);
#endif
sum.h
#include "sum.h"
int sum(int a,int b){
int s = a + b;
return s;
}
sum.c
#include <stdio.h>
#include "sum.h"
int main(void){
int x = 1;
int y = 2;
int z;
z = sum(x,y);
printf("%d + %d = %d\n",x,y,z);
return 0;
}
main.c
ヘッダファイルについて
このサンプルプログラム(list 6-1)では、sum.hを作成して、main.cとsum.cでインクルードしています。
これまで、標準ライブラリの関数を使用する際には、stdio.hやstring.hといった既存のヘッダファイルを利用してきました。これらのヘッダファイルには、printfやstrcpy_sといった標準関数の宣言(プロトタイプ宣言)が含まれています。
自作の関数に対しても、ヘッダファイルを作成し、その中に関数の宣言を記述します。
ヘッダファイルは、拡張子が.hであり、一意の名前が必要です。インクルードガードと呼ばれる方法を使用して、ヘッダファイルが複数回読み込まれることによる重複定義のエラーを防ぎます。
ヘッダファイルの書式は以下の通りです。
#ifndef 識別子
#define 識別子
プロトタイプ宣言;
構造体のtypedef宣言など;
#endif
識別子には、他のヘッダファイルの識別子と重複しないような名前を指定します。通常はヘッダファイル名を大文字に変換した形で名前を付けます。
#ifndefは「もし定義されていなければ」を意味します。
#ifndefの後に指定された識別子が未定義である場合、#ifndefから#endifまでの間のコードが読み込まれます。
次に#defineを使用してインクルガードで使用するための識別子を定義します。
#ifndefで指定した識別子を#defineで定義すると、2回目以降のインクルード時には#ifndefと#endifの間のコードは読み込まれないようになります。
標準ライブラリのヘッダファイルをインクルードする際には「<>」(山括弧)を使用しますが、自作のヘッダファイルをインクルードする際には「””」(ダブルクォーテーション)を使用します。
「<>」(山括弧)は、主にC言語の標準ライブラリのヘッダファイルに使用され、コンパイラは予め定義されたディレクトリ(コンパイラのインストール時に設定されたパス)を検索します。
一方、「””」(ダブルクォーテーション)は、自作のカスタムヘッダファイルに使用され、コンパイラはソースファイルと同じディレクトリ内を検索します。
「<>」(山括弧)と「””」(ダブルクオーテーション)の使い分けにより、標準ライブラリとユーザー定義のファイルを区別し、ヘッダーファイルの性質や位置を明確に示すことができます。
複数ファイルのコンパイル方法
複数のファイルをコンパイルする場合、gccコマンドでは以下のように入力します。
gcc main.c sum.c -o sum
コンパイルするソースファイル名として「main.c」と「sum.c」を列挙しています。ヘッダファイルはインクルードによって、ソースファイルに読み込まれるので指定しません。
-o sumは実行ファイル名を指定しています。
変数の共有
グローバル変数について
C言語 7日目で取り上げましたが、C言語にはグローバル変数が存在します。
グローバル変数は、どの関数からでもアクセス可能であり、プログラムが終了するまでメモリ上に保持されるという特性を持っています。
以下のように、グローバル変数を用いたプログラムを書くことができます。
#include <stdio.h>
int calc(int a,int b);
int g_th = 1;
int main(){
int x = 10;
int y = 20;
int z;
for(int i=0;i<10;i++){
z = calc(x,y);
printf("[%d](%d,%d)=%d\n",g_th,x,y,z);
x = x + g_th;
y = y + g_th;
}
return 0;
}
int calc(int a, int b){
int x;
if(g_th %2 == 0){
x = (a+b)/g_th;
}else{
x = (a+b) * g_th;
}
g_th++;
return x;
}
list6-2
実行結果
[2](10,20)=30
[3](12,22)=17
[4](15,25)=120
[5](19,29)=12
[6](24,34)=290
[7](30,40)=11
[8](37,47)=588
[9](45,55)=12
[10](54,64)=1062
[11](64,74)=13
グローバル変数をファイルをまたいで参照する方法
上記のソース(list6-2)をファイルを分割する場合、以下のように記述することができます。
list 6-3
#ifndef _CALC_H
#define _CALC_H
int calc(int a,int b);
#endif
calc.h
#include <stdio.h>
#include "calc.h"
int g_th = 1;
int main(){
int x = 10;
int y = 20;
int z;
for(int i=0;i<10;i++){
z = calc(x,y);
printf("[%d](%d,%d)=%d\n",g_th,x,y,z);
x = x + g_th;
y = y + g_th;
}
return 0;
}
main.c
#include <stdio.h>
#include "calc.h"
extern int g_th;
int calc(int a, int b){
int x;
if(g_th %2 == 0){
x = (a+b)/g_th;
}else{
x = (a+b) * g_th;
}
g_th++;
return x;
}
calc.c
calc.cでextern int g_th;
と宣言しているところに注目してください。
「extern」とは「外部」を意味し、変数に「extern」宣言を行うことで、その変数が外部ファイルで定義されているとコンパイラに教えています。extern int g_th;
が定義されていない場合、calc.cファイルではg_thが未定義とみなされ、コンパイラによって’error: ‘g_th’ undeclared (first use in this function)’というエラーが生成されます。
また、extern宣言を使用する際には、extern int g_th = 0; のように変数を初期化してはなりません。externは、他のファイルで定義された変数の存在を宣言するために使います。
externを利用することで、グローバル変数を外部ファイルからも利用可能になります。しかし、グローバル変数を使用すると、データがどこからでもアクセス可能になり、意図しない書き換えのリスクが生じるため、実務では使用を避けるべきです。
例えば、g_thの値が500以上を許可しないという仕様だった場合、トラブルを引き起こす可能性が十分にあります。
トラブルを避けるためにextern宣言ではなく、static変数を使用する方法があります。
static修飾子について
上のソースプログラム(list 6-3)をstatic修飾子に書き換えたプログラムを示します。
list 6-4
#ifndef _CALC_H
#define _CALC_H
int calc(int a,int b);
int getTh(void);
int setTh(int val);
#endif
calc.h
#include <stdio.h>
#include "calc.h"
static int g_th = 1;
int getTh()
{
return g_th;
}
/* 通常は0、エラーのときは-1を返す */
int setTh(int val)
{
if(val >= 0 && val <= 500){
g_th = val;
return 0;
}else{
return -1;
}
}
setth.c
#include <stdio.h>
#include "calc.h"
int calc(int a, int b){
int x;
int g_th = getTh();
if(g_th %2 == 0){
x = (a+b)/g_th;
}else{
x = (a+b) * g_th;
}
g_th++;
setTh(g_th);
return x;
}
calc.c
#include <stdio.h>
#include "calc.h"
int main(){
int x = 10;
int y = 20;
int z;
int g_th;
for(int i=0;i<10;i++){
z = calc(x,y);
g_th = getTh();
printf("[%d](%d,%d)=%d\n",g_th,x,y,z);
x = x + g_th;
y = y + g_th;
}
return 0;
}
main.c
setth.cというソースファイルを新たに作成し、グローバル変数int g_th = 1;
にstatic修飾子を付与しています。
static修飾子を使用すると、他のファイルからは参照できなくなります。
setth.c内にg_thの値を取得する関数getTh()
と値をセットするsetTh()
関数を用意し、外部ファイルからg_th
には、これら用意した関数を使用してアクセスするように変更しました。
これにより、g_th
に不正な値が代入されることを防ぐことができるようになります。
static修飾子はローカル変数や関数でも使用することができます。
- 関数の先頭にstatic修飾子を付けた場合
static 戻り値 関数名(引数型 引数名,,){
}
と記述します。ソースファイル内部からのみ参照可能で、他のソースファイルからは参照できません。
- ローカル変数にstatic修飾子を付けた場合
以下のような特徴があります。
①関数内で宣言された変数は、その関数内でのみ参照可能です。
②ローカル変数ですが、プログラムが終了するまでその値を保持します。
③ローカル変数の初期値は、初期化時にのみ設定可能です。関数が何度呼び出されても、初期化は最初の一回だけ行われます。
以下にstatic修飾子をローカル変数に付与したサンプルプログラムを示します。
#include <stdio.h>
int func()
{
static int num=5;
printf("num=%d\n",num);
num++;
}
int main(void)
{
func();
func();
func();
}
実行結果
num=5
num=6
num=7
コメント