C/C++ + SLLIB + SFITSIOによるデータ解析講習会 資料 [part1]

山内@天文データセンター

2013年7月 初版
会場: 天文データセンター


本講習会を企画した背景

C/C++の必要性とそれらに関する天文学者向けの情報不足の解消

現在のデータ解析においては,スクリプト言語や解析パッケージが 多用されていますが,解析ソフトウェアの検証,解析手法を工夫し た独創的な研究,あるいはデータのサイズが大きいなどの場合には, ピクセル単位の高速処理が可能な CやC++が必要な場面が多いものです.

しかし,CやC++の開発環境は汎用性を前提としたものであるため, データ解析のための実用的なツールとしては遠く, さらには,技術の選択肢が多いために, 全体として難易度が高いのが実情です.また, 書籍などの情報面をみても, 天文学者が本当に必要とするものが十分であるとは言えません.

このような背景から, 天文学者が本当に必要とする実用的な開発環境として, ライブラリセット「SFITSIO + SLLIB」が開発されました. さらにこの講習会は,天文学者にとって必要な技術面での情報不足を 解消するために企画しました.

天文業界のアマいソフトウェア品質

科学研究を実施する時において最も重要と言われている事として 「正しさ」「再現性」があります.さらに現代では早期の研究成果 出版の必要性から「高速処理」も求められます. しかし実際には,天文業界においては,「正しさ」や「再現性」は あまり重視されていないのが実状です.その根拠としては,

といった状況があげられます.

にもかかわらず,天文業界では 既存のソフトウェア(主に外国製)が信用されすぎているようにも感じます. みんなが使っているから安心…本当でしょうか? 自分である程度大きいプログラムが書けるようになると, 少しコードを読めばそのツールのソフトウェア品質に対する取組みが 見えてくるものですが,実際のソフトウェアのコードをみてみると, 実はその品質はあまり高くありません.

本来は,天文業界で一般的に行なわれている「動けばOK」的な開発ではダメで, 科学研究で重要な「どこでも正しく動作する」事に対する品質や「高速処理」 が求められるはずです. そういった事の追及には, ソフトウェア品質を高めるためのノウハウ(コーディングルールや試験)や 最新のハードウェアに関する知識が求められます. このように考えると,ソフトウェア開発というのは,科学研究と同じで 終わりのない研究課題だと言えます.

この講習会の第1部では,そのようなソフトウェア品質に関するノウハウの一部を取り上げ, 科学用途のソフトウェア開発で重視すべき事やその大変さを知っていただく事も 目的の1つとなっています.


本講習会の内容

本講習会の目的は, C/C++によるデータ解析を行ないたい研究者やプログラマの方々が, 解析結果の信頼性を第一とし, 高いパフォーマンスを狙えてかつ開発効率の良いコーディングスキルを 身につけていただく事が目的です.

本講習会は上記の目的に沿った3部構成になっており, 教科書や雑誌ではなかなか取り上げられない内容を多く含んでいます.


第一部: もっと安全なC言語 目次


第1部: もっと安全なC言語

C言語は最高のパフォーマンスを狙えるかわりに, 安全面での保護の仕組みが十分ではありません. ちょっとした勘違いによるバグであっても, プログラムが予想不能な挙動を示す事も多々あり, バグの修正に不必要な時間をとられている,といった光景をよく見かけます. そんな目にあうのは誰でも嫌なものですが,では, どのようにすればバグを最小化できるのかを, 第1部では具体的な例をあげなら考えていきます.


安全性を高めるための基本的な考え方: ワンパターンに持ち込む

プログラマは必ずバグを作ります. バグを減らすための最も基本的なテクニックは, ワンパターンに持ち込み,多くの記法を使わない事です. 別の言い方をすれば,技術をもて遊ばない事です. 技術をもて遊んだ結果バグを作るというのは,初級者が最も陥りやすい罠です.

ここでは,いくつかの例を示し, ワンパターンに持ち込むとはどういう事なのかを,解説します.


この記法は本当に必要か?

C言語には,無くても困らない記法が存在します. 例えば,次に挙げるものはほとんど使う必要がないものです. 本当に必要かどうかよく考えてみましょう.


変数や関数のスコープを制限しよう

多くの変数がコードの大きな範囲で有効になっていると, コードがわかりにくくなり,バグの原因になります. 変数や関数のスコープについても,「ワンパターン」の考え方を実践する事が 大切です.

