C Puzzles(C言語パズル集)の個人的解答

C Puzzlesの日本語訳があったので、解いてみた。

夏休み中に解いたので、一応「夏休みの課題」になるのかな。とりあえず、所詮は自称Cプログラマなレベルの力量しか持ってないことを再確認した。うーん、修行が足らぬ。

以下、問題文は上記の翻訳版からのコピペ。ちょっと気になる部分だけ訂正している。問題に番号が割り振られていたなら、問題文をコピーしなくて済んだのだけど……。

日付 内容 備考
2015-08-29 初版作成 残り4問

1

次のCプログラムは、配列の要素を表示してくれるはずです。しかし、実際に走らせてみると、期待した出力が得られません。

#include<stdio.h>

#define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
int array[] = {23,34,12,17,204,99,16};

int main()
{
    int d;

    for(d=-1;d <= (TOTAL_ELEMENTS-2);d++)
        printf("%d\n",array[d+1]);

    return 0;
}

何が間違っているのでしょうか。

実行すると、何も表示されない。暗黙の型変換のルールが原因で、for文の継続条件の判定結果がこのコードの作者の意図と異なってしまっている。

マクロTOTAL_ELEMENTSで使用しているsizeof演算子はsize_t型(符号なし広義整数型)を返す。大半の処理系では、size_t型の実体はunsigned long型である。式(TOTAL_ELEMENTS-2)を評価した値の型はsize_t型になる。

for文にて変数dに-1を代入した直後、式d <= (TOTAL_ELEMENTS-2)にて、変数dがint型からsize_t型に型変換された結果、0xFFFFFFFFLUと同じビット列の符号なし整数型と見なされてしまい、(TOTAL_ELEMENTS-2)よりも大きいと判定されて即座にループを終了してしまう。

考える修正方法は2つ。

1つ目は、変数dをsize_t型にした上で、負の値を使わないようにする方法。最初から符号なし整数型で統一してしまえば、負の値による暗黙の型変換時の問題は発生しない。

2つ目は、式(TOTAL_ELEMENTS-2)をint型にキャストして用いる方法。すなわちd <= (int) (TOTAL_ELEMENTS-2)のようにして比較する。こうすればint型同士での比較演算となるので、暗黙の型変換は発生しない。

2

次のコードを見て、私は完璧なCプログラムだと思いました。しかし、コンパイルする際に、下らない間違いを見つけてしまいました。何だか分かりますか?(コンパイルせずに指摘してください:-)

#include<stdio.h>

void OS_Solaris_print()
{
        printf("Solaris - Sun Microsystems\n");
}

void OS_Windows_print()
{
        printf("Windows - Microsoft\n");

}
void OS_HP-UX_print()
{
        printf("HP-UX - Hewlett Packard\n");
}

int main()
{
        int num;
        printf("Enter the number (1-3):\n");
        scanf("%d",&num);
        switch(num)
        {
                case 1:
                        OS_Solaris_print();
                        break;
                case 2:
                        OS_Windows_print();
                        break;
                case 3:
                        OS_HP-UX_print();
                        break;
                default:
                        printf("Hmm! only 1-3 :-)\n");
                        break;
        }

        return 0;
}

関数名OS_HP-UX_printはハイフンマイナス(「-」)を含んでいるため、識別子として正しくない。なのでコンパイル時に当該部分でエラーとなる。

識別子にハイフンマイナスを含んでいても問題ないプログラミング言語って、LispClojureCommon LispEmacs Lisp、ISLisp、Scheme、その他)とTclとシェルスクリプトPowerShellとバッチファイル以外に何があるのだろうか? プログラミング言語か否かを無視するなら、CSSXMLも該当するか。

3

次のプログラムからはどのような出力が得られるでしょうか? また、その理由を答えてください。

enum {false,true};

int main()
{
        int i=1;
        do
        {
                printf("%d\n",i);
                i++;
                if(i < 15)
                        continue;
        }while(false);
        return 0;
}

標準出力に1とだけ出力される。

変数iに1を代入した後、do文のprintfとi++が実行され、式i < 15が真なのでcontinueが実行され、while(false)が評価されて偽となるのでループを抜ける。

正直なところ、この問題が意図するところを掴みかねているのだが……もしかして、「do-while文でcontinueすると継続条件が判定されない」と思い込んでいる人がいる?

4

次のプログラムは”hello-out”と出力してくれるようには”見え”ません。(実行してみてください)

#include <stdio.h>
#include <unistd.h>
int main()
{
        while(1)
        {
                fprintf(stdout,"hello-out");
                fprintf(stderr,"hello-err");
                sleep(1);
        }
        return 0;
}

どんな理由が考えられるでしょうか。

標準出力はIOの高速化のためにバッファリングしているため、実際にデータを書き出す条件に達するまで、標準出力に書き込んだデータは内部のバッファに溜められていく。

setvbuf(3)のバッファリングの種類にもとづけば、_IOFBFならばバッファが満杯になったら出力されるし、_IOLBFならば改行が書き込まれるかバッファが満杯になったら出力される。

上記のソースコードのような使い方の場合は、標準出力に書き込んだデータは、バッファが満杯になるまで表示されず、満杯になったら今まで書き込んだ分が一気に表示される。hello-outhello-errを交互に出力したいならば、fprintf(stdout,"hello-out");の直後にfflush(stdout);を実行するか、予め標準出力のバッファリングを_IONBF(バッファリングしない)にしておくこと。

5

#include <stdio.h>
#define f(a,b) a##b
#define g(a)   #a
#define h(a) g(a)

int main()
{
        printf("%s\n",h(f(1,2)));
        printf("%s\n",g(f(1,2)));
        return 0;
}

このプログラムを見て、両方のprintfの出力は同じになると考える人がいるかもしれません。しかし、プログラムを走らせると、次の結果を取得します。

bash$ ./a.out
12
f(1,2)
bash$

なぜそうなるのでしょうか?

マクロの演算子#トークンをマクロ置換することなく文字列定数に変換する。

そのためg(f(1,2))は、引数f(1,2)をマクロ置換せずに文字列定数に変換することになり、文字列"f(1,2)"となる。

一方でh(f(1,2))の場合、マクロ関数h()では引数がマクロ置換されるため、まずh(f(1,2))が展開された結果g(12)となり、そこからg(12)が展開されて文字列"12"となる。

6

#include<stdio.h>
int main()
{
        int a=10;
        switch(a)
        {
                case '1':
                    printf("ONE\n");
                    break;
                case '2':
                    printf("TWO\n");
                    break;
                defa1ut:
                    printf("NONE\n");
        }
        return 0;
}

上記プログラムの出力がNONEになると予測したなら、本当にそうなるかぜひチェックをしてみてください!!

何も表示されない。

switch文をよく見るとdefaultではなくdefa1utとなっている。そのため、単なるgotoのラベルと見なされてしまう。残りのcase文の条件に合致するものがないため、switch文中のステートメントは何も実行されない。

7

次のCプログラムはIA-64ではセグメンテーション違反ですが、IA-32ではきちんと動きます。

int main()
{
    int* p;
    p = (int*)malloc(sizeof(int));
    *p = 10;
    return 0;
}

なぜそのようなことが起こるのでしょうか?

malloc(3)が成功しているものと仮定して解答すると、stdlib.hをインクルードしていないため、このプログラムのコンパイル時にmalloc(3)の戻り値がint型であると暗黙のうちに解釈されたオブジェクトコードが生成されている。

IA-64ではポインタ型は64bitだが、上記経由より、本来malloc(3)が返す64bitの値のうちint型に収まる32bitの値を、そこからint*にキャストして64bit化した値が、変数pに格納される。その値は不正なアドレス値(64bit中32bitしか正しくない)であるため、不正なアドレスに代入してセグメンテーション違反となる。

