分割コンパイル・まとめ

試験の内容は,(情報処理1・同実習1も含め)これまでに行った実習の全ての範囲である.

特によく理解しておいてほしいものは,

などである.

分割コンパイル

main関数と同じソースファイルにすべての必要な関数を記述しておく方法は,小規模なソフトウェアであれば困ることはないが, 規模が大きくなるにつれて,見通しが悪くなり,不便になってくる.
また,複数人でプログラムを開発したり,自分が過去に作成したソースコードの一部を再利用したい場合などは,処理の機能別にファイルを分けておくと便利である.

理想的には,main 関数は,プロトタイプ宣言と最低限の変数の宣言と,処理内容を簡潔に示した関数を並べるだけ,にしたい. このようにモジュール化しておくと,部品として使用する関数を,別名のソースファイル(実装ファイルという)とヘッダファイルとして分けて作っておけば,デバッグも容易となる.

// 典型的な main 関数の例
// もちろんこのままでは動作しない

int main(void)
{
    Init();         /* 変数などの初期化処理 */
    ReadData();     /* データの読み込み,設定など */

    DoSomething();  /* 何か処理をする */
    DoOtherthing(); /* 何か処理をする */

    Output();       /* 結果の出力.画面やファイルなど */

    return 0;
}

例として,消費税込み金額の計算をする関数を別ファイルに記述し,main関数から呼び出す例を示す.
ファイル名を sub.cpp,sub.h とする.

sub.cpp sub.h
#include <stdio.h>

/* 自作ヘッダファイルは" " で囲う */
#include "sub.h"

/* 税込み金額を計算する関数の定義 */
int calc_tax(int price)
{
    int t_incl;

    /* 消費税10% */
    /* 計算結果をint型にキャスト*/
    t_incl = (int)(1.10 * price);

    return t_incl;
}
// 下記はインクルードガード.
#ifndef _SUB_H
#define _SUB_H

// 関数の宣言のみを記述.
// 実体は .cppファイルに書く.
int calc_tax(int price);

#endif

これらを用いるmain関数を含むソースファイルは

main.cpp
#include <stdio.h>
#include "sub.h"

int main(void)
{
    int orange = 498;  // みかんの値段
    int apple  = 698;  // りんごの値段

    printf("price of orange = %d yen\n", calc_tax(orange));
    printf("price of apple = %d yen\n", calc_tax(apple));
    return 0;
}

となる.

ヘッダファイル内の#ifndef _SUB_H, #define _SUB_H, #endifの部分はインクルードガードと呼ばれ,このヘッダファイルが別のファイルに重複してインクルードされるのを防ぐ役割を果たす.(これが無いと,関数の宣言が複数出現し,コンパイルエラーとなる場合がある.)

コンパイル方法は,

bcc32 main.cpp sub.cpp

と,全ての実装ファイルを同時に指定(正確にはコンパイルとリンク)する必要がある.
(試しに,main.cpp のみコンパイルしてみよ.どのようなエラーが出るか.)

練習問題

上のプログラムを作成し,コンパイル・実行して動作を確認してみよう.
(ソースファイル2個,ヘッダーファイルを1つ作成する.)

おもな復習

1. 関数(ユーザ関数)

関数の定義方法

関数とは,ひとまとまりの処理の塊であり,処理したいデータを引数として受け取り,計算などの処理を行い,その結果を返すための文法である.
具体的に,足し算の関数で説明を行う.

double add_calc(double x, double y)
{
    return x + y ;
}

この関数は,仮引数としてdouble型の変数 x, y を用意し,受け取った値を足したものを,関数add_calcとしての戻り値(返り値とも言う)として結果を返す.

この関数は値を返すので,関数名の前にdoubleという戻り値の型が宣言されている.
引数がいらない場合,または,値を返さない場合は,voidと記述する.

関数の宣言方法

関数の存在を他の関数に知らせ,利用可能とするためには,そのかたちを「宣言」する必要がある. 正確にはプロトタイプ宣言という.