次の例は,私が使っているパターンの例です.変数の宣言は 「{」の直後にまとめて書くようにしています.

int function( ... )
{
    double v;

    if ( ... ) {
        int i;
        for ( i=0 ; i < n ; i++ ) {
            int j;
            double tmp;
            for ( j=0 ; j < m ; j++ ) {
                :
                :
            }
        }
    }

    v = ...;

    return v;
}

関数についても,まずは「static」宣言をつけて作るという 「パターン」を持っておきます.

static int my_private_function( ... )
{

グローバルな関数にする事に決めた時点で,「static」を消去します.


1から数えない

カウンタの使い方もパターン化しておきます.

Cではポインタ変数を使う以上, カウンタ等の数え方を 0 からに統一しておきたいものです. 「0から」「1から」が混在しているとバグの原因になります.

どうしても 1 からの値が必要なら,ループ中で 1 を加算します.

for ( i=0 ; i < n ; i++ ) {
    const int ii = i+1;

for文でのカウントダウン

for文でのカウントアップは,

for ( i=0 ; i < n ; i++ ) {

のように,パターンとして覚えている方も多いと思います. では,カウントダウンの場合(n-1 から 0 まで)はどうしますか? 例えば,次のコードには,ある危険性が潜んでいます. わかりますか?

for ( i=n-1 ; 0 <= i ; i-- ) {

上記のコードの場合,i が unsigned だった場合, 無限ループに陥ってしまいます.
では,正解例を示します.

for ( i=n ; 0 < i ; ) {
  i--;
    :
    :
}

正解は他にもありますが,singed でも unsigned でも 安全な「1つの書き方」を持っている事が大事です.


ポインタ変数を改変しない

次の2つのコードをご覧ください. このコードで登場する 「p」「p_next」「buf」 はポインタ変数です.

  p_next = buf + n;
  for ( p=buf ; p < p_next ; p++ ) {
    *p = ...
  }
  for ( i=0 ; i < n ; i++ ) {
    p[i] = ...
  }

どちらの方がわかりやすいかというと,当然2番目のコードと思います. 1番目はパフォーマンスを狙ったのかもしれませんが, 残念ながらあまり効果はなく,読みにくいだけのコードになってしまっています. このように,ポインタ変数を改変する事はできるだけ避け, オフセット値 i を動かし,p[i] の形で アクセスする というパターンを持っておきます.


関数の引数におけるポインタ変数の書き方

「引数で1つの値を返す場合」「配列を受ける場合」 それぞれで変数の型をどのように書くかのルールを決めておきます.

/* 引数の1つの値を返す場合 */
int function( double *ret_p )
{
/* 配列を受ける場合 */
int function( double array[] )
{

*」の使い方を知っているからといって, むやみに「*」を使うべきではありません.


「引き算」に気をつけよう

for文でのカウントダウンの場合と同様ですが, if文などでの引き算はバグの原因になります.例えば,

  if ( v <= n - 1 ) {

  if ( v < n ) {

と書けますし,足し算で書き直す事もできます. 足し算で書き直した場合,意味がわかりにくくなったら コメントで元の式を書いておきます.

ポインタ変数や配列の [] 演算子の中に引き算を使う場合も 要注意で,次のような条件文もパターンとして覚えておきます.

    if ( 0 < len && str[len - 1] == '\n' ) str[len - 1] = '\0';

「引き算は常に気をつけるべき」と,頭に入れておく事が大切です.

○実習1○

次のコードを実行すると,「hello 1」が10回,表示される事を確認してください.

#include <stdio.h>

int main()
{
    const size_t n = 0;
    size_t i;

    for ( i=0 ; i < 10 ; i++ ) {
        if ( i <= n - 1 ) {
            printf("hello 1\n");
        }
    }

    for ( i=0 ; i < 10 ; i++ ) {
        if ( i + 1 <= n ) {
            printf("hello 2\n");
        }
    }

    return 0;
}

コード中の 2 つの if 文の式は数学的には等価ですが, 結果が違ってきます.このように,unsigned の場合は 0 付近で 常に罠が潜んでいるのです.


型とポインタ変数

プリミティブ型をもっと知ろう

次にデータ解析で最低限知っておかなければならないプリミティブ型を表にまとめました.

符号有り整数

意味ヘッダprintf()のフォーマット
char1バイト符号有り整数-"%hhd"
short一般的に2バイト符号有り整数-"%hd"
int一般的に4バイト以上の符号有り整数-"%d"
long一般的に4バイト以上の符号有り整数-"%ld"
long long一般的に8バイト符号有り整数-"%lld"
ssize_tアドレスの大きさに一致した符号有り整数sys/types.h"%zd"
int8_t1バイト符号有り整数stdint.hintへキャスト
int16_t2バイト符号有り整数stdint.hintへキャスト
int32_t4バイト符号有り整数stdint.hlongへキャスト
int64_t8バイト符号有り整数stdint.hlong longへキャスト

符号無し整数

意味ヘッダprintf()のフォーマット
unsigned char1バイト符号無し整数-"%hhu"
unsigned short一般的に2バイト符号無し整数-"%hu"
unsigned int一般的に4バイト以上の符号無し整数-"%u"
unsigned long一般的に4バイト以上の符号無し整数-"%lu"
unsigned long long一般的に8バイト符号無し整数-"%llu"
size_tアドレスの大きさに一致した符号無し整数stddef.h"%zu"
uint8_t1バイト符号無し整数stdint.hunsigned intへキャスト
uint16_t2バイト符号無し整数stdint.hunsigned intへキャスト
uint32_t4バイト符号無し整数stdint.hunsigned longへキャスト
uint64_t8バイト符号無し整数stdint.hunsigned long longへキャスト

浮動小数点数 (実数)

意味ヘッダprintf()のフォーマット
float4バイト浮動小数点数-"%g" など
double8バイト浮動小数点数-"%.15g" など

これらのうち,重要なポイントについて以下で解説しておきます.

int

1文字を表現する場合(例えば 'a')は int を使います. charunsigned char を使った場合, 標準C関数を使った場合に, EOF (-1と定義される) なのか,ある文字が 0x0ff なのかの 区別がつかない事があるためです.
一般に 32-bit OS でも 64-bit OS でも int は 32-bit 整数ですが, 厳密なビット幅が必要な場合には使ってはいけません.

long,long long

long は一般に 32-bit OS では 32-bit,64-bit OS では 64-bit 整数 です. long long は一般に 64-bit 整数です. データ解析で利用する一般的な整数としては,long を使うと良いでしょう. ただし,厳密なビット幅が必要な場合には使ってはいけません.

size_t

32-bit OS なら 32-bit 符号無し整数であり, 64-bit OS なら 64-bit 符号無し整数です. ファイルI/Oや文字列などの,バッファ操作やポインタ変数の演算を行なう場合に使用します. 符号無しである事に注意してください.
データ解析の場面でも,文字列や画像のサイズ等で利用します.

ssize_t

32-bit OS なら 32-bit 符号有り整数であり, 64-bit OS なら 64-bit 符号有り整数です. 負の値が使えるため,バッファ操作などのためのオフセット値や 関数の返り値として使う事があります.

uint16_t, uint32_t 等

バイナリファイルを扱うなど, 厳密なビット幅を持つ型が必要な場合に使用します.

float, double

浮動小数点の計算を単精度で行なうメリットは無いので, 数学計算用に変数を用意する時にはdouble型を使うようにします. 配列の場合は,要素数が多く単精度で十分な場合には, floatの配列を使うとメモリアクセスを減らす事ができ, パフォーマンスの改善が期待できます.


ポインタ変数の理解の大前提は「型宣言の理解」

C言語の最も難しいと言われるのは, ポインタ変数の取り扱いだと言われています. その「難しい」理由は次の2つで, 特に1番目については書籍等で十分に解説がなされていない事が多く, 初心者にとっての「最大の難関」になっています.

以下では,上記のポイントを踏まえ, ポインタ変数についての基本をわかりやすく解説していきます.

C言語の変数宣言表記の奥深さ

例えば,次のような変数または配列の宣言をみたとき, どこが「型」で,どこが「変数(定数)」なのかわかりますか?

  float *p0;
  long array[256];
  const double *const *p1;
  double (*func)(int, double);

答えは次のとおり:

変数 or 定数
float *p0
long []array
const double *const *p1
double (*)(int, double)func

このように,C言語で「型」を理解するための第一歩は, 次の規則を頭に入れておく事です.

上記の表の型の意味はともかく,まずはこの規則が頭に入っていないと, 普通の感覚では先に進むのが難しくなってきます. 特に,スペースや括弧にだまされてはいけません.


ポインタ変数の基本---「変数のアドレス」「ポインタ変数」

ポインタ変数を学習するにあたって,まずは 「ポインタ」という抽象的な言葉を使う事をやめる事 をお勧めします.「変数のアドレス」「ポインタ変数」のように, 具体的な言葉を使いましょう. 以下では,その具体的な言葉を使ってポインタ変数の基本について 説明していきます.

ポインタ変数の基本 まとめ

○実習2○

/* ★ */」のついたコードについて, 動作確認を行なってください.


「void *」型

void * 型は,ポインタ変数の型の1つですが, ポインタ変数の指す先の型は何でも良いという,特別な型です. ですので,void * 型のポインタ変数は, どんな型の変数のアドレスでも代入できます. 例えば,

    double a = 1.23;
    void *p;
    p = &a;

のようなコードも問題ありません.しかし, 上記のポインタ変数 p の指す先へのアクセスは(型が不明なので) できません. では何のためにあるのか? というと,後で解説する ヒープメモリを扱う時に使います. ここではとにかく,void * 型という,指す先が何でもOKという ポインタ変数があるんだ,という事を頭にいれておけば十分です.


変数のアドレスとポインタ変数のキャスト

ポインタ変数のキャストは,型を理解していれば何も難しい事はありません. すでに出てきましたが,実際のアドレスを表示させる場合には,

    double v;
    printf("address = %zu\n",(size_t)&v);

のようにしました.このほか,void * とのキャストはよく 使います.

static int func( void *p )
{
    double *ptr;
    ptr = (double *)p;

これと逆の場合では,キャスト演算子は無くてもかまいません.

static int func( double *p )
{
    void *ptr = p;

ポインタ配列

「第3の難関」は,この「ポインタ配列」です. このあたりの話になると, 「型」をしっかりおさえておかないと,全くついてこれなくなります.

ポインタ配列とは, アドレスを格納するための配列の事で, 1次元バッファを2次元配列として扱いたい場合などに使います.

以下に,1次元配列を2次元として使う例を示します.

#include <stdio.h>

int main()
{
    double array[] = {10,11,12,13, 20,21,22,23};  /* この配列を 4x2 として使いたい */
    double *p[2];                                 /* これがポインタ配列 */
    int i;

    // それぞれの行の先頭要素のアドレスを登録
    p[0] = &array[0];
    p[1] = &array[4];

    // 以下,2次元配列としてアクセス
    for ( i=0 ; i < 2 ; i++ ) {                   /* 縦方向のループ */
        int j;
        for ( j=0 ; j < 4 ; j++ ) {               /* 横方向のループ */
            printf("[%g]",p[i][j]);
        }
        printf("\n");
    }

    return 0;
}

double型やint型の配列を「double array[256];」 「int array[256];」のように宣言した事を思い出してください. それと同様に,ポインタ配列の場合も「double *」型の配列 と考えれば,ポインタ配列の宣言は「double *p[256];」 と書き下す事ができます. 配列の要素に対するアクセスも,これまでの配列と同様に p[i] のように書けばOKです.ただしこの場合,p[i] はアドレス値(つまりポインタ変数)なので, その指す先ヘアクセスするにはさらに [j] をくっつけて,p[i][j] と書きます. とても論理的ですね.

次の図は,配列 array のアドレスが 3221223216, 配列 p のアドレスが 3221223208 の場合の メモリの状態を示したものです.

実際のコーディングでは,このような図が頭の中で描けるようになれば, ポインタ配列がマスターできたと考えて良いでしょう.

以下に,普通の配列とポインタ配列について 表にまとめてみました.

配列の宣言 配列名
(先頭アドレスを保持する定数)
配列要素 配列要素の型 指す先へのアクセス 指す先の型
double p[256]; double [] p p[i] double
double *p[256]; double *[] p p[i] double * p[i][j] double

ここで p[i] の型と p[i][j] の型とに注目してください. 演算子 […] が1個つくと,型の「*」が1つ減る 事にお気づきでしょうか? これは,ポインタ変数についても同じような規則があります.つまり, 演算子 *(…) が1個つくと,型の「*」が1つ減る のです.次の表にポインタ変数の場合もまとめてみました.

宣言 1つめの演算 1つめの演算後の型
(指す先の型)
2つめの演算 2つめの演算後の型
(指す先の型)
double p[256]; p[i] double
double *p; *(p) double
double *p[256]; p[i] double * p[i][j] double
double **p; *(p) double * *(*(p)) double

これが,C言語における,ポインタ変数に対する演算子と型との, とってもキレイな関係なのです.

○実習3○

  1. 上記のコードを入力し,動作確認を行なってください.
  2. 次のコードを return 文の前に挿入し,プログラムを動作させ, メモリの様子を上の図に従って,紙に描いてください.
        printf("address of array = %zu\n", (size_t)array);
        printf("address of p = %zu\n", (size_t)p);
        printf("p[0] = %zu\n", (size_t)(p[0]));
        printf("p[1] = %zu\n", (size_t)(p[1]));
    

関数の引数におけるポインタ変数

ここまでのポインタ変数の話は実践的な内容が含まれず, ポインタ変数を使う意味があまりわからなかったと思います. ここからは,関数の引数におけるポインタ変数の使い方をとりあげます. ようやくポインタ変数の存在理由がわかるようになるはずです.

関数の引数で,呼び出し元に対して値を返す時

C言語の関数では, 引数は関数内で宣言するローカル変数と同じように扱われます. すなわち,関数の呼び出し時に「引数に値を与える」とは「値のコピー」 にほかなりません.したがって,関数内で引数の値を変更しても, 呼び出し元へは何の影響も与えません. また,C言語の関数では返り値は1つ,と決まっていますので, これでは複数の値を呼び出し元に返せないように思われるかもしれません.

ところが,ポインタ変数を使うと,引数経由で呼び出し元の変数やバッファを 変更する事ができます.例を示します.

static int my_function( double v, double *ret_val )
{
    if ( 0.0 <= v ) {
        *(ret_val) = v * 60.0;
        return 0.0;
    }
    else {
        /* this means error */
        return -1;
    }
}

int main()
{
    double val, val_new;
    :
    if ( my_function(val, &val_new) < 0.0 ) {
    :
}

これが,ポインタ変数の最も基本的な使い方です.

関数の引数で,呼び出し元から1次元配列を与える時

もう1つの基本的な使い方は,次の例のように1次元配列を引数に与える場合です. これは特に難しい事はないと思います.

static double get_sum( double arr[], int n_elements )
{
    int i;
    double ret = 0.0;
    for ( i=0 ; i < n_elements ; i++ ) {
        ret += arr[i];
    }
    return sum;
}

int main()
{
    double array[256];
    :
    sum = get_sum(array,256);
    :
}

なお,引数での配列の宣言は特別で,上記の場合,「arr」は 定数ではありません. というのは,引数での「double arr[]」は 「double *arr」と等価だからです (型はどちらも「double *」です). だからといって,引数 「double *arr」 を配列を受けるために使うのは好ましくありません. 配列なら配列の書き方で書くべきです.

関数の引数で,呼び出し元から2次元配列を与える時

C言語で関数に2次元配列を与えるには, ポインタ配列を利用します. 具体的には,呼び出し元で通常の1次元配列とポインタ配列とを作っておき, 関数にポインタ配列の先頭アドレスを与えるようにします.

◎課題1◎

  1. 次のコードは,main()関数内の 1次元配列 double array[] を 4 × 2 の2次元配列として扱い, 関数 display_2d_array() にて配列の要素を表示するものです. ここまでの資料を参考に,下記のコードの「???」の部分を 適切に埋めて,完成させてください.
    #include <stdio.h>
    
    static void display_2d_array( ???, int n_x, int n_y )
    {
        int i;
        // 2次元配列としてアクセス
        for ( i=0 ; i < n_y ; i++ ) {                   /* 縦方向のループ */
            int j;
            for ( j=0 ; j < n_x ; j++ ) {               /* 横方向のループ */
                printf("[%g]",arr_2d[i][j]);
            }
            printf("\n");
        }
        return;
    }
    
    int main()
    {
        double array[] = {10.0, 11.1, 12.2, 13.3,
                          20.0, 21.1, 22.2, 23.3};  /* この配列を 4x2 として使う */
        double *p[2];                               /* ポインタ配列 */
        ???
        ???
        display_2d_array(p,4,2);
    
        return 0;
    }
    
    なお,関数の引数に1次元配列を与える時と同様, 引数「double *arr[]」は引数「double **arr」と等価です. もちろん,ポインタ配列を受ける場合は前者を使うべきです.

関数の引数でのポインタ変数のまとめ

次の表にまとめてみました.表には比較のため 「double **p」を載せていますが, これまでこの形は詳しく解説していません. 「*」が2つのポインタ変数は後で解説しますが, 関数の引数としてはできるだけ使わない方が良いものです.

引数の宣言 変数 用途
double *p double * p double型の1つの値を *(p) へ返す
double p[] double * p 1次元配列をうける
double **p double ** p double *型の1つの値を *(p) へ返す
double *p[] double ** p 2次元配列をうける

関数に対するポインタ変数

少し前でも触れましたが,変数と同様に関数にもアドレスがあります. したがって,C言語では関数に対するポインタ変数というものがあり, if文を使わずに実行する関数を選択したり, 関数の引数に呼び出されるべき関数を与えたりする事ができます. 基本形は次のような形をとります.

static int foo( double a );

int main()
{
    int (*func_ptr)( double );    /* 関数に対するポインタ変数 */

    func_ptr = &foo;              /* func_ptr に foo() のアドレスを代入 */

復習ですが,「型」と「変数」はそれぞれどこでしょう? 答えは「int (*)(double)」が型で,「func_ptr」 が変数です.

もちろん,ワンパターンに持ち込むという考え方に基づくと, 関数の選択などの場合は単純な if 文による分岐を使うべきなのですが, 場合によってはどうしても関数に対するポインタ変数が必要になる場合が あります.

この「どうしても必要な場面」というのは, 関数の引数に呼び出されるべき関数を与えたい時です. これは,やらせたい事を具体的に関数に与えたい時,とも言えます. この場合にもやはり定石と言われる形があります.例を示します.

/* 
 * 配列の,ある区間の足し算を行なう
 */
#include <stdio.h>

/* 計算範囲を指定するための構造体 */
struct range {
    size_t begin;
    size_t length;
};

/* 配列の足し算を行なう関数 */
static double sum( const double a[], size_t n, void *user_ptr )
{
    const struct range *rng = (const struct range *)user_ptr;
    double ret = 0.0;
    size_t i;

    /* 指定された範囲を計算 */
    for ( i=0 ; i < rng->length ; i++ ) ret += a[rng->begin + i];

    return ret;
}

/* 配列に対する何らかの計算をさせて結果を表示する関数 */
static void display_result( const double val[], size_t n_val,
                        double (*func_calc)( const double [], size_t, void * ),
                        void *user_ptr )
{
    double result;

    /* 指定された計算を実行 */
    result = (*func_calc)( val, n_val, user_ptr );

    /* 表示 */
    printf("result = %g\n", result);

    return;
}

int main()
{
    double elem[] = {0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9};
    const size_t n_elem = sizeof(elem)/sizeof(double);
    struct range rng_calc;

    /* 計算する範囲を指定 */
    rng_calc.begin = 2;
    rng_calc.length = 3;

    /* 計算して表示 */
    display_result(elem, n_elem, &sum, (void *)&rng_calc);

    return 0;
}

ポイントは,関数へのポインタ変数の引数の後に, 「void *」型の引数をつけておく事です. これによって,main()関数から, 構造体を使う事であらゆる情報を最終的な目的の関数(上記の例では sum()) へグローバル変数を使う事なく伝える事ができます.


「const」を使おう

変数や配列の宣言時に「const」修飾子を使うと, 書き込みを禁止する事ができます. そんなものは無くても困らない,と思われるかもしれませんが, 「const」には,

などの御利益があり, 基礎的なところの安全性を高めるために有効ですから, 積極的に使っていく事をお勧めします.


マクロより「const」を

#define から始まるマクロは, 定数を定義したり,簡単な数学関数を定義したりするのに使えます. 例えば,

#define WIDTH  1024
#define HEIGHT  768
#define max(v0,v1) (((v0) < (v1)) ? (v1) : (v0)))

のようなコードはよく見かけると思います. しかし,マクロはパフォーマンスという点で良い事もあるのですが, バグを生みやすいという欠点があります. 例えば,次のような構造体が存在すると,上記のマクロにより意図しない部分が 置き換わってしまいます.

struct REGION {
  long WIDTH;
  long HEIGHT;
};

また,上記の max() の定義ように,演算で引数 v0, v1 を括弧で 囲む事を忘れると,これまた意図しない結果になってしまいます. したがって,安全性という観点からは,特別な理由が無い限りマクロを使わず, 定数やインライン関数を使う事をお勧めします.上記の例だと, 次のように書き直せます.

static const long WIDTH = 1024;
static const long HEIGHT = 768;

inline static double max( double v0, double v1 )
{
    if ( v0 < v1 ) return v1;
    else return v0;
}

インライン関数とは,コンパイル時に 可能な限り関数の実体を作らずに呼び出し元に関数のコードを埋め込むよう, 指示された関数の事です. ただし,「inline」が指定されていても, コンパイル時に常にインライン化されるとは限りません. 関数の中身が複雑な場合は,通常の関数と同じように扱われる事もあります.

○実習4○

次のコードを「-O」オプション付きでコンパイルし, シンボル「min」は存在し, シンボル「max」が存在しない事を nm コマンドで確認してください. 「nm 実行ファイル名 | grep min 」などとすると良いでしょう.

#include <stdio.h>
#include <stdlib.h>

inline static double max( double v0, double v1 )
{
    if ( v0 < v1 ) return v1;
    else return v0;
}

static double min( double v0, double v1 )
{
    if ( v0 < v1 ) return v0;
    else return v1;
}

int main( int argc, char *argv[] )
{
    if ( 2 < argc ) {
        double a = atof(argv[1]);
        double b = atof(argv[2]);
        printf("max: %g\n", max(a,b));
        printf("min: %g\n", min(a,b));
    }

    return 0;
}

「char *str = "abc";」のどこがマズい?

"abc" のようにダブルクォートで囲まれた文字列を, 文字列リテラルといい,C言語ではこれは定数の一種です. 例えば,

  char *str = "abc";

と書くと,これは変数 str に文字列 "abc" の 先頭アドレスが代入される事を意味しますが,この後に

  str[0] = 'A';

と書くと,Segmentation Fault のエラーで強制終了させられます. こういう事は,変数 str の宣言で const を つけておけば絶対に起こりません:

  const char *str = "abc";

文字列リテラルを使う時は,constを必ずつけましょう.

なお,ダブルクォートで囲まれた文字列でも,次のように 配列へコピーしている場合もあります:

  char str[] = "abc";

この場合は str は通常の配列ですので,書き換えは問題ありません. 上記の2つのコードを,安全なパターンとして頭に入れておきましょう.


「const double *const p[]」

ポインタ変数の宣言の場合には, プリミティブ型の前,または「*」の後に 「const」を書く事ができます. この見慣れない書き方は,どういう意味なのでしょうか. 関数の引数の宣言の場合について,以下の表にまとめてみました.

引数の宣言 p++; p[i] = a; p[i][j] = b;
double p[]
const double p[] ×
double *p[]
const double *p[] ×
double *const p[] ×
const double *const p[] × ×

上の表から明らかと思いますが, どこに「const」を書くかによって, 配列の何を書き換えても良いかが決まります. この記法を関数の引数で使えば,関数の仕様が明確になります.

例えば,これまでに出てきた関数 get_sum()display_2d_array() は,配列に対して書き込む事はありませんでしたから, 次のように書くのがより安全と言えます.

static double get_sum( const double arr[], int n_elements )
static void display_2d_array( const double *const arr_2d[], int n_x, int n_y )

なお,これらの「const」がついた型のチェックは, g++ は完全ですが gcc はまだ完全ではないようです.


メモリの扱い

メモリの扱いも C言語の難しいところです. 特に,パフォーマンスを考えて動的にメモリを確保したり, メモリリークを起こさないようにするにはどのようにすれば良いか? といった課題を攻略するのは,そう簡単な事ではありません. ここでは,私の長年の経験から得たメモリの安全な扱い方のノウハウについて 解説します.


スタックとヒープ

C言語のように,ソースコードをコンパイルして実行ファイルを作る プログラミング言語は,プログラムで使用するメモリ領域が 「スタック」と「ヒープ」とに分かれており, それぞれ大きさと役割が異なります. プログラムから見えるメモリ空間は,以下の図のようになっています.

スタックは,関数内の変数や配列が格納される領域で, 変数や配列に対するメモリの割り当てがプログラム開始時に ほぼ決まっているため, メモリの確保と開放は完全自動(関数に入ると自動確保, 関数を抜けると自動開放)で実行コストがかりません.しかし, 使えるメモリサイズはかなり小さく制限されています.ですので, 例えば次のような巨大な配列を宣言しても,プログラムは正常に動きません.

  double array[1000000000];   /* これは不可 */

スタック上に作れる配列の大きさは,せいぜいCPUの1次キャッシュ程度の大きさ (例えば64kByte)までにしておきます. ただし,関数の呼び出しの段数が深い場合,特に再帰呼び出しを行なう場合には これよりももっと小さい配列しか作れません.

ヒープは,スタックとは全く逆の性質を持つメモリ領域で, メモリの確保と開放は手動(C++ではここを自動化可能)であり,これには 若干の実行コストがかかります. 確保できるサイズは搭載メモリ容量に近いところまで設定できます.

ヒープメモリを確保する例を示します.

{
    int ret_status = -1;
    /* ヒープメモリの先頭アドレスを記録するポインタ変数 */
    double *arr_ptr = NULL;
    size_t i;

    /* ヒープメモリの確保 */
    arr_ptr = (double *)malloc(sizeof(*(arr_ptr)) * N_ELEMENTS);
    if ( arr_ptr == NULL ) {
        fprintf(stderr,"[ERROR] %s: malloc() failed\n",__FUNCTION__);
        goto quit;
    }

    for ( i=0 ; i < N_ELEMENTS ; i++ ) {
        /* 何らかの処理 */
        arr_ptr[i] = ...
    }

    ret_status = 0;
 quit:
    /* ヒープメモリの開放 */
    if ( arr_ptr != NULL ) free(arr_ptr);
    return ret_status;
}

上記の例のように,ヒープを使う場合は, 1. ポインタ変数を用意,2. malloc() 関数で確保 & チェック, 3. free() 関数で開放, の3点セットが必ず必要です.


malloc() 関数で 1次元のバッファを確保

malloc()関数を使って,ヒープからバッファを確保するコードを, スタックからとる場合とコードを比較しながら, もう少し詳しくみていきましょう.


malloc() 関数で 2次元のバッファを確保

今度は2次元のバッファをヒープから確保する場合をみていきます. スタックからとる場合とコードを比較しながら解説します.


安全にヒープを使うコツ

C言語の最も難しいところの1つは,このヒープの使い方にあると言われています. 最も深刻なのは,メモリリークのバグを作ってしまう問題です. メモリリークの問題とは,確保したヒープメモリの開放処理を忘れてしまい, プログラムが動く事でヒープメモリの確保ばかりが行なわれて 全メモリを喰い尽くしてしまい, プログラムが強制終了させられてしまう問題の事です.

小規模なコードではメモリリークがあってもコードを見直せば なんとか修正できるものですが,大規模なコード(数万行)になると 修正はかなり困難になってきます.

以下に,メモリリークを起こす典型的なコードを示します:

{
    /* ヒープメモリの先頭アドレスを記録するポインタ変数 */
    double *arr_ptr;

    /* ヒープメモリの確保 */
    arr_ptr = (double *)malloc(sizeof(*(arr_ptr)) * N_ELEMENTS);
    if ( arr_ptr == NULL ) {
        fprintf(stderr,"[ERROR] %s: malloc() failed\n",__FUNCTION__);
        return -1;
    }

      :
      :

    if ( ... ) return -1;                  /* ここでメモリリークを起こす */


    /* ヒープメモリの開放 */
    free(arr_ptr);
    return 0;
}

では,ヒープメモリを安全に使うにはどうしたら良いのでしょう? ここでもやっぱりワンパターンへ持ち込む事を考えます. 具体的には,1つ前のコードのように,次のようなパターンを死守します.

具体的には次のようなパターンになります.

{
    int ret_status = -1;
    double *arr_ptr0 = NULL;       /* ヒープのアドレスを記録するポインタ変数 */
    double *arr_ptr1 = NULL;       /* 必ず NULL で初期化する */

      :
    必要なところで,malloc() を実行
    途中で return したくなったら,「goto quit;」
      :

    ret_status = 0;
 quit:
    if ( arr_ptr0 != NULL ) free(arr_ptr0);            /* ヒープメモリの開放 */
    if ( arr_ptr1 != NULL ) free(arr_ptr1);
    return ret_status;
}

このパターンを守っていれば,コードの量が増えても, エディタの検索機能を使えば メモリの確保 & 開放のチェックを行なうのもそれほど難しくないと思います.

goto」を使う事に対して抵抗がある方もいらっしゃるかと思いますが, このパターンでは return の代わりとして 使っているだけなので,何も悪い事はありません. よく「goto」がダメだと言われるのは, 使い方が難しく濫用すると酷いコードになるからですが, このように限定して使うとむしろ御利益になる事もあるのです. Linuxカーネルのコードにも,かなり goto が使われています.興味のある方は コードを grep してみてください.

なお,このパターンは,ファイルのオープン & クローズにも使えます.

{
    int ret_status = -1;
    FILE *fp = NULL;

      :
    必要なところで,fopen() を実行
    途中で return したくなったら,「goto quit;」
      :

    ret_status = 0;
 quit:
    if ( fp != NULL ) fclose(fp);
    return ret_status;
}

このようにしておけば,ファイルのクローズのし忘れを防ぐ事ができます.


realloc() 関数の使い方の基本

realloc() 関数は, malloc() 関数で確保された領域が足りない時や 領域が大きすぎた時に,領域のサイズを変更するために 利用します(厳密には,malloc() や free() の代わりとして使う事もできます). realloc() 関数も C 言語の難しいところで, 誤解が多く,間違ったコードをよく見かけます.

よくある間違いの例を示します:

    arr_ptr = (double *)realloc(arr_ptr, sizeof(*(arr_ptr)) * new_length);
    if ( arr_ptr == NULL ) {
        エラー処理
    }

上記のコードで arr_ptr はあらかじめ malloc() で確保された領域を指していて,その領域をリサイズしようとしていますが, もし realloc()NULL を返した場合, メモリリークが起こります.また,new_length がゼロ だった場合の事を考慮していません. 正しいコードは以下のとおりです.これもパターンとして覚えてしまいます.

    void *tmp_ptr;
    tmp_ptr = realloc(arr_ptr, sizeof(*(arr_ptr)) * new_length);
    if ( 0 < new_length && tmp_ptr == NULL ) {
        エラー処理
    }
    else {
        arr_ptr = (double *)tmp_ptr;
    }

なお,realloc()関数は, 第2引数にゼロが指定された場合(上記の場合はnew_lengthがゼロ), free()関数が呼ばれたのと同じ事であり,返り値は NULL になります.この点にも注意して以降のコードを作ります. なお,第1引数に NULL が与えられた時は malloc()関数と等価です.


パフォーマンスを考えた realloc() 関数の使い方

realloc()関数でより大きい領域を再確保した場合, かなりの確率で OS によるメモリのコピーが発生します. したがって,少しずつ領域を大きくとるように realloc()関数を何回も呼び出すと, その実行コストはかなり大きなものになり得ます. 特に,1つのプログラムで管理しているヒープ領域の個数が増えた場合, OSによるメモリのコピー処理が多発するため, 本当は数秒で終わる処理が,何十秒もかかるといった事にもなります.

そのような問題を回避するためには, 例えばメモリ領域を n の m 乗のサイズでとる,といった方法をとります. コードの例を示します.

static void *realloc_pow( void *ptr, size_t size, double base )
{
    void *ret_ptr = NULL;

    if ( 0 < size ) {

        size_t nn = (size_t)ceil( log((double)size) / log(base) );
        size_t len = (size_t)pow(base, (double)nn);

        /* 念のためチェック */
        if ( len < size ) {
            len = (size_t)pow(base, (double)(nn + 1));
            if ( len < size ) {
                /* error */
                goto quit;
            }
        }

        /* */
        //fprintf(stderr,"len = %zu\n",len);
        ret_ptr = realloc(ptr, len);
    }
    else {
        ret_ptr = realloc(ptr, size);
    }

 quit:
    return ret_ptr;
}

上記のコードは,必ず base の n 乗の値を第2引数として realloc()関数を呼び出してヒープメモリの再確保を行なうための 関数です. 実は,現在確保しているヒープメモリの大きさを自前で管理すれば, もっと高速化できますが,それは宿題という事にしておきます.

○実習5○

次のコードは,N_REGION個のヒープ領域を確保し, それぞれの領域のサイズを徐々に変化させていくコードです. まず,このコードをコンパイルし,実行に何秒かかるかを確認してください. その後,realloc() のかわりに realloc_pow() を使うように改造し,同様に何秒かかるかを 確認してください.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

static const size_t N_REGION = 1024;

int main()
{
    int ret_status = -1;
    double *arr_ptr[N_REGION]; /* ポインタ配列: 各ヒープの先頭アドレスを記録 */
    size_t i;

    /* arr_ptr[i] の初期化 (すべて NULL にする) */
    memset(arr_ptr, 0, sizeof(*(arr_ptr)) * N_REGION);

    /* 1個から10000個までヒープから得る領域の大きさを変化させる */
    for ( i=0 ; i < 10000 ; i++ ) {
        const size_t n = i+1;
        size_t j;

        /* N_REGION個のヒープ領域を再確保する */
        for ( j=0 ; j < N_REGION ; j++ ) {
            void *tmp_ptr;
            tmp_ptr = realloc(arr_ptr[j], sizeof(*(arr_ptr[j])) * n);
            //tmp_ptr = realloc_pow(arr_ptr[j], sizeof(*(arr_ptr[j])) * n, 2.0);
            if ( 0 < n && tmp_ptr == NULL ) {
                fprintf(stderr,"[ERROR] realloc() failed\n");
                goto quit;
            }
            arr_ptr[j] = (double *)tmp_ptr;

            arr_ptr[j][i] = i;
        }

    }

    ret_status = 0;
 quit:
    for ( i=0 ; i < N_REGION ; i++ ) {
        if ( arr_ptr[i] != NULL ) free(arr_ptr[i]);
    }

    return ret_status;
}

本当はおかしいLIBCの関数---安全な文字列処理のためのヒント

LIBC(C標準ライブラリ)には,一応一通りの文字列処理用の関数があります. しかし,LIBCの関数が制定された当時は今ほど安全性が求められていなかったので, 今となってはおかしな仕様になってしまっているものも多く存在します. 以下に,例をあげます.

このように,LIBCの関数には様々な問題があります. 特にバッファオーバーフローを起こすと, プログラムは予想不可能な挙動をとる事があり, デバッグを難しくする原因になります. できるだけ安全にいきたいと考えるならば, そのまま使える関数は限られてきます.この状況を踏まえて, ここではより安全な文字列の処理について考えていきます.


「バッファの大きさ」か「文字列の長さ」か?

C言語での開発の場合,文字列を扱う関数は, char []型のバッファを引数にとる事が多いものです. その時,引数をバッファの大きさにするか,文字列の長さにするかを 迷うところですが,ここは迷わずバッファの大きさにします

この理由は,もし関数の引数の仕様が「文字列の長さ」だと, 誤って「バッファの大きさ」を与えると '\0' へのアクセス によりバッファオーバーフローを 起こすからです(逆の場合には,バッファオーバーフローは起こりません).


文字列の編集は snprintf() が一番安全

文字列のバッファに対してフォーマット出力を行なうための関数 snprintf() は,数値から文字列への変換だけでなく, strcpy() のかわり(文字列のコピー)や strcat() のかわり(文字列の連結) にも使う事ができます. 第2引数の値が 0 ではない限り,文字列は必ず '\0' で終端し, 第2引数の値が誤っていない限りバッファオーバーフローを起こしません. 例えば,

    strcpy(dest, src0);
    strcat(dest, src1);

は,次のように書く事で安全性を確保できます.

    snprintf(dest, size_dest, "%s%s", src0, src1);

しかし場合によっては後から文字列を追加したい事もあります. その場合は,次のような安全な関数を作って対応します.

#include <string.h>
/**
 * @brief  安全な strcat() 関数
 * @param  dest 結果文字列
 * @param  size_dest 結果文字列バッファの大きさ
 * @param  src 源泉文字列
 * @return  dest の値
 */
inline static char *safe_strcat( char *dest, size_t size_dest, const char *src )
{
    size_t len_dest = strlen(dest);
    if ( len_dest < size_dest ) strncat(dest,src,size_dest-1-len_dest);
    return dest;
}

速度が欲しい場合や文字列の部分コピーを行ないたい場合は, 次のような関数を用意して対応します.

#include <string.h>

/**
 * @brief  安全な strcpy() 関数
 * @param  dest コピー先文字列
 * @param  size_dest コピー先文字列バッファの大きさ
 * @param  src 源泉文字列
 * @return  dest の値
 */
inline static char *safe_strcpy( char *dest, size_t size_dest,
                                 const char *src )
{
    if ( 0 < size_dest ) {
        const size_t m = size_dest - 1;
        strncpy(dest,src,m);
        dest[m] = '\0';
    }
    return dest;
}

/**
 * @brief  安全な strncpy() 関数
 * @param  dest コピー先文字列
 * @param  size_dest コピー先文字列バッファの大きさ
 * @param  src 源泉文字列
 * @param  n コピーを行なう文字数
 * @return  dest の値
 */
inline static char *safe_strncpy( char *dest, size_t size_dest, 
                                  const char *src, size_t n )
{
    n ++;
    if ( n < size_dest ) size_dest = n;
    if ( 0 < size_dest ) {
        const size_t m = size_dest - 1;
        strncpy(dest,src,m);
        dest[m] = '\0';
    }
    return dest;
}

安全な文字や文字列の検索

strchr(), strstr() などは,const char *で受けておきながら, 返り値は char * になっていてイマイチという話をしましたが, これは要するにアドレスを直接扱うから問題になるわけです. アドレスではなく,オフセット値で扱えば,このような問題は起きません. 以下では,オフセット値で文字列の検索処理を行なう事について考えていきます.

文字セットの検索には strspn(),strcspn()

LIBCの文字列検索系の関数でも,strspn(), strcspn() は唯一(?),アドレスではなくオフセット(長さ)を返してくれます.しかもこれらは, 文字セット中にある文字を検索するための関数ですので,利用できる機会も多いです. 安全性という点で,snprintf() と並んでオススメできる関数です.

○実習6○

man strspn」と次のコードとを参考に, これらの関数の仕様を確認してください.

    const char *my_str = "abc def \txyz";
    size_t ix0 = strspn(my_str, " \t");
    size_t ix1 = strcspn(my_str, " \t");

文字,文字列の検索

strchr(), strstr() などはそのままではイマイチなので, 次のようにオフセット値を返すように改良して使うと良いでしょう.

#include <string.h>

/**
 * @brief  文字の検索
 * @param  s 処理対象の文字列
 * @param  c キー文字
 * @return  見つかった場合はオフセット値<br>
 *          見つからない場合は負値
 */
inline static ssize_t find_chr( const char *s, int c )
{
    const char *p = strchr(s, c);
    if ( p == NULL ) return -1;
    else return (ssize_t)(p - s);
}

/**
 * @brief  文字列の検索
 * @param  haystack 処理対象の文字列
 * @param  needle キー文字列
 * @return  見つかった場合はオフセット値<br>
 *          見つからない場合は負値
 */
inline static ssize_t find_str( const char *haystack, const char *needle )
{
    const char *p = strstr(haystack, needle);
    if ( p == NULL ) return -1;
    else return (ssize_t)(p - haystack);
}

sscanf() を安全に使うには

まず,ストリームから直接読む scanf()fscanf() は,行単位で読み込んでくれない事があるので,使うのはやめます. ストリームから読む場合は, まず fgets() でバッファに読み込んでから, sscanf() を使うようにします.

そして, sscanf() の最大の問題はフォーマットに "%s" を 指定した場合にバッファオーバーフローしてしまうという危険性でしたが, これは次のようにして対処できます.

    char el0[20], el1[20], el2[20];
    sscanf(str, "%19s%*[^ ] %19s%*[^ ] %19s%*[^ ]", el0, el1, el2);

"%*[^ ]" は空白以外の文字からなる文字列を読み飛ばすための指定です. これで,文字列中の各要素を確実に分割でき,かつ 各要素それぞれの最初の 19 文字だけを取り出す事ができます.


scanf(), sscanf() の代わりはどうするか?

sscanf() は何とか安全に使う方法がわかったわけですが, どうしても要求された仕様に合わない事があります. その場合は,自分でかわりのものを作って対応します. 一から作るのは面倒なので,strspn()関数と strcspn()関数とを利用します. ここでは文字列をデリミタで要素へ分割する場合を考えてみます.

まず,1つの文字列をスキャンし,デリミタで要素へ分割するための情報を 取得するための関数を示します. 意外と簡単に作れる事がおわかりいただけると思います.

#include <string.h>

/**
 * @brief  1つの文字列をスキャンし,デリミタで要素へ分割するための情報を取得
 * @param  str スキャン対象の文字列
 * @param  delim デリミタ(文字セット)
 * @param  begin 各要素文字列の先頭位置 (返り値)
 * @param  length 各要素文字列の長さ (返り値)
 * @param  max_n begin[], length[] のサイズ
 * @return  要素の個数
 */
static size_t sscan_str( const char *str, const char *delim,
                         size_t begin[], size_t length[], size_t max_n )
{
    size_t ix = 0;                  /* 文字列のパース位置 */
    size_t n_elem = 0;              /* 要素の個数 */
    size_t spn;

    /* 最初のデリミタを飛ばす */
    spn = strspn(str + ix, delim);
    ix += spn;

    while ( n_elem < max_n ) {

        /* 要素部分 */
        spn = strcspn(str + ix, delim);
        if ( spn == 0 ) break;

        /* 配列に登録 */
        begin[n_elem] = ix;
        length[n_elem] = spn;
        n_elem ++;
        ix += spn;

        /* デリミタを飛ばす */
        spn = strspn(str + ix, delim);
        if ( spn == 0 ) break;
        ix += spn;
    }

    return n_elem;
}

○実習7○

次に,上の関数の動作を確認するためのコードの例を示します. コード中の my_str0 を変更しながら, sscan_str()関数の動作確認を行なってください.

int main()
{
    const char *my_str0 = " abc def  \txyz123  ";
    const size_t max_elem = 32;
    size_t begin[max_elem];
    size_t length[max_elem];
    size_t i, n;

    /* スキャン */
    n = sscan_str( my_str0, " \t", begin, length, max_elem);

    /* 要素を表示 */
    for ( i=0 ; i < n ; i++ ) {
        char buf[256];
        safe_strncpy(buf,256, my_str0 + begin[i], length[i]);
        printf("len=%zu elem=[%s]\n", length[i], buf);
    }
    
    return 0;
}

◎課題2◎

  1. テキストファイルから必ず1行を読みとるための関数を作ってください. ファイルからの読み込みには fgets()関数を使用します. プロトタイプは次のとおりとし, バッファの長さ size_str を越えた場合の 読み飛ばしを行なうようにしてください.
    #include <stdio.h>
    
    /**
     * @brief  ファイルから必ず1行を読みとる.バッファサイズ以上の部分は捨てられる
     * @param  fp ファイルハンドラ
     * @param  str 1行分を保存するバッファ
     * @param  size_str str[]のバッファサイズ
     * @return  成功した場合は str を返す<br>
     *          EOFまたはエラーの場合 NULL
     */
    static char *fget_str( FILE *fp, char *str, size_t size_str );
    
  2. fget_str() を使ってテキストファイルから1行読みとり, デリミタで要素へ分割するための情報を取得するための 関数を作ってください.プロトタイプは次のとおりとしてください. 文字列バッファの最後の改行文字を消去してから,sscan_str() を呼ぶようにします.
    #include <stdio.h>
    #include <sys/types.h>
    
    /**
     * @brief  ファイルから1行を読みとり,デリミタで要素へ分割するための情報を取得
     * @param  fp ファイルハンドラ
     * @param  str 1行分を保存するバッファ
     * @param  size_str str[]のバッファサイズ
     * @param  delim デリミタ(文字セット)
     * @param  begin 各要素文字列の先頭位置 (返り値)
     * @param  length 各要素文字列の長さ (返り値)
     * @param  max_n begin[], length[] のサイズ
     * @return  成功した場合,要素の個数<br>
     *          EOFの場合,負値
     */
    static ssize_t fscan_str( FILE *fp, char str[], size_t size_str,
                              const char *delim,
                              size_t begin[], size_t length[], size_t max_n );
    
  3. このテキストファイル を fscan_str() でスキャンし,ra[deg], dec[deg] から 単位ベクトル cx, cy, cz を計算して結果を表示するコードを作成してください. 文字列から数値への変換は,atof()関数などを使用します.
    参考:
    cx = cos(ra) × cos(dec)
    cy = sin(ra) × cos(dec)
    cz = sin(dec)

危っかしい非数(NAN)と数学関数

非数(NAN)の罠

浮動小数点型(float または double)には, 無限大や非数(not a number)という特別な値があります. これらの値は math.h でそれぞれ INFINITYNAN として マクロで定義されています.

これらの特別な値のうち,非数(NAN)はさらに特別で, if文などで == で他の値と比較した時に,常に false を返すように 定義されています(どんな値とも一致しないという意味).ありがちな間違いは,

    if ( v == NAN ) { ... }

というコードで,これは v が非数であっても常にこの条件文は成立しません. このような場合は isnan()isfinite() を使って 値を判定します.

○実習8○

次のコードをコンパイルし,「check 1」が表示されない事を確認してください. 「man isnan」などとして, isnan()isinf()isfinite() について各自調べてください.

#include <stdio.h>
#include <math.h>

int main()
{
    double v = NAN;

    if ( v == NAN ) {
        printf("check 1\n");
    }
    if ( isnan(v) ) {
        printf("check 2\n");
    }

    return 0;
}

注意すべき数学関数

数学関数には,取り得る引数値の範囲が決まっており, 範囲外の値を与えると無限大(INFINITY)や非数(NAN)を返すものがあります. これもバグの原因になりやすいので,特定の範囲しか与える事ができない 数学関数についてまとめておきます. コードのレビュー時などにまとめてチェックすると良いでしょう.

関数引数の範囲範囲外の場合の返り値
sqrt(x)0 ≦ x NAN
asin(x)-1 ≦ x ≦ 1 NAN
acos(x)-1 ≦ x ≦ 1 NAN
acosh(x)1 ≦ x NAN
atanh(x)-1 < x < 1 引数の絶対値が1なら -INFINITY or INFINITY,1を越えたら NAN
log(x)0 < x 引数が0なら -INFINITY,負なら NAN
log10(x)0 < x 引数が0なら -INFINITY,負なら NAN
log1p(x)-1 < x 引数が-1なら -INFINITY,-1より小さい場合 NAN

次のように,数学関数をwrapして使うのも1つの方法です.

/**
 * @brief  安全な acos() 関数
 * @param  x -1〜1の値 (計算誤差分は許容する)
 * @return  計算結果 (NANを返す事はない)
 * @note   計算誤差を越える範囲外の引数値については,stderr に警告を出力します
 */
inline static double safe_acos( double x )
{
    const double d = 0.0000001;    /* 許容する誤差 */
    if ( x < -1.0 ) {
        if ( x < -1.0 - d ) fprintf(stderr,"[WARNING] %s: arg is %.15g\n",
                                    __FUNCTION__, x);
        x = -1.0;
    }
    else if ( 1.0 < x ) {
        if ( 1.0 + d < x ) fprintf(stderr,"[WARNING] %s: arg is %.15g\n",
                                   __FUNCTION__, x);
        x = 1.0;
    }
    return acos(x);
}

プロトタイプ宣言の本当の目的と正しいソースファイル分割

プロトタイプ宣言

C言語では,呼び出される関数の引数と返り値の定義は, 呼び出す所よりも前に書かなければなりません. これまでみてきたコードは,関数の内容すべてを,呼び出す所より前に書いてき ましたが,次のようにプロトタイプ宣言をすれば, 関数の内容を呼び出す所よりも後に書く事もできます.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/types.h>

/* プロトタイプ宣言 */
static size_t sscan_str( const char *str, const char *delim,
                         size_t begin[], size_t length[], size_t max_n );

static char *fget_str( FILE *fp, char *str, size_t size_str );

static ssize_t fscan_str( FILE *fp, char str[], size_t size_str,
                          const char *delim,
                          size_t begin[], size_t length[], size_t max_n );

inline static char *safe_strncpy( char *dest, size_t size_dest, 
                                  const char *src, size_t n );

/* main 関数 */
int main( int argc, char *argv[] )
{
    int ret_status = -1;

    略

    return ret_status;
}

/* 関数のコード */
static size_t sscan_str( const char *str, const char *delim,
                         size_t begin[], size_t length[], size_t max_n )
{
    size_t ix = 0;                  /* 文字列のパース位置 */
    size_t n_elem = 0;              /* 要素の個数 */

このように,関数の引数と返り値の定義だけを書いた部分を, プロトタイプ宣言といいますが, 一体,こんな事をして何の御利益があるのか? そもそも関数の定義に前後関係があるなんて,C言語って何考えてるの? と思われるかもしれません. しかし,プロトタイプは,ソースを分割したりライブラリを作ったりする場合に, 非常に重要な役割がある事を,この後を読み進めるとわかるようになります.


正しいソースファイルの分割

コードが増えてきた場合,ソースファイルを分割した方が見通しが良くなる事が あります. ソースファイルの分割も,やはり安全なパターンがあり, 次のように書くのが一般的です.

まずはヘッダファイル foo.h です.

#ifndef _FOO_H             /* 二重にincludeされるのを防ぐ */
#define _FOO_H 1

#include <stdio.h>         /* FILE を使うために include している */
#include <sys/types.h>     /* ssize_t を使うために include している */

/* プロトタイプ */
extern int foo_bar( FILE *fp );
extern ssize_t foo_hoge( const char *str );

/* inline 関数はヘッダファイルに直接内容を書く.static を残す事! */
inline static double super_fast_calc( double a, double b )
{
    関数の内容
}

#endif  /* _FOO_H */

次にソースファイル foo.c です.

#include "foo.h"          /* 自分用のヘッダを include.これを忘れない */

#include <string.h>       /* ソースファイルで必要なヘッダを include */
#include <math.h>

int foo_bar( FILE *fp )
{
    関数の内容
}

ssize_t foo_hoge( const char *str )
{
    関数の内容
}

ポイントは,ヘッダファイルの #ifndef#endif によって二重に include されるのを防ぐ事です.ここのマクロ名は先頭に 「_」をつけ,ソースファイル名を大文字にしたものとするのが 一般的ですが,ユニークでなければならないので,先頭に何らかの文字列を 追加しても良いでしょう.

ここまでくれば,main()関数は普通に次のように書けます.

/* my_program.c */
#include "foo.h"

int main()
{
   ここで foo.h の関数を呼ぶ事が可能
}

なお,#include"…" は, ヘッダファイルが現在の(#includeを書く側の)ソースファイルと 同じディレクトリにある場合に使います.通常は <…> と書きますが,これはそのヘッダファイルが コンパイル時に検索パスに存在する場合に使います.

分割した場合も,コンパイルは同様に行なえます.

$ gcc -Wall -O foo.c my_program.c -o my_program

さて,プロトタイプの本当の目的が理解できたでしょうか? その答えは,「関数の呼び出し側(my_program.c)と,関数本体のソース(foo.c)との 関数の引数の型チェックを行なうため」が正解です. 例えば,関数foo_bar() の引数を1つ追加したとしましょう. この場合,当然 foo.hfoo.cmy_program.c の3つのファイルを変更する必要がありますが, その関数に関する部分のすべてのコードがキッチリ変更されていないと, コンパイルでエラーになるのです.このようにして, C言語ではプロトタイプによって,関数の厳密な型チェックが可能になっており, それによって「変更し忘れ」によるバグを防いでくれるのです.

◎課題3◎

  1. 課題2で作成したコードについて, 次の表に従ってソースファイルを分割, コンパイルし,動作確認を行なってください.
    ファイル関数
    fscan_test.c main()
    safe_string.h safe_strncpy()
    safe_stdio.{h,c} sscan_str(), fget_str(), fscan_str()
  2. 関数の引数を故意に1箇所だけ変更し(例えば,safe_stdio.cfget_str() だけを改変), コンパイルでエラーになる事を確認してください.

可読性向上,デバッグしやすさのための工夫

関数,変数,定数の名前

スコープが非常に小さい変数の場合には, 「v」や「r」 などの1文字の名前でもOKですが,ある程度のスコープ(約10行以上)の場合には, 具体的な名前をつけていきます.スコープが広くなればなるほど, 名前の具体性をあげていくようにします.

関数の名前についても,LIBCの関数名を含む場合 (例えば,safe_strcpy())のように, 非常に有名な関数名を含む場合は短縮形でもかまいませんが, 全くのオリジナルの関数の場合にはできるだけ具体的な名前をつけたいものです.


行間

行間をあける というのは,地味ですが可読性を上げるための有効なテクニックだったりします. 誰でも「真っ黒」なコードは読みたくないもので, 適度に行間が空いているだけでも,読み手のストレスを軽減する効果があります. このテキストのコードも,行間をあけて書かれている事にお気づきでしょうか?


コメント

特にプロジェクトにおける開発では, 関数の仕様をコメントで明確にしておく事は基本です. このテキストでは javadoc と呼ばれる形式を使っています. javadoc のコメントは 「/**」から始まって「*/」で終わり, 引数などの各要素(doxygenではコマンドと呼ぶ)を「@」で 記述する形をとります.例を示します.

/**
 * @file   safe_string.h
 * @brief  安全な文字列処理用関数のヘッダ
 */

/**
 * @brief  安全な strncpy() 関数
 *
 *  文字列 src の一部分を dest へコピーします.文字列 dest は常に '\0' で
 *  終端します.
 *
 * @param  dest コピー先文字列
 * @param  size_dest コピー先文字列バッファの大きさ
 * @param  src 源泉文字列
 * @param  n コピーを行なう文字数
 * @return  dest の値
 */
inline static char *safe_strncpy( char *dest, size_t size_dest, 
                                  const char *src, size_t n )

javadoc 以外の形式でも良いですが, doxygen (マニュアルコマンド) でサポートされている形式だと, 上記のコメントを読み取って HTML の関数リファレンスを 自動的に作成する事ができます.上記の例のように, ファイルの説明と関数の説明があるだけで,かなり見やすいドキュメントになります.

コード中のコメントも,ある程度パターン化しておくと良いかもしれ ません.例えば,


    /* xxxx をオープンする */
    コード;
    コード;
    コード;

    /* xxxx の計算をする */
    コード;
    コード;
    コード;

のように,空行,コメント,コードのようなパターンが一般的です.


エラー,警告の出力

これもパターン化しておくと良いでしょう. 「エラーのレベル」「関数名」「エラーの状況」 の3つは必ず出力したいものです. マメにエラーの処理を行なっているのといないのとでは, 何か問題が起きた場合のデバッグ効率が全然違います. 基本形として,次のような形を提案しておきます.

    fprintf(stderr,"[ERROR] %s: xxx failed\n",__FUNCTION__);
    fprintf(stderr,"[WARNING] %s: xxx ignored\n",__FUNCTION__);
    fprintf(stderr,"[NOTICE] %s: skipped xxx\n",__FUNCTION__);

最適化のヒント

プログラムの最適化,高速化は安全性とは方向性が異なりますが, C言語を使う以上,速さを求められる事も多いものですで, 基本的な話を少しとりあげたいと思います.


最近のCPUは内部256-bit

今日の実習で使っている環境は 64-bit OS ですし, みなさんが普段使っている OS も同様だと思います. しかし,CPUの内部はすでに128-bit単位あるいは256-bit単位で 演算が可能になっているところまで進化しています. これを「SIMD命令」といい,整数値や浮動小数点値の複数の演算を 1命令で同時に行なう事ができます.128-bit単位に演算する命令セットをSSE2, 256-bit単位に演算する命令セットをAVXと呼び, 例えば,AVXならdouble型の演算を4つ同時に行なえるというわけです. 64-bit環境であれば,gccで最適化オプションをつけてコンパイルすると, SSE2まではSIMD命令を使った自動的な最適化を行なってくれます.

CPUがどのSIMD命令をサポートしているかを調べるには, 次のようにします.

$ cat /proc/cpuinfo | grep flags
flags           : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm dca sse4_1 sse4_2 x2apic popcnt aes xsave avx lahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid

gccのオプションと自動的な最適化の限界

警告オプション「-Wall」と 最適化オプション「-O」または「-O2」を コンパイル時につけるのは当然として,gcc-4.6 以降 + AVX付きCPU という組み合わせ においては,「-mavx」を追加し AVXの命令を使った自動最適化が可能です.

ただし,SIMD命令は下手に使うとむしろパフォーマンスを低下させる事もあるので, 自動的な最適化には限界があり,コンパイラが コードを最初からトレースできる範囲しかSIMD命令を埋め込んでくれないと 考えた方が良いでしょう.例えば, main()関数で gcc による最適化のテスト をしたら良好なので,そのコードをライブラリの内部コードにしたら, パフォーマンスが悪化してしまった,というのは良くある話です.

したがって,本気で現代の CPU の性能を発揮させたい場合には, SIMD 命令を使うコードを自分で書く必要があります (SLLIB,SFITSIO では高速化のため SIMD 命令を使うコードを持っています). SIMD命令は gcc-4 以降であれば,アセンブラで書く必要はなく, C言語で記述する事ができます(といっても簡単ではありません). このページ にSIMD命令についてまとめてありますので,興味がある方はご覧ください.


ハードウェアを意識しよう: 一時バッファの最適な容量は?

画像データのように大きなサイズのデータを処理する場合には, CPUキャッシュとメモリを意識する事が大切です. CPUの一次キャッシュとメモリの速度比は20倍以上にもなり, うまくキャッシュを使えるようにコードを組めばパフォーマンスも良くなります.

圧縮・展開をはじめとする画像データのプロセッシングにおいては, 画像データの大きさより小さい「一時バッファ」を使って繰り返し処理を 行う事が良くあります. この時,この一時バッファの大きさは 1kB・10kB・100kB・1MB … どの大きさがいいのでしょう?

場合によりますが,プログラムで使う一時バッファの容量は, CPUの一次キャッシュの容量(32kB程度)から始めて 調整をしていくのが良いでしょう.


ハードウェアを意識しよう: メモリアクセス

メモリというのは,連続したアドレスのアクセスは速く, 飛び飛びのアドレスへのアクセスは遅い,という性質があります. 例えば,4096x4096 の画像に対して,横方向に連続してアクセスする場合と, 縦方向に連続してアクセスする場合とでは,後者の方が圧倒的に遅くなります.

どうしても縦方向へのアクセスを高速化したい場合は, 対象の領域について高速なアルゴリズムで x と y とを 入れ替えてから(transpose),横方向にアクセスするようにします. 一般的なtransposeのアルゴリズムは, 新規バッファ(4096x4096)を作り, CPUキャッシュに十分入るくらいの一時バッファ(例えば64x64)を用意し, その一時バッファ上にデータをコピー,一時バッファ上で x と y との入れ替え, 入れ替えたものを新規バッファにコピーするという処理を, 領域ごとに実行するというものです(下の図). なんだか非常に面倒な事をしているように見えますが, このようにする事で,メモリへの連続アクセスによる高速化 + CPUキャッシュ のヒット率向上による高速化という2つの恩恵を得る事ができ, 処理のステップは多いものの,結果的にこの方が高速に動作するのです (SLLIBやSFITSIOでは,このアルゴリズムによるtransposeが利用できます). 画像のフィルタ処理など,他の場面でもこのアルゴリズムを応用できる場合が あります.

このように,現代の計算機における最適化では, CPUキャッシュのサイズとメモリの性質を理解し, それらを踏まえたコーディングが必要とされるのです.


数学関数を速くするには

現状では gcc + LIBC の組み合わせにおいては, 数学関数において SSE2 や AVX の本来の性能を発揮できるようには なっていません. SSE2 や AVX を使って数学関数を自作するという手もありますが, 非常に難易度が高いので,速度が必要な場合は 素直にインテルコンパイラを購入される事をお勧めします. インテルコンパイラを使うと,数学関数は LIBC のそれに比べて数倍高速 に動作します. 逆に,数学関数以外の部分は gcc もかなり頑張っているため, 必ずしも高速化されるわけではないようです.


よく使うシェル上のコマンド

実際の開発では,シェルがどの程度使えるかによって 開発効率も変わってきます. 以下に代表的なコマンドを紹介しますが, sh の基本構文,grep,sed,tr くらいはある程度使えるように 修行をした方が良いでしょう.


課題の解答例