これ、OSのプロセスのメモリモデル次第だけど、x86-64で64bit OSを使っている場合にも起きるんではないだろうか?

8

9

次のプログラムからは、どんな出力が得られると思いますか? また、その理由も答えてください。(もし”fは1.0″"f is 1.0"だと思うのであれば、もう一度、確認してみてください)

#include <stdio.h>

int main()
{
        float f=0.0f;
        int i;

        for(i=0;i<10;i++)
                f = f + 0.1f;

        if(f == 1.0f)
                printf("f is 1.0 \n");
        else
                printf("f is NOT 1.0\n");

        return 0;
}

f is NOT 1.0と出力される。

そもそも0.1の二進数表現(≒bit列による表現)は循環小数となるため、使用している浮動小数点数の形式が二進浮動小数点形式であるならば、0.1を正確に表現することができない。近似値を用いていることになるため、加算を繰り返すほど誤差が広がっていく。10回加算した結果、1.0にわずかに満たない値になってしまうため、1.0との比較が真となることはありえない。

10

C言語のカンマ演算子について学んでいたので、次のCプログラムは完璧だと思っていました。しかし、このプログラムには誤りがあります。それは何でしょうか?

#include <stdio.h>

int main()
{
        int a = 1,2;
        printf("a : %d\n",a);
        return 0;
}

この書き方では、int a = 1,2;のコンマは初期化宣言子並びにおける区切りのコンマであると解釈される。コンマより後を独立した初期化宣言子並びと解釈しようと試み、不正な形式であるため、コンパイルエラーとなる。

int a = (1,2);のように括弧で囲めば、1,2コンマ演算子を含む1つの初期化子であると解釈されるため、コンパイルエラーは起きない。

11

次のCプログラムからは、どのような出力が得られるでしょうか? (このCプログラムは正しく動くでしょうか?)

#include <stdio.h>
int main()
{
        int i=43;
        printf("%d\n",printf("%d",printf("%d",i)));
        return 0;
}

画面に4321と表示される。末尾は改行されている。

最初の43はネストの最も内側のprintfが変数iの内容を表示したもの。次の2は、ネストの真ん中のprintfが、ネストの内側のprintfの戻り値(出力した文字数である2)を表示したもの。最後の1と改行は、ネストの外側のprintfが、ネストの真ん中のprintfの戻り値(出力した文字数である1)を表示したもの。

12

void duff(register char *to, register char *from, register int count)
{
    register int n=(count+7)/8;
    switch(count%8){
    case 0: do{ *to++ = *from++;
    case 7:  *to++ = *from++;
    case 6: *to++ = *from++;
    case 5: *to++ = *from++;
    case 4: *to++ = *from++;
    case 3: *to++ = *from++;
    case 2: *to++ = *from++;
    case 1: *to++ = *from++;
            }while( --n >0);
    }
}

上のC言語のコードは、正しく動くでしょうか? もし動くのであれば、このコードによって何を得ることができますか? なぜ、誰もがこのようなコードを書くのでしょうか?

有名なDuff's Deviceの改変版。オリジナルはメモリマップドIOのハードウェアにて周辺機器への書き込みに用いるコードだったため、変数toをインクリメントしない。このコードは、メモリからメモリへのデータコピー用に改変されたものになる。合法的なコードである。

このコードの意図は、まず第一にループ展開による高速化である。doループ中でコピーを8回行うことで、継続条件での比較演算の回数が約8分の1に減るため、処理の高速化が期待できる。第二に、ループ展開したときに生じる端数分のコピー処理への巧妙な対応である。コピーするバイト数がループ展開するバイト数で割り切れない場合、まずswitch文にて0以外のcase文にジャンプして、FALLTHROUGHを利用して端数分のコピーを行い、その後do-while文の継続条件のチェックを経て、残りのコピーをループ展開されたdoループにて実施する。

ループ展開による高速化も、端数処理の巧妙な対応も、ある意味Cプログラマーらしいといえる気がしないでもない。というのも、このコードのアルゴリズムは、アセンブラにて比較・分岐命令を最小限に抑えたコピー処理を記述したケースに非常に近い。かつてのシステム・プログラミングでは、現在よりも遥かに限られたハードウェア・リソースの下で、現在よりも最適化性能が低かったコンパイラを用いていたこともあり、高速な処理を実現するためにアセンブラ的な手法を採用することが多かった(現在でも、組み込み分野では同様の傾向がみられる)。

もっとも高速化の観点で言えば、現在ではこのコードには次のような問題がある:

  • オリジナルのDuff's Deviceならともかく、メモリからメモリへのコピーならmemcpy(3)の方が高速である可能性が高い。各環境に応じた最適化がなされているし、多数の人が使用している実績もある。
  • こういう巧妙なコードは、コンパイラが最適化し辛い。
  • 環境によっては端数処理を独立させて2つの処理に分けた方が高速らしい。
  • 環境によっては、CPUの高速化機能(パイプラインとか)がうまく機能しない可能性があるらしい。

13

14

次の2つの関数のプロトタイプ宣言は同じでしょうか?

int foobar(void);
int foobar();

違う(同じではない)。

int foobar(void);ANSI C89で採用された関数プロトタイプ宣言であり、戻り値の型と、引数の型と順序(ここでは引数を何もとらないこと)がチェック対象となる。

一方、int foobar();はK&R以前で用いられていた関数の前方宣言である(プロトタイプ宣言ではない)。戻り値の型のみがチェック対象となり、引数の型と順序(この例では、引数をとらないこと)はチェック対象外となる。

(宣言が可視であるなら)前者の形式を用いれば、コンパイル時に誤って引数に値を指定しているコードが発見され、警告が出力されるはずである。一方で後者の形式では、誤って引数に値を指定したコードを記述してしまった際、コンパイル時に警告されることはない。そのようなプログラムを実行すると、何も問題が起こらないかもしれないし、原因不明のエラーが発生して散々な目に遭うかもしれない。

なお上記の解釈は規格Cにもとづく。C++では、この2つの宣言はどちらも同じ意味を持つ関数プロトタイプ宣言である。

15

次のプログラムからは、どのような出力が得られるでしょうか? また、その理由も答えてください。

#include <stdio.h>
int main()
{
 float a = 12.5;
 printf("%d\n", a);
 printf("%d\n", *(int *)&a);
 return 0;
}

12.5や小数部を除いた12のような値とは全く別物の、よく分からない値が出力される。2つの出力は異なるものである可能性が高い。

printf(3)のような可変長引数の関数は、プロトタイプ宣言による型チェックの対象外であり、仮引数への代入時に型変換が行われることもない。すなわち、上記コードにおけるprintf(3)では、変換指定にて%d(int型)を期待しているものの、当該変換指定に対応する引数の値が暗黙のうちにint型に変換されるようなことはない。与えられた変数の型そのままに、printf(3)に引き渡される。

また、可変長引数ではK&Rの頃にはお馴染みだった昇格変換が行われる。そのため、char型やshort型の値はint型に、float型の値はdouble型に変換される。

printf("%d\n", a)では、変数aがdouble型に昇格変換された値が実引数としてprintf(3)に引き渡される。int型が32bit、double型が64bitの環境では、(double)12.5の64bitの表現のうち、下位アドレス側の32bitのビット列の並びをint型として解釈した値が出力される。

printf("%d\n", *(int *)&a)では、変数aのビット列の並びをint型として解釈したint型の値が実引数としてprintf(3)に引き渡され、それがそのまま出力される。int型もfloat型も32bitである環境だと仮定するとして、同じ32bitのビット列でもその解釈は大きく異なる。そのため、元の12.5や近似値の12とは似ても似つかぬ妙な値が出力されることになる。

