2013年7月 初版
会場: 天文データセンター
現在のデータ解析においては,スクリプト言語や解析パッケージが 多用されていますが,解析ソフトウェアの検証,解析手法を工夫し た独創的な研究,あるいはデータのサイズが大きいなどの場合には, ピクセル単位の高速処理が可能な CやC++が必要な場面が多いものです.
しかし,CやC++の開発環境は汎用性を前提としたものであるため, データ解析のための実用的なツールとしては遠く, さらには,技術の選択肢が多いために, 全体として難易度が高いのが実情です.また, 書籍などの情報面をみても, 天文学者が本当に必要とするものが十分であるとは言えません.
このような背景から, 天文学者が本当に必要とする実用的な開発環境として, ライブラリセット「SFITSIO + SLLIB」が開発されました. さらにこの講習会は,天文学者にとって必要な技術面での情報不足を 解消するために企画しました.
科学研究を実施する時において最も重要と言われている事として 「正しさ」「再現性」があります.さらに現代では早期の研究成果 出版の必要性から「高速処理」も求められます. しかし実際には,天文業界においては,「正しさ」や「再現性」は あまり重視されていないのが実状です.その根拠としては,
といった状況があげられます.
にもかかわらず,天文業界では 既存のソフトウェア(主に外国製)が信用されすぎているようにも感じます. みんなが使っているから安心…本当でしょうか? 自分である程度大きいプログラムが書けるようになると, 少しコードを読めばそのツールのソフトウェア品質に対する取組みが 見えてくるものですが,実際のソフトウェアのコードをみてみると, 実はその品質はあまり高くありません.
本来は,天文業界で一般的に行なわれている「動けばOK」的な開発ではダメで, 科学研究で重要な「どこでも正しく動作する」事に対する品質や「高速処理」 が求められるはずです. そういった事の追及には, ソフトウェア品質を高めるためのノウハウ(コーディングルールや試験)や 最新のハードウェアに関する知識が求められます. このように考えると,ソフトウェア開発というのは,科学研究と同じで 終わりのない研究課題だと言えます.
この講習会の第1部では,そのようなソフトウェア品質に関するノウハウの一部を取り上げ, 科学用途のソフトウェア開発で重視すべき事やその大変さを知っていただく事も 目的の1つとなっています.
本講習会の目的は, C/C++によるデータ解析を行ないたい研究者やプログラマの方々が, 解析結果の信頼性を第一とし, 高いパフォーマンスを狙えてかつ開発効率の良いコーディングスキルを 身につけていただく事が目的です.
本講習会は上記の目的に沿った3部構成になっており, 教科書や雑誌ではなかなか取り上げられない内容を多く含んでいます.
C言語は最高のパフォーマンスを狙えるかわりに, 安全面での保護の仕組みが十分ではありません. ちょっとした勘違いによるバグであっても, プログラムが予想不能な挙動を示す事も多々あり, バグの修正に不必要な時間をとられている,といった光景をよく見かけます. そんな目にあうのは誰でも嫌なものですが,では, どのようにすればバグを最小化できるのかを, 第1部では具体的な例をあげなら考えていきます.
プログラマは必ずバグを作ります. バグを減らすための最も基本的なテクニックは, ワンパターンに持ち込み,多くの記法を使わない事です. 別の言い方をすれば,技術をもて遊ばない事です. 技術をもて遊んだ結果バグを作るというのは,初級者が最も陥りやすい罠です.
ここでは,いくつかの例を示し, ワンパターンに持ち込むとはどういう事なのかを,解説します.
C言語には,無くても困らない記法が存在します. 例えば,次に挙げるものはほとんど使う必要がないものです. 本当に必要かどうかよく考えてみましょう.
switch
〜 case
文 break;
を書き忘れてバグの原因になります.
if
文で十分です.
*p++
」のような縮約表記 v = *p; p++;と丁寧に書きましょう.
if
〜 else
の分岐で済むのなら,その方が良いです.
例えば,次のコードでは if
の後に
continue
を書いても同じ事ですが,使わずに済ましています.
for ( i=0 ; i < n ; i++ ) { if ( ... ) { /* do nothing */ } else { : } }上方向へのジャンプは
goto
文
以外であっても好ましくありません.
flag = (( 0 < x ) ? 1 : 0);使うメリットがあまりありません. マクロでどうしても必要な時以外は使う必要はないでしょう.
多くの変数がコードの大きな範囲で有効になっていると, コードがわかりにくくなり,バグの原因になります. 変数や関数のスコープについても,「ワンパターン」の考え方を実践する事が 大切です.
次の例は,私が使っているパターンの例です.変数の宣言は
「{
」の直後にまとめて書くようにしています.
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」を消去します.
カウンタの使い方もパターン化しておきます.
Cではポインタ変数を使う以上, カウンタ等の数え方を 0 からに統一しておきたいものです. 「0から」「1から」が混在しているとバグの原因になります.
どうしても 1 からの値が必要なら,ループ中で 1 を加算します.
for ( i=0 ; i < n ; i++ ) { const int ii = i+1;
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()のフォーマット |
---|---|---|---|
char | 1バイト符号有り整数 | - | "%hhd" |
short | 一般的に2バイト符号有り整数 | - | "%hd" |
int | 一般的に4バイト以上の符号有り整数 | - | "%d" |
long | 一般的に4バイト以上の符号有り整数 | - | "%ld" |
long long | 一般的に8バイト符号有り整数 | - | "%lld" |
ssize_t | アドレスの大きさに一致した符号有り整数 | sys/types.h | "%zd" |
int8_t | 1バイト符号有り整数 | stdint.h | intへキャスト |
int16_t | 2バイト符号有り整数 | stdint.h | intへキャスト |
int32_t | 4バイト符号有り整数 | stdint.h | longへキャスト |
int64_t | 8バイト符号有り整数 | stdint.h | long longへキャスト |
符号無し整数
型 | 意味 | ヘッダ | printf()のフォーマット |
---|---|---|---|
unsigned char | 1バイト符号無し整数 | - | "%hhu" |
unsigned short | 一般的に2バイト符号無し整数 | - | "%hu" |
unsigned int | 一般的に4バイト以上の符号無し整数 | - | "%u" |
unsigned long | 一般的に4バイト以上の符号無し整数 | - | "%lu" |
unsigned long long | 一般的に8バイト符号無し整数 | - | "%llu" |
size_t | アドレスの大きさに一致した符号無し整数 | stddef.h | "%zu" |
uint8_t | 1バイト符号無し整数 | stdint.h | unsigned intへキャスト |
uint16_t | 2バイト符号無し整数 | stdint.h | unsigned intへキャスト |
uint32_t | 4バイト符号無し整数 | stdint.h | unsigned longへキャスト |
uint64_t | 8バイト符号無し整数 | stdint.h | unsigned long longへキャスト |
浮動小数点数 (実数)
型 | 意味 | ヘッダ | printf()のフォーマット |
---|---|---|---|
float | 4バイト浮動小数点数 | - | "%g" など |
double | 8バイト浮動小数点数 | - | "%.15g" など |
これらのうち,重要なポイントについて以下で解説しておきます.
1文字を表現する場合(例えば 'a'
)は int
を使います.
char
や unsigned char
を使った場合,
標準C関数を使った場合に,
EOF (-1と定義される) なのか,ある文字が 0x0ff
なのかの
区別がつかない事があるためです.
一般に 32-bit OS でも 64-bit OS でも int は 32-bit 整数ですが,
厳密なビット幅が必要な場合には使ってはいけません.
long は一般に 32-bit OS では 32-bit,64-bit OS では 64-bit 整数 です. long long は一般に 64-bit 整数です. データ解析で利用する一般的な整数としては,long を使うと良いでしょう. ただし,厳密なビット幅が必要な場合には使ってはいけません.
32-bit OS なら 32-bit 符号無し整数であり,
64-bit OS なら 64-bit 符号無し整数です.
ファイルI/Oや文字列などの,バッファ操作やポインタ変数の演算を行なう場合に使用します.
符号無しである事に注意してください.
データ解析の場面でも,文字列や画像のサイズ等で利用します.
32-bit OS なら 32-bit 符号有り整数であり, 64-bit OS なら 64-bit 符号有り整数です. 負の値が使えるため,バッファ操作などのためのオフセット値や 関数の返り値として使う事があります.
バイナリファイルを扱うなど,
厳密なビット幅を持つ型が必要な場合に使用します.
浮動小数点の計算を単精度で行なうメリットは無いので, 数学計算用に変数を用意する時にはdouble型を使うようにします. 配列の場合は,要素数が多く単精度で十分な場合には, floatの配列を使うとメモリアクセスを減らす事ができ, パフォーマンスの改善が期待できます.
C言語の最も難しいと言われるのは, ポインタ変数の取り扱いだと言われています. その「難しい」理由は次の2つで, 特に1番目については書籍等で十分に解説がなされていない事が多く, 初心者にとっての「最大の難関」になっています.
*
」を異なる意味で使う.
以下では,上記のポイントを踏まえ, ポインタ変数についての基本をわかりやすく解説していきます.
例えば,次のような変数または配列の宣言をみたとき, どこが「型」で,どこが「変数(定数)」なのかわかりますか?
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言語で「型」を理解するための第一歩は, 次の規則を頭に入れておく事です.
上記の表の型の意味はともかく,まずはこの規則が頭に入っていないと, 普通の感覚では先に進むのが難しくなってきます. 特に,スペースや括弧にだまされてはいけません.
ポインタ変数を学習するにあたって,まずは 「ポインタ」という抽象的な言葉を使う事をやめる事 をお勧めします.「変数のアドレス」「ポインタ変数」のように, 具体的な言葉を使いましょう. 以下では,その具体的な言葉を使ってポインタ変数の基本について 説明していきます.
a
」のアドレスを表示するには,
次のようにします.
/* ★ */ double a; printf("address of a = %zu\n", (size_t)(&a));配列の場合は,「配列名が配列の先頭要素のアドレスを表す」 という決まりになっています.例えば,
/* ★ */ double array[256]; printf("address of array = %zu\n", (size_t)array);のようにすると,
array[0]
のアドレスを
取得する事ができます.もちろんこれは次のようにも書けます.
/* ★ */ double array[256]; printf("address of array = %zu\n", (size_t)(&array[0]));お気付きの方もいらっしゃるかもしれませんが, この例での「
array
」は定数です.
そして,「array[i]
」は変数なのですね.a
のアドレスが 3221223272,
配列 array
のアドレスが 3221221216 だとわかったとします.
この場合を図示してみると,次のようになります.double a; size_t p; p = &a;のように書いてしまえば良いと思われるかもしれませんが, 「p」の指す先(つまり保持しているアドレスに存在する値) を取得したい場合, これではどうにもならないという事になってしまいます.
double *
」「int *
」
のように「*
」をつけて表します.
上の例をポインタ変数を使って書いてみると次のようになります.
double a; double *p; p = &a;念のため確認しますが, 宣言「
double *p;
」では,どこまでが型で,
どこまでが変数なのか,理解していますか?
空白文字に騙されないように注意しましょう.
「型」「変数」がわかっていれば,次のような表記でも
ビクともしないはずです.
double a; double *p = &a;
*
」を使うのです.
例えば,
double a = 123; double *p; p = &a; printf("%g\n",*p);と書くと,変数「a」の値を表示する事ができます. 「*」はかけ算にも使われるので,C言語では 3つの意味で使われるという実にいやらしい仕様で困ったものです.
/* ★ */ double a = 123; double *p; p = &a; printf("%g\n",*(p));
p
を次のように
加算するとどうなるでしょうか?
p++;この場合は p の値(アドレス値)が +8 されます. p は変数 a のアドレスを保持しているので,こんな事をしても 何の御利益もありませんが,ある大きさを持ったバッファの場合には 意味を持ちます.
/* ★ */ double array[] = {1.23, 4.56}; double *p; p = array; printf("%g\n",*(p)); p++; printf("%g\n",*(p));そうですね.「
p++;
」で,
次の要素にアクセスするために double のバイト幅である
8 がプラスされるわけです./* ★ */ double array[] = {1.23, 4.56}; double *p; p = array; printf("%g\n",*(p+0)); printf("%g\n",*(p+1));
/* ★ */ double array[] = {1.23, 4.56}; double *p; p = array; printf("%g\n",p[0]); printf("%g\n",p[1]);最後の
[
i]
の形を使う形が誰がみてもわかりやすく,
最も良いコードです.バッファを操作する時は,この書き方を
自分の「パターン」として持っておく事をお勧めします.&
」を前につける.
*
」を使って「double *
」
「int *
」のようになる.
*(p)
」のように書き,
指す先がバッファの場合には「p[i]
」のように書く.
ポインタ変数の宣言 | 型 | ポインタ変数 | 指す先へのアクセス (指す先が変数) | 指す先へのアクセス (指す先がバッファ) |
---|---|---|---|---|
double *p; | double * | p | *(p) | p[i] |
○実習2○
「/* ★ */
」のついたコードについて,
動作確認を行なってください.
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○
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次元配列を引数に与える場合です. これは特に難しい事はないと思います.
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
」
を配列を受けるために使うのは好ましくありません.
配列なら配列の書き方で書くべきです.
C言語で関数に2次元配列を与えるには, ポインタ配列を利用します. 具体的には,呼び出し元で通常の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
」には,
などの御利益があり, 基礎的なところの安全性を高めるために有効ですから, 積極的に使っていく事をお勧めします.
#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; }
"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
」を書く事ができます.
この見慣れない書き方は,どういう意味なのでしょうか.
関数の引数の宣言の場合について,以下の表にまとめてみました.
引数の宣言 | 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()
関数を使って,ヒープからバッファを確保するコードを,
スタックからとる場合とコードを比較しながら,
もう少し詳しくみていきましょう.
double array[N_ELEMENTS];
double *array = NULL; array = (double *)malloc(sizeof(*(array)) * N_ELEMENTS); if ( array == NULL ) { エラー処理 }「
array
」が「double *
」型のアドレスを入れるためのポインタ変数で,
関数malloc()
の返り値が
array
に入る.この時,返り値は void *
型
なので double *
型にキャスト.
関数malloc()
の返り値は
成功した場合は,バッファの先頭アドレス,失敗した場合は NULL.
関数malloc()
の引数は,バイト単位で与える事
に注意.man malloc
」してみる事.
size_t i; for ( i=0 ; i < N_ELEMENTS ; i++ ) { array[i] = ... }
double []
型でうければOK.
static int func( double arr[], size_t n ) ...
スタック上の配列の宣言 | ヒープ用のポインタ変数 | 配列へのアクセス |
---|---|---|
double array[256]; |
double *array = NULL; |
array[i] = ... |
今度は2次元のバッファをヒープから確保する場合をみていきます. スタックからとる場合とコードを比較しながら解説します.
double array_1d[N_X * N_Y]; double *array_2d[N_Y];
double *array_1d = NULL; double **array_2d = NULL; array_1d = (double *)malloc(sizeof(*(array_1d)) * N_X * N_Y); if ( array_1d == NULL ) { エラー処理 } array_2d = (double **)malloc(sizeof(*(array_2d)) * N_Y); if ( array_2d == NULL ) { エラー処理 }「
double **
」を使うのがポイント.
これは double *
型の変数やバッファのアドレスを
入れるための型.「指す先」を見にいく演算子([i]
または *()
)を2つつけると,double型に戻る.
size_t i; for ( i=0 ; i < N_Y ; i++ ) { array_2d[i] = &array_1d[N_X * i]; }代入文の左右の「型」が一致してる事を確認しよう.
size_t i; for ( i=0 ; i < N_Y ; i++ ) { size_t j; for ( j=0 ; j < N_X ; j++ ) { array_2d[i][j] = ... } }
double *[]
型でうければOK.
static int func( double *arr_2d[], size_t nx, size_t ny ) ...
スタック上の配列の宣言 | ヒープ用のポインタ変数 | 配列へのアクセス |
---|---|---|
double array_1d[N_X * N_Y]; double *array_2d[N_Y]; |
double *array_1d = NULL; double **array_2d = NULL; |
array_2d[i][j] = ... |
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つ前のコードのように,次のようなパターンを死守します.
return
を複数個書かない.
return は最後に1回のみとする.
free()
は,最後の return
の直前にまとめて書く.
quit:
を free()
の塊の前に設置し,
途中で return
したくなったら,
「goto quit;
」と書く.
具体的には次のようなパターンになります.
{ 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()
関数は,
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()
関数でより大きい領域を再確保した場合,
かなりの確率で 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(C標準ライブラリ)には,一応一通りの文字列処理用の関数があります. しかし,LIBCの関数が制定された当時は今ほど安全性が求められていなかったので, 今となってはおかしな仕様になってしまっているものも多く存在します. 以下に,例をあげます.
sscanf()
"%s"
指定の場合,バッファオバーフローを引きおこす.
scanf()
, fscanf()
"%s"
指定の場合,バッファオバーフローを引きおこす.
strncpy()
'\0'
で終端しない事がある.そのまま使うべきではない.
strcpy()
, strcat()
,
strncat()
strchr()
,
strrchr()
,
strstr()
など.他にも多数const char *
で受けておきながら,
返り値は char *
である.
strtok()
,strsep()
このように,LIBCの関数には様々な問題があります. 特にバッファオーバーフローを起こすと, プログラムは予想不可能な挙動をとる事があり, デバッグを難しくする原因になります. できるだけ安全にいきたいと考えるならば, そのまま使える関数は限られてきます.この状況を踏まえて, ここではより安全な文字列の処理について考えていきます.
C言語での開発の場合,文字列を扱う関数は,
char []
型のバッファを引数にとる事が多いものです.
その時,引数をバッファの大きさにするか,文字列の長さにするかを
迷うところですが,ここは迷わずバッファの大きさにします.
この理由は,もし関数の引数の仕様が「文字列の長さ」だと,
誤って「バッファの大きさ」を与えると '\0'
へのアクセス
によりバッファオーバーフローを
起こすからです(逆の場合には,バッファオーバーフローは起こりません).
文字列のバッファに対してフォーマット出力を行なうための関数
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 *
になっていてイマイチという話をしましたが,
これは要するにアドレスを直接扱うから問題になるわけです.
アドレスではなく,オフセット値で扱えば,このような問題は起きません.
以下では,オフセット値で文字列の検索処理を行なう事について考えていきます.
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); }
まず,ストリームから直接読む
scanf()
や fscanf()
は,行単位で読み込んでくれない事があるので,使うのはやめます.
ストリームから読む場合は,
まず fgets()
でバッファに読み込んでから,
sscanf()
を使うようにします.
そして,
sscanf()
の最大の問題はフォーマットに "%s"
を
指定した場合にバッファオーバーフローしてしまうという危険性でしたが,
これは次のようにして対処できます.
char el0[20], el1[20], el2[20]; sscanf(str, "%19s%*[^ ] %19s%*[^ ] %19s%*[^ ]", el0, el1, el2);
"%*[^ ]"
は空白以外の文字からなる文字列を読み飛ばすための指定です.
これで,文字列中の各要素を確実に分割でき,かつ
各要素それぞれの最初の 19 文字だけを取り出す事ができます.
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◎
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 );
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 );
atof()
関数などを使用します.
浮動小数点型(float または double)には,
無限大や非数(not a number)という特別な値があります.
これらの値は math.h
でそれぞれ
INFINITY
と NAN
として
マクロで定義されています.
これらの特別な値のうち,非数(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.h
,foo.c
,
my_program.c
の3つのファイルを変更する必要がありますが,
その関数に関する部分のすべてのコードがキッチリ変更されていないと,
コンパイルでエラーになるのです.このようにして,
C言語ではプロトタイプによって,関数の厳密な型チェックが可能になっており,
それによって「変更し忘れ」によるバグを防いでくれるのです.
◎課題3◎
ファイル | 関数 |
---|---|
fscan_test.c |
main() |
safe_string.h |
safe_strncpy() |
safe_stdio.{h,c} |
sscan_str() , fget_str() , fscan_str() |
safe_stdio.c
の
fget_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言語を使う以上,速さを求められる事も多いものですで, 基本的な話を少しとりあげたいと思います.
今日の実習で使っている環境は 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
警告オプション「-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 くらいはある程度使えるように 修行をした方が良いでしょう.
file 任意のファイル
$ file /usr/bin/gcc /usr/bin/gcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped
ldd 任意の実行ファイル
$ ldd /usr/bin/gcc linux-vdso.so.1 => (0x00007fffe7fff000) libc.so.6 => /lib64/libc.so.6 (0x0000003573000000) /lib64/ld-linux-x86-64.so.2 (0x0000003572800000)
nm 任意のオブジェクトファイル
strings 任意のオブジェクトファイル
head,tail
grep パターン ソースファイル
ssize_t
を含む部分を表示する.
$ cd /usr/include $ grep ssize_t `find .` ./_G_config.h:#define _G_ssize_t __ssize_t ./dirent.h:extern __ssize_t getdirentries (int __fd, char *__restrict __buf, ./dirent.h:extern __ssize_t __REDIRECT_NTH (getdirentries, ./dirent.h:extern __ssize_t getdirentries64 (int __fd, char *__restrict __buf, (省略)「
grep -i パターン ファイル
」で
大文字小文字の区別をせずに検索可能.
grep はデバッグで重宝するので,開発には必須のコマンド.
sys
」がつくファイルを
日付順に並べかえる$ L=`find . | grep sys` $ LL=`for i in $L ; do if [ -f $i ]; then echo $i ; fi ; done` $ ls -ltr $LL