C言語初心者用スキルチェック

ここのチェック項目の回答例と補足
(リンク先のサイトと私は無関係です。初心者のスキルチェックとしてわりと妥当だと感じたので、引用させてもらっています。)

printf関数が何をする関数か知っている。

回答:第二引数以降の引数を第一引数の書式化に従って整形した上で、標準出力に出力する関数である。
これは標準関数の存在と使い方を問うている。C言語において最もメジャーであろうprintf関数が何をする関数なのかはCプログラマとしては知っていて当然だ。ここでのスキルチェックは、とりわけ正しい回答を求めているわけではなく、「あまり、深く考えずに、直感的にすばやく答えてください。」とされているので、「文字列をフォーマットして画面に出す関数」というのがパッと頭に浮かべばOKだろう。


double型の変数a, b, cにゼロ以外の値が入っていても、「double x = ( 1/3 )*(a+b+c);」の値は常にゼロになる理由を説明できる。

回答:1/3は整数型演算であり、その時点での演算結果が0であるため。
変数と型と四則演算の理解が試されている。パッと見でこれが0になることは分からないかもしれないが、「ゼロになる理由を」としているところから、よく式を見れば気づくだろう。関連して、少なくともchar, int, float型がどのような演算に用いられるのかを確認しておくこと。


平方根を求めるsqrt関数を使うには、標準ヘッダファイルをインクルードしないといけないことを知っている。

回答:標準ライブラリの関数は、対応するヘッダファイルをインクルードしなければ使用できない。
ちなみにsqrt関数はmath.hだ。これも標準ライブラリの存在とその使い方を知っているかの確認である。ヘッダファイルのインクルードを行わなければ、コンパイラがシンボルを解釈できない。ヘッダファイルには通常、関数や定数、構造体などの定義がされており、コードの実体は、標準ライブラリのものはコンパイラに組み込まれている。サードパーティのライブラリを使用する場合には、ヘッダファイルのインクルードをした上で、コンパイラにライブラリの静的リンクの設定をしてやる必要がある。


「while(1){ ... }」をどのようなときに使うかを知っている。

回答:ループ開始時点で終了条件が定義できないときに、ループ内で終了条件を判定してbreakするようにして使用する。
無限ループと答えたくなるかもしれないが、本当の無限ループが存在して良いプログラムというのはほとんどあり得ない。通常、プログラムは何かの条件で必ず終了しなければならない。しかし、ループの終了条件の判定が複雑な場合、whileの括弧中に書けないこともある。あるいは、ループ開始時点では終了判定ができない場合もある。逆にdo〜whileの末尾に達するまで終了判定を先延ばしできないということもあるだろう。
【例】

int next;

while (1)
{
    func_call();
    next = func_call2();
    
    if (!next)
    {
        break;
    }

    func_call3();
}

1からnまでループで加えるプログラムをすぐに書ける。

回答:

int i, total = 0;
for (i = 1; n >= i; ++ i) { total += i; }

N回の繰り返し処理というのは最も基本的なループの定義なので、Cでプログラムを書いていれば自然とすらすら出てくるようになる。これが出て来ないのであれば、まだプログラミングの実習が足りない。


単純な関数であれば、自分で関数を作って、プログラムを組むことができる。

これは関数の定義と使用方法の確認なので、特に引数と戻り値のある関数が相応しいだろう。例えば、2次元上の2つの点の距離を計算する関数とか。

float distance(float x1, float y1, float x2, float y2)
{
    return sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2));
}

int main()
{
    float x1 = 0.0f, y1 = 0.0f, x2 = 0.4f, y2 = -0.025f;
    printf("2点間の距離は%fです\n", distance(x1, y1, x2, y2));
}

「int *p; int *p[3]; int (*p)[3];」の違いについて理解している。

回答:int*型、int*[3]型、int(*)[3]型である。
多分これが一番難しい。C言語の初心者にとってポインタが壁だというのは昔から言われていることだ。関数ポインタを除けば、これら3種類のポインタ型と、あとint**型を覚えておけば大丈夫。後でもう少し詳しく解説する。


自分で構造体を作ってプログラムを作ることができる。

構造体は、複数の関連するデータをまとめて1つのデータ型として定義するものである。例として、先ほどの2次元座標間の距離を計算する関数に構造体を適用してみる。

struct Point2
{
    float x, y;
};

float distance(struct Point2 p1, struct Point2 p2)
{
    return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}

int main()
{
    struct Point2 p1 = {0.0f, 0.0f}, p2 = {0.4f, -0.025f};
    printf("2点間の距離は%fです\n", distance(p1, p2));
}

「int a[2]; int *p=a;」のときに、「int b=*p++;」の動作について理解している。

回答:bにa[0]の値を代入して、pの指すアドレスをsizeof(int)バイト進めている。結果、pはa[1]を指すことになる。


英文だけからなるファイルを入力して、その中に'a'という文字が何個あるのか計測するプログラムを作ることができる。

これは最後のチェック項目だけあって、ちょっとだけ実践的だ。ファイルI/Oと文字列操作はプログラミングの基本である。以下、メイン関数の引数でファイルパスを受け取るものとする場合のプログラム例である。