16

次のプログラムは、2つのファイルに分けた小さなCプログラムです。これらのファイルをコンパイルし、実行したとき、このプログラムからはどのような出力が得られるでしょうか?

int arr[80];
extern int *arr;
int main()
{
    arr[1] = 100;
    return 0;
}

まず前提として、配列とポインタは別物である。つまりint arr[80]int *arrは別物である。よってextern int *arr;int arr[80];の参照(宣言)ではない。

このプログラムの挙動は、外部変数の定義出現と参照出現の区別についてコンパイラが採用しているモデルに依存する。あるコンパイラでは、int arr[80];extern int *arr;双方を参照とみなし、定義が存在しないためリンクに失敗するかもしれない。別のコンパイラでは、int arr[80];extern int *arr;双方を定義とみなし、同じ名前の定義が重複するためリンクに失敗するかもしれない。また別のコンパイラでは、int arr[80];を定義と、extern int *arr;を参照とみなし、定義と参照とで食い違いがあるためリンクに失敗するかもしれない。さらに別のコンパイラでは、int arr[80];を参照と、extern int *arr;を定義と見なし、int arr[80];を使用している形跡がみられないためコンパイル時に取り除き、リンクには成功するものの、実行時に静的オブジェクトのため初期化時にNULLになっているarrを参照して値を書き込もうとしてプログラムが落ちるかもしれない。

規格Cとしては「リンクに失敗」が正しいように思われるが、Visual Studio 2013やGCC 4.8.1では「プログラムが落ちる」となる。

なお興味深いことに、これがC言語ではなくC++のコードだった場合、GCC 4.8.1ではC言語の時と同じくプログラムが落ちるが、Visual Studio 2013ではリンクに失敗する。C++のモデルは記憶域クラス省略モデルと同じなので、この点についてはVisual Studio 2013の方が正しいように思われる。

17

次のCプログラムの出力を説明してください(出力は20ではありません)。

#include<stdio.h>
int main()
{
    int a=1;
    switch(a)
    {   int b=20;
        case 1: printf("b is %d\n",b);
                break;
        default:printf("b is %d\n",b);
                break;
    }
    return 0;
}

変数bの値として何が表示されるか分からない。

switch文にて最初のcase文よりも前の位置で変数の宣言と初期化を行い、case文内で使用しようとした場合、その変数のスコープはswitch文の内側になるが、初期化が行われない。そのため、変数の値は不定となる。

これは、switch文での制御の移行がgotoに近いためである。要は、次のようなコードと等価だと考えれば分かりやすい。

#include<stdio.h>
int main()
{
    int a=1;
    if(a == 1)
        goto CASE_1;
    else
        goto CASE_DEFALUT;
    {   int b=20;
CASE_1:         printf("b is %d\n",b);
                goto END;
CASE_DEFALUT:   printf("b is %d\n",b);
                goto END;
    }
END:
    return 0;
}

int b=20;を実行することなくgotoで飛び越えてしまった。ではbはどうなるのか? C言語では、変数b自体は定義されるが、初期化のコードは実行されない。仮にインタプリタ的に逐次実行する言語だったとしたら、変数の定義すらされず、結果として未定義の変数bを参照することになっていただろう。

なお、この挙動はC++でも同様だが、C++ではコンパイラのチェックが厳しいため、コンパイルエラーとなる。おそらくC++ではブロック先頭以外で変数の宣言と初期化が可能であるため、同様の問題が通常の変数宣言とgoto文でも起こるからだろう。

static void f(const bool b)
{
    using std::cout;
    using std::endl;

    if (b)
        goto FOO;

    int a = 1;

FOO:
    cout << a << endl;
}

18

次のプログラムからは、どのような出力が得られるでしょうか? (整数のサイズが4の場合、出力は40ではありません)

#define SIZE 10
void size(int arr[SIZE])
{
        printf("size of array is:%d\n",sizeof(arr));
}

int main()
{
        int arr[SIZE];
        size(arr);
        return 0;
}

一般的に、32bit OSではsize of array is:4と、64bit OSではsize of array is:8と表示される。要するにint型のポインタの大きさである。

関数の仮引数で宣言した配列は、ポインタに変換される。つまりvoid size(int arr[SIZE])void size(int *arr)と等価である。そのため、sizeof(arr)はint型のポインタの大きさを返す。

19

次のプログラムは、エラーを表示するために、Errorと呼ばれる関数を利用した簡単なCプログラムです。Errorが定義されている方法で、考えられる問題はどんなことでしょうか?

#include <stdlib.h>
#include <stdio.h>
void Error(char* s)
{
    printf(s);
    return;
}

int main()
{
    int *p;
    p = malloc(sizeof(int));
    if(p == NULL)
    {
        Error("Could not allocate the memory\n");
        Error("Quitting....\n");
        exit(1);
    }
    else
    {
        /*some stuff to use p*/
    }
    return 0;
}

思いつく問題は4つ。

1つ目は、エラーメッセージをprintf(3)の第1引数として実行しているため、意図せず書式指定文字列と解釈されうるメッセージを表示させようとしてしまった時、プログラムの異常終了を引き起こすなどの問題が起こる可能性があること。特に、デバッグ用に外部由来のデータを表示させるために使っているならば、書式指定文字列を使用した攻撃が可能となってしまう。非常に危険である。

2つ目は、これはプログラムの内容や使い方次第だが、標準出力はバッファリングを行っているため、Error()を実行してから実際に出力されるまでにタイムラグが発生する可能性があること。即座に出力したいなら、fflush(3)を使うか、setbuf(3)/setvbut(3)でバッファリングを無効化しておくこと。

3つ目は、エラーの出力先が標準出力であること。正常時に標準出力に何かしら出力するのならば、エラーメッセージは標準エラー出力に出力したほうがよい。でなければ、正常な出力とエラーメッセージが混ざってしまう。

4つ目は、引数の型がchar *であること。実行時に引数に文字列定数を指定した時、コンパイラによっては毎度毎度警告がでて煩わしい。const char *にすべき。

という訳で、上記全てを踏まえて、最終的に、私ならこう直す。

void Error(const char * const s)
{
    assert(s != NULL);
    fprintf(stderr, "%s", s);
    return;
}
void Error(const char * const s)
{
    assert(s != NULL);
    fputs(s, stderr);
    return;
}

20

Scanfscanfを使った次の2つの関数の呼び出しの違いは何でしょうか? (2番目の関数の呼び出しでは、スペースがあることに注意しましょう。スペースを削除したときのプログラムの動きを観察してみてください)

#include <stdio.h>
int main()
{
    char c;
    scanf("%c",&c);
    printf("%c\n",c);

    scanf(" %c",&c);
    printf("%c\n",c);

    return 0;
}

まず前提として、scanf(3)の変換指定の大半においては、変換に先立ち先頭の空白文字を読み飛ばす(後続の空白文字は読み飛ばさない)。空白文字には、半角スペースやタブ以外に、改行も含まれる。