例えば,main関数内で上記の関数を呼び出したければ,main関数よりも上方の行に

double add_calc(double x, double y);

のように,関数名と引数および戻り値の型を宣言しておく.(ここにはセミコロンが必要)
関数のプロトタイプ宣言さえあれば,関数の定義は別の.cppファイルに書かれていても良い.
つまり,ファイルを分割してコンパイルすることができる.
もちろん,「コンパイル」の後の「リンク」時に,関数が全て揃っていないとエラーになる.

関数の使い方

メイン関数や関数の中で,関数名および()で囲った引数を正しく記述すれば,関数の呼び出し(=関数への処理のジャンプ)ができる.
例えば,

#include <stdio.h>

int main(void)
{
    double a = 7.8;
    double b = 2.7;

    printf("a+b= %f", add_calc(a, b) );
    return 0;
}

となる.
もちろん,この記述中にはadd_calc関数の定義が書かれていないため,

の,いずれかを行う必要がある.
後述の分割コンパイルの章を参照のこと.

2. 配列と構造体

配列は「同じ型」の複数の変数を扱うための手段である.
例えば,要素数100個の倍精度実数(double)の配列は

double x[100];

のような形で表される.2次元配列であれば,

double y[100][100];

と書ける.
ちょうど1次元配列ならベクトル,2次元配列なら行列のようにみなすこともできるが, 注意しなければならないのは,配列は「変数の集合体」に過ぎず, 各要素が「1個の変数」と考えておく必要がある.
言い換えると,数学的な「ベクトル」や「行列」の性質は無く,一括での初期化や代入はできない. (従って,繰り返し文を使って,1つ1つ処理するのが基本である.)

さて,配列を使えば,「同じ型」のデータをひとまとめにできる,データ処理を行う際には複数の「異なる型」をひとまとめに使いたいことが往々にしてある. そこで利用するのが,構造体である.

/* 構造体の定義の例 */

/* 例1:住所録.型が異なる変数からなる.*/
struct address
{
    char name[20];
    int age;
    char address[100];
    int postal_code;
}

/* 例2:成績データ.すべて同じ型だが,意味が違う */
struct seiseki
{
    int kouriki;
    int math_a;
    int math_d;
    int english;
};

上記の例のように,異なる基本型を組み合わせて,新しい型を作ることができる.
構造体はデータ変数群まとめるための方法と考えておけばよく,扱いたいデータ構造の設計に非常に重要である.

3.ポインタ(アドレス参照)

一言で言えば,ポインタ変数は,「アドレス」を格納するための変数である.

int main(void)
{
    int x = 0, y = 0;
    int* p;

    p = &x;  /* 変数xのポインタ(アドレス)を p に代入.変数の型とポインタの型はそろえる */
    *p = 100;    /* ポインタの中身(つまり変数x)を代入 */
    y = *p +50;  /* ポインタの中身に50を足したものを変数yに代入 */
}

ポインタ変数(ここではp)を宣言するとき,pの値はアドレスそのものであり,アスタリスク演算子*を用いて*pと記述すれば,ポインタの中身が参照できる.
ただし,予め p = &x として,ポインタ p が別の変数 (ここでは,x) を指すようにしておく必要がある.

上記の例では,初めx== 0 となっており,ポインタを使って*pに100を代入すると,pの指す変数 x も100となる.
逆に,x=100;と代入すれば,xの値は100となり,*pの値も100となる.

ポインタ演算子:& は変数のアドレスを取り出す演算子,* はそのアドレスに格納されているの変数の値を取り出す演算子.

ポインタ利用のメリット

関数の引数としての利用(アドレス参照)

ポインタの応用として,「関数」の引数をポインタとする意義をもう一度考えてみよう.
毎度おなじみの,2つの値を入れ替える関数 irekae を考える.
まず,ポインタを使わないと,

#include <stdio.h>

void irekae(int, int);       /* 関数のプロトタイプ宣言では,このように型名だけで変数名を省略することもできる. */