int main(int argc, const char** argv)
{
    static const int BufferSize = 1024;
    static const char Letter = 'a';
    char buffer[BufferSize];
    int count = 0;
    FILE* fp = NULL;
    char* cp;
    
    if (1 < argc)
    {
        fp = fopen(argv[1], "r");
        
        if (NULL == fp)
        {
            fprintf(stderr, "file not found.(%s)\n", argv[1]);
            exit(1);
        }
        
        while (NULL != fgets(buffer, sizeof(buffer), fp))
        {
            for (cp = buffer; cp != &buffer[BufferSize] && '\0' != *cp; ++ cp)
            {
                if (Letter == *cp)
                {
                    ++ count;
                }
            }
        }
        
        fclose(fp);
    }
    else
    {
        fprintf(stderr, "too few arguments.\n");
    }
    
    printf("%d", count);
}

いくつかのポインタについてちょっとだけ解説

リンク先のサイトのおっさんはポインタ遊びが大好きなようだが、何も int *(*f(double x, int y))[2] こんなけったいなポインタ定義をマスターする必要はない。まあ、どうしてもそういうものが必要になることはあるかもしれない。まずは、必要になったときにすぐに調べて使えるだけの基礎知識があれば十分だ。

およそ、普段使用するポインタの種類としては、前述した3つとint**くらいだろう。尚、ここでは便宜上int型へのポインタのみで説明しているが、ポインタの型はアドレスを参照するときの型を示すということであって、ポインタの本質的な理解とは無関係のことである。

【コード例】

int a = 1, b = 2, c = 3;
int array[3] = {a, b, c};
	
int* p1 = &a;			// int*      型である (int型変数のアドレスを指すポインタ)
int* p2[3] = {&a, &b, &c};	// int*[3]   型である (長さ3のint*型の配列)
int (*p3)[3] = &array;	        // int(*)[3] 型である (長さ3のint配列全体に対するポインタ)
int** p4 = p2;			// int**     型である (int*型のアドレスを指すポインタ)

C言語の難しいところの1つは、配列とポインタが似ていることである。
両者を区別する前に、まずポインタが何なのかを理解すべきだ。

ポインタは、メモリアドレスを指す変数型である。
ポインタ型が何バイトなのかは決められていないが、すべてのポインタ型は同じサイズである。
int*もchar*もint**も任意の構造体へのポインタも同じサイズである。

では、何のためにchar*とint*が区別されているのか。
それは、ポインタ変数の指し示す先の変数の型を区別するためである。

int *ip;
char* cp;

この2つのポインタがあった場合に、*ipはint型で、*cpはchar型である。
ポインタ変数の指し示す先の変数型のサイズが異なる場合、ポインタの歩幅が異なることになる。仮にintを4バイト、charを1バイトとしたとき、++ipは参照先のアドレスを4バイト進めることになり、++cpは1バイト進めることになる。

それでは、ポインタと配列との違いは何なのか。

①配列は宣言時に「中身の変数」を格納するのに必要なサイズのアドレス空間を確保する
②配列型変数は、確保したメモリ領域の先頭アドレスを指すものであり、アドレスを変更することはできない

それ以外においては、配列は単にポインタ演算に対するシンタックスシュガーである。
即ち、

int a[3] = {10, 20, 30};
int* p = a;

とした場合に、aとpの指すアドレスは等しく、a[0]は*pであり、a[1]は*(p+1)である。

ここで①の違いについて見てみよう。
至極当然のことであるが、aは宣言時に、int変数3つ分のメモリ領域を確保している。一方、pはポインタ変数として、自身のメモリ(sizeof(int*)分)を確保しただけである。p = aとしたところで、sizeof(p)とsizeof(a)は当然異なるのである。この時、sizeof(a)は、配列内の変数のために確保したアドレス空間のサイズを表し、sizeof(p)は、p自身、即ちメモリアドレスを記憶するためポインタ変数のサイズを返す。

次に②の違いについて見てみよう。先のコード例にポインタ演算を加えてみた。このうち配列のアドレス変更はコンパイルエラーになる。

int a = 1, b = 2, c = 3;
int array[3] = {a, b, c};
	
int* p1 = &a;			// int*      型である (int型変数のアドレスを指すポインタ)
int* p2[3] = {&a, &b, &c};	// int*[3]   型である (長さ3のint*型の配列)
int (*p3)[3] = &array;	        // int(*)[3] 型である (長さ3のint配列全体に対するポインタ)
int** p4 = p2;			// int**     型である (int*型のアドレスを指すポインタ)

++array;    // NG:配列の参照先アドレス変更はできない
++p1;       // 参照先アドレスをsizeof(int)バイト進める
++p2;       // NG:配列の参照先アドレスは変更できない
++p3;       // 参照先アドレスをsizeof(int[3])バイト進める
++p4;       // 参照先アドレスをsizeof(int*)バイト進める

ここで、p4 = p2がエラーにならないことに注目して頂きたい。
これを、p2 = p4とするとエラーになる。

なぜなら、p4はポインタであり、p2は配列だからだ。
ポインタ変数p4は、参照先がint*型である限り、いかなる場所も指し示せる。
配列p2の指し示す場所を変更することはできない。

また、p4 = p3もエラーになる。
これは、p4が参照する型がint*型であるのに対して、p3の参照する型がint[3]であるためだ。前述の①の相違により、int*とint[3]は全くサイズの異なるものである。int(*)[3]という珍妙なポインタは、配列全体に対するポインタの指定であることに注意されたし。


以上で「ポインタとは何か」と「ポインタと配列の違い」を理解して頂けただけたのではないだろうか。他の9つのチェックリストもクリアできていれば、初心者は卒業と言って良いだろう。Cでアプリケーションを作るも良し、C++, Objective-C, Javaなど、Cと似た言語を学ぶも良し。