ただし例外として、cと[においては先頭の空白文字の読み飛ばしを行わない。

次に、scanf(3)のフォーマット文字列中に書かれた空白文字は、それが1文字だけであっても、1文字以上の連続する空白文字の読み飛ばしを意味する。読み飛ばしを行った後に存在する、空白文字以外の任意の文字が、フォーマット文字列中の空白文字に続く変換指定にて読み込まれる文字となる。

上記にもとづいてサンプルコードを解釈すると、scanf("%c",&c);は空白文字を含む任意の1文字(1byte)を読み込む。scanf(" %c",&c);は0文字以上の連続する空白文字を読み飛ばした後に出現するだろう、空白文字を除くの任意の1文字(1byte)を読み込む。

21

次のCプログラムで、考えられる問題はどんなことでしょうか?

#include <stdio.h>
int main()
{
    char str[80];
    printf("Enter the string:");
    scanf("%s",str);
    printf("You entered:%s\n",str);

    return 0;
}

思いつく問題は2つ。

1つ目は、配列strの大きさを超える入力があった場合にバッファオーバーフローを起こすこと。対策としてはscanf(3)の変換指定に"%79s"のようにフィールド幅を追加することが考えられるが、読み込められなかった残りの入力をどう扱うか、仕様を検討する必要がある。また、UTF-8のような可変長な文字コード体系を採用している場合、単純にバッファの大きさで制限をかけると、バッファ終端側の文字が不正な値となる可能性があるので、適切な切り詰め処理を行う必要もあるだろう。

2つ目は、何も入力されなかった(いきなりEOFに到達した)場合に、未初期化で不定値が入っている状態のままの配列strの中身を表示しようとしてしまうこと。運が良ければ、途中でヌル文字と解釈されうる値に遭遇することなくprintf(3)で表示し続けようとしてしまい、本来参照するべきでないメモリを参照しようとしてプログラムが落ちるだろう。しかし運が悪ければ、偶然ヌル文字と解釈されうる値が配列strの先頭要素の不定値として存在していて、問題があることに気づかないまま見過ごしてしまうだろう。

22

次のプログラムからは、どのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
    int i;
    i = 10;
    printf("i : %d\n",i);
    printf("sizeof(i++) is: %d\n",sizeof(i++));
    printf("i : %d\n",i);
    return 0;
}

sizeof(int) == 4が成立する環境だと仮定すると、こんな感じ。

i : 10
sizeof(i++) is: 4
i : 10

sizeof演算子オペランド(被演算子)に記述した式は、その型が可変長配列型ならば評価されるが、それ以外の型の場合は評価されない。

式が評価されないということは、式の評価によって生じる副作用も発生しない、ということだ。

sizeof(i++)オペランドであるiは可変長配列型ではないため、式の評価は発生しない。そのため、副作用によるインクリメントは起こらない。よって、この式の前後にて変数iの値は変化しない。

23

次のプログラムでは、なぜ警告が発せられるのでしょうか? (constポインタを必要とする関数に対して通常のポインタを送ることについて、警告は発せられません)

#include <stdio.h>
void foo(const char **p) { }
int main(int argc, char **argv)
{
        foo(argv);
        return 0;
}

この問題で話題にしている警告は、おそらくchar **const char **の型が異なるという指摘だろう、と推測して書く(警告自体は、それ以外に2種類計3つほど発せられる可能性がある*1)。

といっても、警告の通り型が違うだけだ。

char **型は、「『[char型の、変更可能なオブジェクト]を指し示すポインタ型の、変更可能なオブジェクト』を指し示すポインタ型の、変更可能なオブジェクト」を意味する。

一方、const char **型は、「『[char型の、constな(変更不可能な/読み込み専用な)オブジェクト]を指し示すポインタ型の、変更可能なオブジェクト』を指し示すポインタ型の、変更可能なオブジェクト」を意味する。

注意すべきなのは、『』で囲んだ部分に記述した型が同じなら警告はでないが、異なる場合には警告がでる、ということだ。

  • [char型の、変更可能なオブジェクト]を指し示すポインタ型
  • [char型の、constな(変更不可能な/読み込み専用な)オブジェクト]を指し示すポインタ型

要するにポインタ型として指し示す先のオブジェクトの型が違う。例えるならint *(『int型の、変更可能なオブジェクト』を指し示すポインタ型の、変更可能なオブジェクト)とunsigned int *(『unsigned int型の、変更可能なオブジェクト』を指し示すポインタ型の、変更可能なオブジェクト)ぐらいに違う。

char **型のオブジェクトを代入しても警告されないのは、char * const *型かchar ** const型かchar * const * const型である。

char * const *型は、「『[char型の、変更可能なオブジェクト]を指し示すポインタ型の、constな(変更不可能な/読み込み専用な)オブジェクト』を指し示すポインタ型の、変更可能なオブジェクト」を意味する。

char ** const型は、「『[char型の、変更可能なオブジェクト]を指し示すポインタ型の、変更可能なオブジェクト』を指し示すポインタ型の、constな(変更不可能な/読み込み専用な)オブジェクト」を意味する。

char * const * const型については省略する。

この3つとも、『』で囲んだ部分に記述した型は「[char型の、変更可能なオブジェクト]を指し示すポインタ型」になる。型が同じであるため、警告はでない。

ちなみに、よく見かけるchar *型とconst char *型について書くなら:

  • char *型:「『char型の、変更可能なオブジェクト』を指し示すポインタ型の、変更可能なオブジェクト」
  • const char *型:「『char型の、constな(変更不可能な/読み込み専用な)オブジェクト』を指し示すポインタ型の、変更可能なオブジェクト」

『』で囲んだ部分の型は、どちらもchar型である(なので警告はでない)。

なお、この解答とは別の解答がcomp.lang.cのC FAQ 11.10に書かれている。

24

次のプログラムからは、どのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
        int i;
        i = 1,2,3;
        printf("i:%d\n",i);
        return 0;
}

画面にi:1と表示される。

ステートメントi = 1,2,3;は、コンマ演算子の存在と、単純代入演算子の方が優先順位が高いことより、次の3つの式が順番に評価されたかのように振る舞う。

  1. i = 1
  2. 2
  3. 3

よって変数iには1が代入される。

ちなみにi = (1,2,3);の場合は、式1,2,3内の3つの式を左から順に評価し、先頭側の式12の評価結果を捨てて、最後の式3を評価した結果である値3が、変数iに代入される。

25

次のプログラムは、逆ポーランド計算機を実装するコードの一部です。このコードには重大なバグがあります。それを見つけてください。getop関数は、operandsやopcodes、EOFなどに対する妥当な戻り値を返すと想定してください。

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

#define MAX 80
#define NUMBER '0'

int getop(char[]);
void push(double);
double pop(void);
int main()
{
    int type;
    char s[MAX];

    while((type = getop(s)) != EOF)
    {
        switch(type)
        {
            case NUMBER:
                push(atof(s));
                break;
            case '+':
                push(pop() + pop());
                break;
            case '*':
                push(pop() * pop());
                break;
            case '-':
                push(pop() - pop());
                break;
            case '/':
                push(pop() / pop());
                break;
            /*   ... 
             *   ...    
             *   ... 
             */
        }
    }
}

思いつく問題は3つ。最後の1つは余分かもしれないが。

1つ目は、演算子がきた時にpush(pop() + pop());のように関数pop()を2回実行しているが、pop()が呼び出される順番が不定であること(つまり、左から右の順に呼ばれるかもしれないし、右から左の順に呼ばれるかもしれない、ということ)。減算や除算にて誤った結果が得られてしまう危険がある。

2つ目は、除算の時に除数が0だった場合(ゼロ除算)のことを考えていないこと。

3つ目は、スタックが空の状態でpop()が呼ばれるようなケースを想定していないこと。

26

次のプログラムは、ほとんどのUNIX系システムで使用されているbannerコマンドの最小版を実装する簡単なプログラムです。プログラムで使用されているロジックを挙げてください。

#include<stdio.h>
#include<ctype.h>