int main(void)
{
    int a, b;

    printf("整数2つ入力");
    scanf("%d", &a );
    scanf("%d", &b );
    irekae( a, b );
    printf("a = %d , b = %d \n", a, b );
    return 0;
}

void irekae(int a, int b)
{
    int temp;

    temp = a;
    a = b;
    b = temp;
}

このプログラムを実行して,例えば100, 50と入力し,irekae関数を呼び出しても入れ替えは行われず, a = 100, b = 50 と出力される.
これは,main関数内の変数a,bに値が入力されたとき,関数の引数にそのまま「コピー」され,irekae関数内で入れ替えてもmain関数内に反映されないためである.
main関数内の変数 a, bと,irekae関数内の変数 a, b は,ローカル変数なので,同じ変数名であっても全く別物である.

では,ポインタを使ってみよう.

#include <stdio.h>

void irekae(int*, int*);      /* 変数名は省略,型名のみ. */

int main()
{
    int a, b ;
    printf("整数2つ入力");
    scanf("%d", &a );
    scanf("%d", &b );
    irekae( &a, &b );      /* アドレスを渡す */
    printf("a = %d , b = %d \n", a, b );
    return 0;
}

void irekae ( int* a , int* b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

一見,ポインタに*をつけて中身をやり取りしているから,同じことをしているように見えるかもしれないが, 先に述べたとおり,main関数内のa, bと関数内のa, bは文字は同じでも別物であるら,ポインタの意味合いを考える必要がある.

  1. メイン関数でa, bが定義され,scanfで a,b に値が格納される.
  2. irekae関数を呼び出す際,関数の引数に値そのものではなく,main関数の変数a, bの「アドレス」を渡す.
  3. irekae関数の引数はポインタなので,ポインタの指す相手を操作できる.
  4. ポインタ演算子*を用いて入れ替えを行うと,main関数の変数が書き換えられる.
  5. その結果,returnで戻り値を設定しなくても,main関数の変数a,bが正しく書き換えられる.

4.グローバル変数

グローバル変数とは,ソースコードの中でどの関数からも共通して参照・代入が可能な変数である.
様々な解説書や記事に,「グローバル変数はなるべく使わないこと」と記述されていることが多いが,気を付けておけば便利な場合もある. 例えば,数学・物理定数など利用する場合などがそれにあたる.

円周率を考えてみよう. 一つの方法としては,直接与える方法で,double型が約15桁の精度を持っているとすると,

const double pi = 3.14159265358979;

としてもよい.
また,算術関数を用いて定数を作る方法もある.

const double pi = 4.0 * atan(1.0);

と書けばよい. atan(1)は45度,すなわち?/4であるので,これを4倍すれば?が算出できる.

5.クラス

C++言語では,構造体に「変数」だけで無く「関数」も含めることができる「クラス」という新しい概念が追加された.
ごく簡単な例を示しておくので,その概念は理解しておいてほしい.

#include <stdio.h>    /* C の標準入出力ヘッダファイル */
#include <iostream>   /* C++ の標準入出力ヘッダファイル */
using namespace std;


class Ckeisan      /* struct の代わりに class キーワードを使用する. */
{
public:    /* <-C++固有のキーワード.変数や関数を「公開」するという意味. */

    /* メンバ「変数」.これは構造体と同じ */
    int a;
    int b;

    /* メンバ「関数」. 構造体には無く,クラス特有のもの */
    int add(void)
    {
        return a+b;
    }
    int sub(void)
    {
        return a-b;
    }
};

int main(void)
{
    Ckeisan k;    /* 計算クラス.頭文字のCはクラスの意 */

    k.a = 10;   /* 構造体と同様,Ckeisanクラスのメンバ変数に値を代入 */
    k.b = 5;

    cout << "add = " << k.add() << endl;   /* k.add()は,Ckeisanクラスのメンバ関数呼び出し */
    printf("sub = %d\n", k.sub() );   /* k.sub()は,Ckeisanクラスのメンバ関数呼び出し */
}

練習問題

上のプログラムを作成し,コンパイル・実行して動作を確認してみよう.