プログラムの分割とリンク

ここでは, [モジュール化] という立場からのプログラムの分割を見る.

プログラムを作成する上で, 機能や目的, 役割別に関数を作成することはプログラムを構造化する上で必要不可欠である. しかし, 関数の数が増加するにつれ, ソースファイルは次第に変更箇所を探すのも難しくなる程膨張する. そこで, 一つの役割を主題にした関数群を別のソースファイルにまとめることが必要になっている. そのようなソースファイルの分割を [モジュール化] と読んだいる.

まずは, 分割前のプログラムを見てみよう

prog_div.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

void myPrint(char *);

int main(void){
  char str[12] = "module test";
  myPrint(str);

  return 0;
}

void myPrint(char *x){
  printf("This is a %s.\n", x);
}

prog_div.c の実行結果は:

[cactus:~/code_c/c_tuts]% gcc -Wall -o prog_div prog_div.c
[cactus:~/code_c/c_tuts]% ./prog_div
This is a module test.

上記のプログラム prog_div.c を メインの処理部分 (main_myPrint.c) とプリント関連部分 (myPrint.c) に分けてみる.

main_myPrint.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdlib.h>

void myPrint(char *); // この関数の宣言が必要である. 別のヘッダファイルに保存しても構わない

int main(void){
  char str[12] = "module test";
  myPrint(str);

  return 0;
}

myPrint.c

1
2
3
4
5
#include <stdio.h>

void myPrint(char *x){
  printf("This is a %s.\n", x);
}
[cactus:~/code_c/c_tuts]% gcc -Wall -c myPrint.c
[cactus:~/code_c/c_tuts]% gcc -Wall -c main_myPrint.c
[cactus:~/code_c/c_tuts]% gcc -o myPrint myPrint.o main_myPrint.o
[cactus:~/code_c/c_tuts]% ./myPrint
This is a module test.

有効範囲 (スコープ)

変数名, 関数名, ラベル, タグ, 構造体や列挙体のメンバといった [識別子] (Identifiers) は, プログラム内の宣言された場所によって, プログラム内で使用可能な範囲が異なる. 使用可能な有効範囲 (スコープ, Scope) と言い, 使用可能なときその識別子は可視 (visible) であるという.

有効範囲 (Scope):

関数スコープ (関数有効範囲, Function Scope)
宣言された関数内. この有効範囲をもつのはラベルのみである.

プロトタイプ・スコープ (関数原型有効範囲, Function Prototype Scope)
プロトタイプ宣言内. この有効範囲をもつのはプロトタイプ宣言内の仮引数.


ファイル・スコープ (ファイル有効範囲, File Scope)
そのソースファイル内. すべてのブロックの外側で宣言されたか, あるいは仮引数ではない場合.

ブロック・スコープ (ブロック有効範囲, Block Scope)
ブロック {} 内で宣言されたか, 関数の定義における仮引数
/* scope.c */ // ファイル・スコープの始まり
void f(int x); // プロトタイプ・スコープ まで
double y;

int main(void)
{ // ブロック・スコープ 1 の始まり
  int n;
  { // ブロック・スコープ 2 の始まり
    char c;
    double y;
  } // ブロック・スコープ 2 の終わり
} // ファイル・スコープとブロック・スコープ 1 の終わり

外部定義

すべての関数の外側での宣言を外部宣言 (External Declaration) と言い, 外部宣言によるオブジェクトや関数の定義を外部定義 (External Efinition) と言う.

external.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

int n = 1; // この変数はファイル・スコープに有効

int main(void){
  int m = 2; // この変数はブロック・スコープに有効
  printf("n = %d, m = %d\n", n, m);

  return 0;
}

external.c の実行結果は:

[cactus:~/code_c/c_tuts]% ./external
n = 1, m = 2

リンケージ (結合)

プログラム中に同じオブジェクト名や関数名で複数の宣言があるとき, それらが同一のオブジェクト, あるいは関数として参照されることをリンケージあるいは結合という.

結合 (リンケージ, Linkage):

内部結合 (内部リンケージ, Internal Linkage)
そのソースファイル内で同一のオブジェクト, あるいは関数として参照.

外部結合 (外部リンケージ, External Linkage)
プログラムが複数のソースファイルから構成される場合, それらすべてのソースファイルにおいて同一のオブジェクト, あるいは関数として参照.

リンケージは, 有効範囲と記憶域クラスで決まる.

_images/linkage.png

リンケージがある場合の初期化ルール

linkage.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

char c = 'A'; // ファイル・スコープを持ち, 外部結合を持つ
static double pi = 3.14; // ファイル・スコープを持ち, 記憶域クラスには static を指定している. したがって, 内部結合を持つ.