char t[]={
    0,0,0,0,0,0,12,18,33,63,
    33,33,62,32,62,33,33,62,30,33,
    32,32,33,30,62,33,33,33,33,62,
    63,32,62,32,32,63,63,32,62,32,
    32,32,30,33,32,39,33,30,33,33,
    63,33,33,33,4,4,4,4,4,4,
    1,1,1,1,33,30,33,34,60,36,
    34,33,32,32,32,32,32,63,33,51,
    45,33,33,33,33,49,41,37,35,33,
    30,33,33,33,33,30,62,33,33,62,
    32,32,30,33,33,37,34,29,62,33,
    33,62,34,33,30,32,30,1,33,30,
    31,4,4,4,4,4,33,33,33,33,
    33,30,33,33,33,33,18,12,33,33,
    33,45,51,33,33,18,12,12,18,33,
    17,10,4,4,4,4,63,2,4,8,
    16,63
    };

int main(int argc,char** argv)
{

    int r,pr;
    for(r=0;r<6;++r)
        {
        char *p=argv[1];

        while(pr&&*p)
            {
            int o=(toupper(*p++)-'A')*6+6+r;
            o=(o<0||o>=sizeof(t))?0:o;
            for(pr=5;pr>=-1;--pr)
                {
                printf("%c",( ( (pr>=0) && (t[o]&(1<<pr)))?'#':' '));

                }
            }
        printf("\n");
        }
    return 0;
}

(先に書いておくと、while(pr&&*p)は間違い。正しくはwhile(p&&*p)。原文由来のバグだ。)

このプログラムは、アルファベットを全て大文字に変換した上で、コンソール上に6×6の大きさで表示する。アルファベット以外は空白として扱われる。

$ ./a.out abc
  ##   #####   ####  
 #  #  #      #    # 
#    # #####  #      
###### #    # #      
#    # #    # #    # 
#    # #####   ####  
$ ./a.out a1c
  ##           ####  
 #  #         #    # 
#    #        #      
######        #      
#    #        #    # 
#    #         ####  
$ _

Bの変換が、微妙に間違っている気がする。

配列tは英大文字の6×6のドットを意味するビット集合だ。t[0]からt[5]は空白を表示するためのデータで、t[6]からt[11]がAで、以降B、C……という具合に続いていく。参考までに、A・B・Cのデータの持ち方は、こんな感じ。

t[ 6] | 12 | 0b001100 |   11   |
t[ 7] | 18 | 0b010010 |  1  1  |
t[ 8] | 33 | 0b100001 | 1    1 |
t[ 9] | 63 | 0b111111 | 111111 |
t[10] | 33 | 0b100001 | 1    1 |
t[11] | 33 | 0b100001 | 1    1 |

t[12] | 62 | 0b111110 | 11111  |
t[13] | 32 | 0b100000 | 1      |
t[14] | 62 | 0b111110 | 11111  |
t[15] | 33 | 0b100001 | 1    1 |
t[16] | 33 | 0b100001 | 1    1 |
t[17] | 62 | 0b111110 | 11111  |

t[18] | 30 | 0b011110 |  1111  |
t[19] | 33 | 0b100001 | 1    1 |
t[20] | 32 | 0b100000 | 1      |
t[21] | 32 | 0b100000 | 1      |
t[22] | 33 | 0b100001 | 1    1 |
t[23] | 30 | 0b011110 |  1111  |

各要素の下位6bitについて、1bitを1dotとして、描画するなら1を、描画しないなら0を保持している。6×6なので、配列の要素6個で1文字分のデータとなる。

──と、こんな感じにデータ構造について説明した時点で、ステートメント・レベルの細かい説明までしなくても十分な気がしてきた。だって、これを見れば(そして、このデータ構造を念頭に、コードを読めば)、何となく分かるでしょう?

あえていうなら、whileループの中のforループは、6×6のドットの横方向(左から右)のプロットの処理になるが、6回ではなく7回ループしている。最後のループでは必ず空白が出力される。これによって、次に描画する文字との間に必ず1ドット分の空白が入ることになる。

27

次のプログラムからは、どのような出力が得られるでしょうか?

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

#define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0]))

#define PrintInt(expr) printf("%s:%d\n",#expr,(expr))
int main()
{
    /* The powers of 10 */
    int pot[] = {
        0001,
        0010,
        0100,
        1000
    };
    int i;

    for(i=0;i<SIZEOF(pot);i++)
        PrintInt(pot[i]);
    return 0;
}

こんな感じ。

pot[i]:1
pot[i]:8
pot[i]:64
pot[i]:1000

000100100100は、0で始まる定数なので、8進数として解釈される。10進数に変換すると、0010は8に、0100は64になる。

28

29

次のプログラムからは、どのような出力が得らえるでしょうか? (10ではありません)

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int main()
{
    int y = 100;
    int *p;
    p = malloc(sizeof(int));
    *p = 10;
    y = y/*p; /*dividing y by *p */;
    PrintInt(y);
    return 0;
}

画面にy : 100と表示される。

y/*pと、識別子の間にスペースを入れなかったのが敗因。コメントの開始(/*)と解釈されてしまった結果、y = y/*p; /*dividing y by *p */;というステートメントは、コメント部分を除くとy = y;となる。

まあ、この手の問題はシンタックス・ハイライト機能を持つテキストエディタを使用していれば気づきやすい。コンパイラの警告レベルや警告内容を気にする人も、気づく可能性は高いだろう。

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int main()
{
    int y = 100;
    int *p;
    p = malloc(sizeof(int));
    *p = 10;
    y = y/*p; /*dividing y by *p */;
    PrintInt(y);
    return 0;
}

30

次のプログラムは、日付を読み込んだり、出力したりするための簡単なCプログラムです。これを実行して、その振る舞いを説明してください。

#include <stdio.h>
int main()
{
    int day,month,year;
    printf("Enter the date (dd-mm-yyyy) format including -'s:");
    scanf("%d-%d-%d",&day,&month,&year);
    printf("The date you have entered is %d-%d-%d\n",day,month,year);
    return 0;
}

「振る舞いを説明してください」といっても、何も特別な点はない気がするが……。

このプログラムは31-12-2015のような形式での日付入力を期待し、入力データから年・月・日をint型で個別に取り出し、再び同じような形式で出力する。

ただ、入力値の桁数を制限していないため、例えば2015-120-31を許容してしまう。また負の整数も許容してしまうため、-31--12--2015もOKだったりする。

あとscanf(3)の書式指定は、cと[以外では、変換に先立ち先頭の連続する空白文字を読み飛ばすので、 31- 12- 2015 -31- -12- -2015も許容する。

途中までしか(または全く何も)変換できなかった場合への対応もされていない。この場合、変数daymonthyearのうち変換できなかった項目を取得する予定だったものには何も代入されない。これらの変数は初期化もされていないので、最後のprintf(3)にてゴミの値が表示されてしまう。

空白文字の件を除けば、scanf(3)の書式指定として"%2u-%2u-%4u"を用いて、年・月・日をunsigned int型で取り出すようにした上で、scanf(3)の戻り値をチェックすれば、もう少し厳密な判定となるだろう。

31

次のプログラムは、整数を読み込んだり、出力したりするための簡単なCプログラムです。しかし、正しく動きません。問題を見つけてください。

#include <stdio.h>
int main()
{
    int n;
    printf("Enter a number:\n");
    scanf("%d\n",n);

    printf("You entered %d \n",n);
    return 0;
}

scanf()の使い方が間違っている。scanf("%d\n",&n);が正しい。

32

次のプログラムは、ビット演算子を使って整数を5倍にさせる簡単なCプログラムです。しかし、5倍になりません。プログラムが誤った振る舞いをしてしまう理由を説明してください。

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int FiveTimes(int a)
{
    int t;
    t = a<<2 + a;
    return t;
}

int main()
{
    int a = 1, b = 2,c = 3;
    PrintInt(FiveTimes(a));
    PrintInt(FiveTimes(b));
    PrintInt(FiveTimes(c));
    return 0;
}

シフト演算子よりも+演算子の方が優先順位が高いため、a<<2 + aa<<(2 + a)という風に評価されてしまう。(a<<2) + aという風に括弧を付け加えること。

33

次のプログラムは、正しく動くでしょうか?

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int max(int x, int y)
{
    (x > y) ? return x : return y;
}

int main()
{
    int a = 10, b = 20;
    PrintInt(a);
    PrintInt(b);
    PrintInt(max(a,b));
}

コンパイルすら通らない(はず)。

(x > y) ? return x : return y;return (x > y) ? x : y;に書き直すこと。三項演算子はあくまでも演算子であり、ifのような制御構文ではない。なので文法的に、式を埋め込むことは可能だが、ステートメント(文)を埋め込むことはできない。

34

次のプログラムは、マイナス記号を20回出力するように指示したCコードの一部です。お分かりだと思いますが、これでは動きません。

#include <stdio.h>
int main()
{
    int i;
    int n = 20;
    for( i = 0; i < n; i-- )
        printf("-");
    return 0;
}

このコードを修正するのはとても簡単です。問題が面白くなるように、こうしたいと思います。1文字だけ変えてこのコードを修正してみてください。答えは3つあります。さて、すべて答えられますか?

解答その1:変数nをデクリメントする。

#include <stdio.h>
int main()
{
    int i;
    int n = 20;
    for( i = 0; i < n; n-- )
        printf("-");
    return 0;
}

これは分かりやすい。

解答その2:for文の継続条件を-i < nにする。

#include <stdio.h>
int main()
{
    int i;
    int n = 20;
    for( i = 0; -i < n; i-- )
        printf("-");
    return 0;
}

これも分かりやすいかも。単項演算子-で変数iの符号を反転させることで、デクリメントで負の値になっているiが継続条件では正の値とみなされる。つまり継続条件での比較演算においては、実質的にiがインクリメントされているに等しい。

解答その3:for文の継続条件をi + nにする。

#include <stdio.h>
int main()
{
    int i;
    int n = 20;
    for( i = 0; i + n; i-- )
        printf("-");
    return 0;
}

iがデクリメントされていくと、最終的にi + n-20 + 20となり、評価結果が0になってfor文を終了する。

解答その4:for文の継続条件を~i < nにする。ただし1の補数で負の値を表現している環境限定。まあ、そんな環境に出くわしたことはないのだが……C言語の規格では負の値をどう表現するかについての定義されていない訳で、PDP-1のような古いコンピュータ*2なら、もしかしたら……。

#include <stdio.h>

int main(void)
{
    int i;
    int n = 20;
    for( i = 0; ~i < n; i-- )
        printf("-");
    return 0;
}

1の補数を採用している環境でiをビット反転させる(1の補数に変換する)と、-1が1に、-2が2になる。つまり、最終的に~i < n20 < 20となり、評価結果が0になってfor文を終了する。

なお、残念ながら2の補数を採用している環境では、ビット反転させることで、-1が0に、-2が1に、という風になり、結果としてマイナス記号が21回出力されてしまう。現在のコンピュータの大半では2の補数が採用されているので、つまりこの解答は大抵の環境では正しく動作しない。

35

次のコードは何が間違っているのでしょうか?

#include <stdio.h>
int main()
{
    int* ptr1,ptr2;
    ptr1 = malloc(sizeof(int));
    ptr2 = ptr1;
    *ptr2 = 10;
    return 0;
}

変数ptr2はint型である。int型のポインタではない。そのためデリファレンスできない。ポインタとして宣言したいならば、int *ptr1, *ptr2;int* ptr1; int* ptr2;などのように宣言すること。

36

次のプログラムからはどのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
    int cnt = 5, a;

    do {
        a /= cnt;
    } while (cnt --);

    printf ("%d\n", a);
    return 0;
}

変数aの値が表示される前に、a /= cnt;にてゼロ除算が発生する。ループの最後の週にて、変数cntの値が0になっているからである。

37

次のプログラムからはどのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
    int i = 6;
    if( ((++i < 7) && ( i++/6)) || (++i <= 9))
        ;
    printf("%d\n",i);
    return 0;
}

画面に8と表示される。

if文の条件式にて、まず++i < 7が評価され、7 < 7で偽となる。次に、論理演算子の短絡評価より、++i <= 9が評価され、8 <= 9で真となる。++iが2回実行されるため、初期値の6から2増加した8になる。

なお論理AND演算子や論理OR演算子は副作用完了点であるため、上記コードは副作用が複数回発生する未定義のコードには該当しない。

38

次のプログラムに含まれるバグは何でしょうか?

#include <stdlib.h>
#include <stdio.h>
#define SIZE 15 
int main()
{
    int *a, i;

    a = malloc(SIZE*sizeof(int));

    for (i=0; i<SIZE; i++)
        *(a + i) = i * i;
    for (i=0; i<SIZE; i++)
        printf("%d\n", *a++);
    free(a);
    return 0;
}

free(a);がNG。直前のforループにてポインタaをインクリメントしているため、本来free(3)する時に指定するべきアドレスからint型変数15個分ほど離れた場所を指定してfree(3)してしまっている。

39

次のCプログラムは正しく動くでしょうか? もし動く場合、どのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
  int a=3, b = 5;

  printf(&a["Ya!Hello! how is this? %s\n"], &b["junk/super"]);
  printf(&a["WHAT%c%c%c  %c%c  %c !\n"], 1["this"],
     2["beauty"],0["tool"],0["is"],3["sensitive"],4["CCCCCC"]);
  return 0;
}

非常に残念ながら、C言語の文法として合法なプログラムで、コンパイルも実行も可能である。

ちなみに実行するとこんな感じ。

Hello! how is this? super
That  is  C !

上記のコードは、次のコードとだいたい同じだ。

#include <stdio.h>
int main()
{
  int a=3, b = 5;

  printf(&"Ya!Hello! how is this? %s\n"[a], &"junk/super"[b]);
  printf(&"WHAT%c%c%c  %c%c  %c !\n"[a], "this"[1],
     "beauty"[2],"tool"[0],"is"[0],"sensitive"[3],"CCCCCC"[4]);
  return 0;
}

C言語の配列の添字演算子[]のオペランドは交換可能であるため、X[Y]をY[X]と記述することが可能だ。可能だが、せいぜいIOCCCネタになるぐらいだ。

この辺のことをもう少し詳しく知りたいなら、comp.lang.cのC FAQ 6.11を参照すること。

40

次のプログラムで、Life is beautifulと入力した場合、どのような出力が得られるでしょうか?

#include <stdio.h>
int main()
{
    char dummy[80];
    printf("Enter a string:\n");
    scanf("%[^a]",dummy);
    printf("%s\n",dummy);
    return 0;
}

画面にLife is beと表示される。