int main(void){
  extern double x; // 変数 x は, 宣言済みの同一変数がないので外部結合を持つ. これには外部定義が必要である. また, ブロック・スコープを持つので初期化できない.
  
  static long l; // 変数 l は記憶域暮らすに static を付しているが, ブロック・スコープを持つため, リンケージを持たない

  return 0;
}

オブジェクトの寿命: 静的記憶域期間と自動記憶域期間

プログラムを分割する場合には一つの大域変数を複数の翻訳単位で共通に使いたい場合も考えられる. このような場合には extern 記憶クラス指定子を用いる. 例をあげてみよう.

main_extern.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* main_extern.c */

#include <stdio.h>

int x = 5; // 大域変数 x の定義

void modify(void);

int main(void){
  printf("original: x = %d\n", x);
  
  modify();

  printf("modified: x = %d\n", x);

  return 0;
}

modify.c

1
2
3
4
5
6
7
/* modify.c */

extern int x; // 大域変数 x の宣言

void modify(void){
  x *= 2;
}

上記のプログラムの実行結果は:

[cactus:~/code_c/c_tuts]% gcc -Wall -O2 -o main_extern main_extern.c modify.c
[cactus:~/code_c/c_tuts]% ./main_extern
original: x = 5
modified: x = 10
_images/extern_new.png

extern2.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int n;
static double r = 3.14;

int main(void){
  printf("n = %d\n", n);
  printf("r = %f\n", r);
  {
    extern double r; // この一行を付けなくても OK
    int n = 2;
    static long l = 1234567890L;

    printf("n = %d\n", n);
    printf("r = %f\n", r);
    printf("l = %ld\n", l);
  }

  printf("n = %d\n", n);
  printf("r = %f\n", r);
  // printf("l = %ld\n", l); // エラーになる.

  return 0;
}

extern2.c の実行結果は:

[cactus:~/code_c/c_tuts]% ./extern2
n = 0
r = 3.140000
n = 2
r = 3.140000
l = 1234567890
n = 0
r = 3.140000

extern3.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

char *f(void);

int main(void){
  char *y;
  y = f();

  printf("%s [%p]\n", y, y);

  return 0;
}

char *f(void){
  static char str[8] = "testing";
  char *x = str;
  return x;
}

extern3.c の実行結果は:

[cactus:~/code_c/c_tuts]% ./extern3
testing [0x100001068]

ソースファイルの分割と外部参照の解消

ここでは簡単なプログラム分割を行い, 各ソースファイルで個別に定義された関数やオブジェクトをリンクさせてみる.

分割前のプログラム (strPrint_All.c) は最初に見た例である.

strPrint_All.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

void strPrint(char *);
char head[] = "C Programming:";

int main(void){
  char str[8] = "testing";
  strPrint(str);

  return 0;
}

void strPrint(char *x){
  printf("%s\n", head);
  printf("This is a %s.\n", x);
}

strPrint_All.c の実行結果は:

[cactus:~/code_c/c_tuts]% ./strPrint_All
C Programming:
This is a testing.

プログラムの分割は下のようになる:

1. mainPrint.h
2. mainPrint.c
3. strPrint.h
4. strPrint.c

mainPrint.h

1
2
/* mainPrint.h */
extern char head[];

mainPrint.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include "strPrint.h"

char head[] = "C Programming:";

int main(void){
  char str[8] = "testing";
  strPrint(str);

  return 0;
}

strPrint.h

1
2
3
/* strPrint.h */

extern void strPrint(char *);

strPrint.c

1
2
3
4
5
6
7
8
9
/* strPrint.c */

#include <stdio.h>
#include "mainPrint.h"

void strPrint(char *x){
  printf("%s\n", head);
  printf("This is a %s.\n", x);
}

上記のプログラムの実行結果は:

[cactus:~/code_c/c_tuts/strPrint]% ./mainPrint
C Programming:
This is a testing.

付録: Makefile

Makefile を使った make コマンドによるコンパイルには, いくつかの利点がある:

プログラムが複数のソースファイルから構成される場合, その依存性を記述しておくことができる.

変更されたソースファイルのみをコンパイルし, 実行ファイルを生成してくれる.

コンパイルのオプションを記述しておくことができる.

コンパイル, インストール, クリーニングなどの作業を記述して, それらを簡単に実行できる.

make コマンド:

make [options] [target]

% make -h

また, target とは, Makefile に記されたものである. その一般形式は次の通りである:

target: prerequisites
        command

これを [ルール] と呼ぶ. make コマンドは, Makefile を探し, そこに記述されている target の command を実行する. command に指定するのは, UNIX のコマンドである.