[はscanf(3)の書式指定の1つで、[]で囲んだ中に記述した文字の集合を読み込むか、前述のコードのように[^]とした場合には中に記述した文字以外の集合を読み込む。

つまり、"%[^a]"は「a以外の文字の集合を読み込む」という意味となる。「Life is beautiful」の先頭から始まる、a以外の文字の集合──先頭から、aが出現する直前の文字までが読み込まれることになる。

41

注記 :これはC言語というより、リンカに関連した問題です。
次のようなa.c、b.c、main.cという3つのファイルがあるとします。

a.c

int a;

b.c

int a = 10;

main.c

extern int a;
int main()
{
        printf("a = %d\n",a);
        return 0;
}

これらのファイルを一緒にコンパイルするとどうなるか見てみましょう。

bash$ gcc a.c b.c main.c
bash$ ./a.out
a = 10

うーん、リンカエラーが発生してコンパイルできません! なぜでしょうか?

原文が「Hmm!! no compilation/linker error!!! Why is it so??」なので、「コンパイルエラーもリンカエラーも起きないぞ! なんで?」という感じだと思われる。

外部変数の定義出現と参照出現だが、規格C的には、a.cのint a;は参照とみなされ、b.cのint a = 10;は定義とみなされ、main.cのextern int a;は参照とみなされる。つまり定義が1ヶ所と解釈されるため、何の問題もない。

ちなみに、外部変数の定義出現と参照出現の区別について、規格Cのモデルと、実際にコンパイラが採用しているモデルは異なる。コンパイラが採用しているモデルには以下の4種類がある。

  1. 初期化子モデル
  2. 記憶域クラス省略モデル
  3. COMMONモデル
  4. 折衷COMMONモデル

よく分からないが、多分gccは上記3ないし4を採用していると思われる。この2モデルは規格Cのモデルに近いため、先に書いた規格Cのモデルに基づく解釈と同様の結果となったと思われる。

蛇足だが、C++は記憶域クラス省略モデルを採用している。記憶域クラス省略モデルでは、a.cのint a;とb.cのint a = 10;の両方とも定義と見なされるため、リンク時に変数aの多重定義でエラーとなる。

42

次のマクロはオフセットを返すために、よく使われているものです。このマクロの処理内容と、これを使用するメリットを挙げてください。

#define offsetof(a,b) ((int)(&(((a*)(0))->b)))

C言語の標準ライブラリstddef.hに定義されているマクロoffsetofとほぼ同等である。マクロの中身自体は、処理系によって異なる(かもしれない)が……。あと、標準ライブラリの方ではsize_t型の値を返す式に展開されることになっているが、このサンプルコードではint型になる。

何をやるマクロかというと、構造体のメンバのオフセット(構造体の先頭アドレスから何byte離れた位置にあるか?)を、その構造体の変数を実際に定義してアドレス値から計算することなく求めるものだ(ただしビットフィールドを除く)。

使い方の例:

#include <stddef.h>
#include <stdio.h>

int main(void)
{
    struct st {
        char a;
        short b;
        int c;
        double d;
        char e;
        char f;
        long g;
    };
    printf("%u\n", (unsigned) offsetof(struct st, a));
    printf("%u\n", (unsigned) offsetof(struct st, b));
    printf("%u\n", (unsigned) offsetof(struct st, c));
    printf("%u\n", (unsigned) offsetof(struct st, d));
    printf("%u\n", (unsigned) offsetof(struct st, e));
    printf("%u\n", (unsigned) offsetof(struct st, f));
    printf("%u\n", (unsigned) offsetof(struct st, g));
    return 0;
}

Ubuntu 12.04.5 LTS(32bit)での実行例。

$ uname -a
Linux fabrico 3.2.0-88-generic-pae #126-Ubuntu SMP Mon Jul 6 21:47:47 UTC 2015 
i686 i686 i386 GNU/Linux
$ ./a.out
0
2
4
8
16
17
20
$ _

構造体のメンバは、メモリ上に隙間なく詰め込まれてはいない(処理系依存の技を使えば別だが)。大抵は、CPUが効率的かつ何の問題もなくメモリアクセスできるように配置されるため、隙間が存在している。正しいオフセットを知りたいなら、offsetofを使うべきだろう。

43

次のコードは有名なTriple xor swapのマクロ実装です。

#define SWAP(a,b) ((a) ^= (b) ^= (a) ^= (b))

上記のマクロではどのような問題が起こり得るでしょうか?

副作用完了点に到達するまでに、同一のオブジェクトに対して副作用が複数回発生する、規格C的に未定義のコードに展開されるため、何が起きるか分からない――つまり、どのような問題が発生してもおかしくない、危険なコードである。

44

次のマクロの用途は何でしょうか?

#define DPRINTF(x) printf("%s:%d\n",#x,x)

シンボル(変数/マクロ定数/列挙子/int型を返す関数呼び出し)の名前と値を標準出力に出力する。マクロ名の通りデバッグ向け、それもソースデバッガを使わない場合向けである。

45

IAddOverFlowという関数のコーディングを依頼されたとしましょう。この関数は、結果を格納する整数型変数へのポインタ1つと、加算する整数2つ、合計3つのパラメータを取ります。戻り値は、オーバーフローがあれば0、なければ1とします。

int IAddOverFlow(int* result,int a,int b)
{
    /* ... */
}

あなたなら上記の関数をどうコーディングしますか? (要はオーバーフローの検知に、どのようなロジックを使うかという質問です)

アンダーフローのことも考慮するべきだろうか?

規格C的には符号付き整数のオーバーフローは未定義の動作なので、オーバーフローが発生したことを検知するのではなく、オーバーフローが発生する可能性を検知すべきだろう。

よって、例えばこんな感じだろうか。

int IAddOverFlow(int* result,int a,int b)
{
    assert(result != NULL);

    if (((a > 0) && ((INT_MAX - a) < b)) ||
            ((a < 0) && ((INT_MIN - a) > b))) {
        /* オーバーフロー/アンダーフローしそうなので加算しない */
        return 0;
    }
    *result = a + b;
    return 1;
}

未定義の動作を回避する目的があるため、オーバーフロー/アンダーフローが発生する可能性がある場合は、加算処理をしない。そのため、計算結果を返さないというか、返すことができない。

46

次のマクロの役割は何でしょうか?

#define ROUNDUP(x,n) ((x+n-1)&(~(n-1)))

2進数の切り上げ処理を行う。例えばこんな感じ。

ROUNDUP(0x24, 0x01) == 0x24 | ROUNDUP(0b0010_0100, 0b0000_0001) == 0b0010_0100 |
ROUNDUP(0x24, 0x03) == 0x24 | ROUNDUP(0b0010_0100, 0b0000_0011) == 0b0010_0100 |
ROUNDUP(0x24, 0x07) == 0x28 | ROUNDUP(0b0010_0100, 0b0000_0111) == 0b0010_1000 |
ROUNDUP(0x24, 0x0F) == 0x30 | ROUNDUP(0b0010_0100, 0b0000_1111) == 0b0011_0000 |
ROUNDUP(0x24, 0x1F) == 0x40 | ROUNDUP(0b0010_0100, 0b0001_1111) == 0b0100_0000 |
ROUNDUP(0x24, 0x3F) == 0x40 | ROUNDUP(0b0010_0100, 0b0011_1111) == 0b0100_0000 |
ROUNDUP(0x24, 0x7F) == 0x80 | ROUNDUP(0b0010_0100, 0b0111_1111) == 0b1000_0000 |

引数nで切り上げ対象とする2進数の桁数を(桁数そのものではなく)ビットマスクで指定すると、その範囲にてビットが立っているなら、切り上げ対象桁数+1の桁に0b01がキャリーオーバーされる。

47

Cプログラミングの本では大抵、マクロの定義として次のような例を挙げています。

#define isupper(c) (((c) >= 'A') && ((c) <= 'Z'))

しかし上記のマクロ定義を次のように使用した場合、重大な問題が発生します。(その問題とは何でしょうか?)

char c;
/* ... */
if(isupper(c++))
{
    /* ... */
}

実際には大半のライブラリで、(types.hで宣言されている)isupperをマクロとして(特に副作用なく)実装しています。あなたのシステムでは、どのようにisupper()を実装しているか調べてみましょう。

マクロ展開によって、

if((((c++) >= 'A') && ((c++) <= 'Z')))

――という風なコードに展開されるため、&&の右側の式で評価する値が、意図していた値とは別物になってしまう。

ちなみに&&は副作用完了点であるため、副作用が複数回発生する未定義のコードには該当しない。

Visual Studio 2013やTDM-GCCGCC 4.8.1)、glibcFreeBSDNetBSDのctype.hを軽く見た限り、isupper()は関数として定義されているか、マクロの場合でも__isctype()のような別の関数を1回呼び出す(その際isupper()の引数を1ヶ所だけ使用する)コードに展開されるようになっている。

48

関数の引数となる変数を指定するためにellipsis (…)が使われていることは、ご存知でしょうか。(printf関数のプロトタイプ宣言はどうなるでしょうか?) また、次の宣言の問題点は何でしょうか?

int VarArguments(...)
{
    /*....*/
    return 0;
}

原文が「I hope you know that ellipsis (...) is used to specify variable number of arguments to a function」なので、「関数にて可変長引数を定義するのに省略記号(...)が用いられることはご存知かと思います」という感じだと思われる。

実際に可変長引数を取り扱う関数を実装してみれば分かるが、va_start(3)の引数の1つとして可変長引数となる部分の直前の引数を渡す必要がある。そのため、可変長となる部分(≒関数の宣言にて省略機号を記述する部分)の直前に1つ以上の引数がなくてはならない。

あとこれは蛇足だが、可変長引数として実際にどの型の値がどの順番で何個渡されたかを知る標準的な方法が無いため、例えば引数の個数を渡すとか、printf(3)のフォーマット文字列のようなもの(どの型の引数が、どの順番で引数指定されているか、判断する材料となる代物)を渡すなどの工夫が必要となる。そのため、結局は可変長引数以外に1個以上の引数が必要となることが多い。

よって、例えば次のような宣言に変更する必要があるだろう。

/* 例1:可変長引数として全て同じ型の値をとる。
 *       引数の個数を渡すようにする(使い勝手は微妙かも)
 */
int VarArguments(const size_t nargs, ...)
{
    /*....*/
    return 0;
}
/* 例2:可変長引数として全て同じ型(ここではconst char *)の値をとる。
 *       常に 1個以上の引数を要求する。
 *       引数の終わりは番兵で判定する(execl(2)で最後にNULLを要求するのと同じ)
 */
int VarArguments(const char *first, ...)
{
    /*....*/
    return 0;
}
/* 例3:可変長引数として色々な型の値をとるので、
 *       どの型をどの順番で渡したか分かるようなミニ言語的な引数を渡すようにする。
 *       (printf(3)やscanf(3) のフォーマット文字列のようなもの)
 */
int VarArguments(const char *fmt, ...)
{
    /*....*/
    return 0;
}

49

3つの整数のうち最小のものを見つけるCプログラムを、比較演算子を一切使わずに書いてください。

あえて、この問題の作者が求めているものとは異なるだろう解答を書く。

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

#define NELEMS(ary) (sizeof(ary) / sizeof((ary)[0]))

static int smallest(const int a, const int b, const int c)
{
    return (int) fmin(fmin((double) a, (double) b), (double) c);
}

int main(void)
{
    static const int TESTDATA[][4] = {
        { -1,  0,  1, -1},
        {  0, -1,  1, -1},
        {  1,  0, -1, -1},
        {  1,  2,  3,  1},
        { -1, -2, -3, -3},
    };
    size_t i;

    for (i = 0; i < NELEMS(TESTDATA); i++) {
        int retval = smallest(TESTDATA[i][0], TESTDATA[i][1], TESTDATA[i][2]);
        printf("smallest(%d, %d, %d) == %d : %s\n",
               TESTDATA[i][0],
               TESTDATA[i][1],
               TESTDATA[i][2],
               retval,
               retval == TESTDATA[i][3] ? "OK" : "NG");
    }

    return 0;
}

C99以降専用かつmath.hを使用。C++ならstd::minを使うだろうなあ。この問題に限っていえば、ものすごくインチキな方法である。

50

printf関数の書式指定子%nはどう動くでしょうか?

printfの引数に指定したポインタが指し示す先の変数に、%nが出現するまでに出力した文字数が書き込まれる。

51

52

どのようにすればprintf関数を使って、I can print %と出力できるでしょうか?(%は書式指定子として使用されることを忘れないでください!)

解答その1:%をエスケープする。

printf("I can print %%\n");

解答その2:%sの引数にしてしまう。

printf("%s\n", "I can print %");

外部から取得したデータを出力するようなケースでは、セキュア・コーディング的には解答その2の手法が唯一の選択肢となるはず。

53

次の2つのC言語の宣言には、どのような違いがあるでしょうか?

const char *p;
char* const p;

const char *pは「『char型の、constな(変更不可能な/読み込み専用な)オブジェクト』を指し示すポインタ型の、変更可能な変数p」を意味する。p自体の値を変更することは可能だが、pが指し示す先のオブジェクトの値を変更することは許されていない。

const char *p;

/* p の値は変更してもよい */
p = "hello, world";
p++;
p--;

/* でも p が指し示す先の領域を変更したらNG */
#if 0
*p = 'H';
#endif

char* const p;は「『char型の、変更可能なオブジェクト』を指し示すポインタ型の、constな(変更不可能な/読み込み専用な)変数p」を意味する。p自体の値を変更することは許されていないが、pが指し示す先のオブジェクトの値を変更することは可能である。

char msg[] = "hello, world";
char* const p = msg;

/* p の値は変更できない */
#if 0
p++;
p--;
#endif

/* p が指し示す先の領域は変更してもよい */
*p = 'H';

54

memcpy と memmoveの違いは何でしょうか?

コピー元とコピー先のメモリ領域に重なり合っている部分がある際の挙動が異なる。memcpyでは未定義の動作となるが、memmoveの場合は正しく動作する。

あと、一般的にはmemcpyはmemmoveよりも高速であることが多い。

55

double型とfloat型の値を出力するprintf用の書式指定子は何でしょうか?

C95以前ならf、F、e、E、g、Gの6種類。C99以降ではa、Aを加えた8種類。厳密にはこれらはdouble型用の書式指定子だが、可変長引数ではfloat型の値がdouble型に昇格変換されるため、これらの書式指定子を使用して問題ない(そもそもfloat型専用の書式指定子は存在しない)。

ちなみにscanf系の関数の場合は、float型の場合はa、f、e、gの4種類、double型の場合はla、lf、le、lgの4種類となる。こちらは引数がポインタ型(float *、double *)なので昇格変換はされないというか、ポインタ経由で書き込む先の大きさが異なるので、書式指定子を分ける必要がある。

56

マシンのタイプがリトルエンディアンかビッグエンディアンかを判別する、小さなCプログラムを書いてください。

https://github.com/eel3/boc/blob/master/boc.cを参照のこと。

57

セミコロンを使わずにHello World!と出力するCプログラムを書いてください!

……ということは、returnを書かなくてOKということか。C99以降やC++なら清々しいまでに合法的だからなあ。C95以前だとグレーゾーンだと思うけど。

とりあえず、こんな感じで。

#include <stdio.h>

int main(void)
{
    if (puts("Hello World!")) {}
}

*1:使用するコンパイラや警告オプション次第だが、foo()の引数pやmain()の引数argcが未使用であることの警告と、foo()が外部公開される関数なのにプロトタイプ宣言が見当たらないという警告が、gccでは発せられる可能性がある。

*2:昔は1の補数を採用したコンピュータも多かったらしい。