誤り傾向

last update 2005, June 22, by tokuhisa


第1回

■ null文字忘れ

以下の例では,文字列 s2 の末尾に '\0' が代入されない.
void mystrcpy(unsigned char *s1, unsigned char *s2)
{
    while( *s1 != '\0' ){
        *s2++ = *s1++
    }
}

ちなみに,文字列の末尾に代入するnull文字は,'\0' を使います.NULL,0,0x00,であっても動作しますが,習慣に従う方が誤解が出ません.

また,文字列のコピーだけならば,次の方法でも構いませんが,やはり誤解の元ですので,避けたほうが良いと思います.
void mystrcpy(unsigned char *s1, unsigned char *s2)
{
    while( *s1++ = *s2++ ) {}
}

■ 2バイト文字の操作誤り

EUCコードは,2バイト文字なので,2バイトをまとめて操作しなければなりません.したがって,次のプログラムでは,たまたま動作していたものと言えます.
void printeucstr(unsigned char *s1)
{
    while( *s1 != '\0' ){
        if( *s1 & 0x80 )
            putchar( *s1++ );
    }
}

正しくは,次のようにすべきです.
void printeucstr(unsigned char *s1)
{
    while( *s1 != '\0' ){
        if( *s1 & 0x80 ){
            putchar( *s1++ );
            putchar( *s1++ );
        }
    }
}

課題4のように少し複雑になると誤動作の原因になります.EUCコードの定義に忠実なプログラムを書きましょう.

課題4で,0x80 で最初の条件分岐をせずに,0xa3 を使っている人は,動作確認で ".筧" を試してみてください.

■ ビット判定

8ビット目の判定に,割り算を使っている人がいました.
void printeucstr(unsigned char *s1)
{
    while( *s1 != '\0' ){
        if( *s1 / 128 ){
            putchar( *s1++ );
            putchar( *s1++ );
        }
    }
}

確かに動作はしますが,通常,割り算は処理コストが高いので使いません.また,8ビット目の判定をしているという意味が,読み手に伝わらないので,止めましょう.


第2回

■ s++ の使い方

以下の例では,文字列の先頭文字が検査されません.
while( *s++ != '\0' ){
  if( (*s & 0x80) != 0 ){
    - - -
  }
}

while 文の条件判断は,*s と '\0' を比較します.その直後に,s++ が実行されます.if (*s & 0x80) != 0 ) の段階では,s が2文字目を指しているので,先頭の文字が検査されません.

■ マジックナンバー

ソースコードに,直接書き込まれた数値は,意味がわかりません.これをマジックナンバーと言います.この方法は,あまり好まれません.
if( *s1 & 0x80 ) - - -

if( *s1 >= 0x30 ) - - -

*s++ = 0xa3;
*s++ = 0xb0 - 0x30 + *p++;

0x80 のように,8ビット目とビット演算の論理積をとることで,そのビットの値を調べることを,「マスクをかける」という言い方をします.マスクの定数としてソースコードを書く方法が好まれます.C言語では,#defineがよく使われます.

アスキー文字の場合は,シングルコートを使って表します.コンパイラは,1文字というよりは,1つの文字コードとみなして処理をします.

eucコードの片方のバイトのコードを使うときは,定数とする方法がありますが,単にコメントを書く場合もあります.
#define MASK8BIT 0x80

if( *s1 & MASK8BIT ) - - -

if( *s1 >= '0' ) - - -

*s++ = 0xa3;               /* 0xa3 = "0"のEUCコードの1バイト目 */
*s++ = 0xb0 - '0' + *p++;  /* 0xb0 = "0"のEUCコードの2バイト目 */

■ for 文の意味

for 文は,for(初期値;条件;繰り返し前の処理) の形式で使います.直感的にわかりやすいプログラムは,「初期値」,「条件」,「繰り返し前の処理」の3つの関係がわかりやすく書かれています.
int i = 0;

for(; s[i] != '\0'; i += 1 ){
  - - -
}

上記のような場合,i がループの中で1つずつ増加することを表します.i の初期値は,変数宣言時に与えるよりも,for 文で与えてください.読み手が,for文よりも上で,初期値が変化していることを心配するからです.
int i;

for( i = 0; s[i] != '\0'; i += 1 ){
  - - -
}

■ emacs の自動インデントの機能を使おう

emacs では,C言語のソースファイルに対しては,C言語の書式に合うようにインデントを入れる機能があります.この機能を使うには,拡張子が“c”となっているファイルをオープンしてください.新規でプログラムを書くときは,最初に「C-x C-f」によりファイル名を決めて下さい.

タブキーを押すと,適切なインデントが入ります.インデントが不適切な場合は,ソースプログラムに間違いがあることが多いです.たとえば,ダブルコーテーション「"」,やシングルコーテーション「'」,セミコロン「;」,閉括弧「}」の挿入ミスが考えられます.


第3回目

■ コメントの書き方

プログラムのコメントで書くべきことは,変数に込めた意味,サブルーチンの意味,想定している条件,といった意味を書く.
main()
{
  int d;       /* 日数 * 3 を表す */

  - - -
  reverse(line)  /* 1週分の文字列 line を左右反転する */
  - - -

  switch(d){  
  case 3:     /* カレンダーの最終週の日数が 1 日のとき */
    - - -
  case 6:     /* カレンダーの最終週の日数が 2 日のとき */
    - - -
  }
}

※ 上記は「いぢ悪カレンダー」の答えではありません.

■ sscanf の返り値と格納

sscanf や scanf の返り値は,フォーマットの指定通りに代入された変数の数を返り値としています.
  line = "    12 34";
  num = sscanf(line,"%d %d %d", &a, &b, &c);
上記のプログラムでは,num = 2 となります.変数 a, b, c のうち,左側から順に代入されるので,a = 12, b = 34 となります.c の値は不明です.なお,スペースは,区切とみなされ,無視されます.


第4回目

■ ヒント・画像ファイルのフォーマット(1次元配列と2次元配列)

通常,画像ファイルは2次元の座標系の上で扱います.C言語で2次元配列は扱えますが,画像ファイルのように,縦横の長さが可変の2次元配列をサブルーチンに渡すには,1次元配列でプログラムを作成するほうが簡単です.

2次元配列を1次元配列に変換するには,x + y * 幅 の式で1次元配列のインデックスを求めます.以下の形が基本的です.
void prog1(unsigned char *buff1, int sizex, int sizey)
{
  int x,y;

  for( y = 0 ; y < sizey ; y += 1){
    for( x = 0 ; x < sizex ; x += 1){
      buff1[x + y * sizex] = 代入値;
    }
  }
}

void main()
{
  unsigned char *buff1;
  int sizex, sizey;

  - - -
  buff1 = (unsigned char *)malloc(sizeof(unsigned char)*(sizex * sizey));
  prog1(buff1, sizex, sizey);
}

ここで,buff1[x + y * sizex] という指定は,書き込み先の1ピクセルごとに指定していることに注意してください.for 文により,範囲外への書き込みが発生しないことが一目でわかります.

■ メモリオーバーラン・上下反転のプログラムの場合

上記の例では,画像の縦のサイズは,sizey です.ところが,配列変数で使ってもよい値は,0 から数えるので,sizey - 1 までです.したがって,上下反転のプログラムでは,最下行を示すには,

  buff2[x + (sizey - 1 - y) * sizex] = 代入値
としなければなりません.y = 0 のとき,および,y = sizey - 1 のとき,配列のインデックス値が範囲を越えないか,検算してみましょう.決して
  buff2[x + (sizey - y) * sizex] = 代入値
ではありません

■ メモリオーバーラン・半分に縮小するプログラムの場合

画像の縦・横の長さをそれぞれ半分にするので,新しい幅 sizex2, sizey2 は,次の計算で求まります.

  sizex2 = sizex / 2;
  sizey2 = sizey / 2;

ここで,落し穴があります.sizex, sizey は int 型ですので,もし奇数の場合は,切り捨てられます.上記(1)の prog1 を使うと,代入先の配列変数のインデックスに合せて for 文が動作するので不都合は生じません.しかし,次のプログラムでは,問題が生じます.

  buff2 = (unsigned char *)malloc(sizeof(unsigned char)*(sizex2 * sizey2));
  for( y = 0 ; y < sizey ; y += 2)
    for( x = 0 ; x < sizex ; x += 2)
      buff2[x / 2 + ( y / 2 ) * sizex2] = 代入値

たとえば,sizex = sizey = 5 の場合,malloc で確保する領域は,

  5/2 * 5/2 = 2 * 2 = 4
です.つまり,buff2[0], buff2[1], buff2[2], buff2[3] の4つが使用できます.

しかし,for文では,y = 0, 2, 4,x = 0, 2, 4 の値をとることができるので,y = 4, x = 4のとき,

  buff2[2 + 2 * 2] = 代入値
すなわち,
  buff2[6] = 代入値
ということになり,明らかに配列の範囲を越えています.

第5回目

■ ポインタを使った線型リストでは,先頭のノードをダミーノードとして扱うこと

ポインタを使った線型リストでは,先頭のノードをダミーノードとして処理することが普通です.

先頭ノードをダミーノードとするメリットは, list_push(Node *head, int num) のプログラムの作成時に気がつくかもしれませんが,この関数内で処理が完結することです.

もし,先頭ノードがダミーノードでないならば,list_push では,新しい先頭ノードを返り値として返す必要があり,コールをした側でも返り値を先頭ノードに代入しなおす必要があります.
main()
{
  Node *head;

  head = NULL;
  head = list_push(head, x);
}

Node *list_push(Node *p, int x);
{
  Node *ans;

  ans = list_make_node(x);
  ans->next = p;
  return ans;
}
main()
{
  Node head;

  head.next = NULL;
  list_push(&head, x);
}

Node *list_push(Node *h, int x);
{
  Node *p;

  p = list_make_node(x);
  p->next = h->next;
  h->next = p
}

左側が,先頭ノードにデータを入れる場合のプログラム,右側が,先頭ノードをダミーとする場合のプログラムです.一見便利に見えますが,返値を他の目的で使いたい場合には,プログラム作成上の障害になります.右側のほうが,呼出し側(main関数内)が単純になります.

なお,課題3は,この決まりを逆手にとったものです.すなわち,リストの先頭は,大小の範囲の検査をしないことに着目しています.

■ if 文における比較の方法

よく,以下のようなプログラムを見かけます.

if( a = 1 )
  return 0;

if文の条件が,「a == 1」ではなく「a = 1」という代入になっている点に注意してください.

この誤りを防ぐには,2つの方法があります.

  1. gcc -Wall でコンパイルをする: もし代入をしていれば,警告を表示します.
  2. if( 1 == a ) のように即値を左辺に書く: もし間違って代入をしていると,エラーを表示します.ただし,変数間の比較の書き誤りは検出できません.

第8回目

■ レポートの書き方(採点方法)

第6回から第8回にかけて,アプリケーションの仕様作成とプログラミングをしてきました.今回の採点は,第7回の「課題」に示した書式どおりに説明がなされていることをチェックしました.

  1. 「pgmcat プログラム内部設計書」がそろっていること.
  2. 「ソースプログラム」が添付されていること.
  3. 「操作説明書」がそろっていること.あらかじめ準備すべきデータファイルの書式,コマンドライン上で入力すべき事柄,について説明があること.
  4. 「動作例」が示されていること.完全動作していない場合であっても構わない.誤った出力があれば,それをありのまま添付する(真っ黒な画像であっても).また,コンパイルエラーで止まっていても,示してほしかった.
  5. 「実装の完成度」が説明されていること.「時間がなかったのでできません」は説明になりません.

各2点で計算しました.章立てが悪く,上記の説明がすぐに見付からない場合は,1点減点しています.

完全に完成している人が2人いました.+5点しています.


第9回目

■ 指定サイズまでデータが格納できない

ダミーヘッド版のカーソル型リストで,配列の1つぶんはダミーヘッドに使います.したがって,list_new(int size) において,malloc するサイズおよび l->max のサイズは,size + 1 とする必要があります.

■ ダミーのヘッドの .data を参照してしまった

ダミーのヘッドには,データを格納してはいけません.また,ダミーヘッドのデータを参照してはいけません.l->nodes[0].data に相当する操作があってはいけません.たとえば,n = 0; l->nodes[n].data という組み合わせもいけません.具体的には以下の事例です.

void list_print(List *l)
{
  Index n;

  n = 0;
  while( n != EOL ){
    printf("%c",l->nodes[n].data);
  - - -
}

■ list_remove() はもっと簡潔になる

ダミーを用いることで,ヘッドノードのためだけの処理というものを減らすことができます.list_remove() では,ヘッドノードのための処理がありました.それを無くすことができます. 以下の左を,右のようにしている人がいました.
if( l->head == EOL)
  return;
if( l->nodes[0].next == EOL )
  return;
以下の処理があるので不要です.
n = 0;
while( l->nodes[n].next != EOL ){
  t = l->nodes[n].next;
  - - -
}

■ ダミー版のメリットを詳しく説明すること

メリットの説明を単に「場合分けが不要になる」とだけ書いている人がいます.もっと詳しく説明してください.

■ 採点方法

出題が、プログラムを全部自力で作成するものではないので、採点方法を変えました。動作説明(3点)、Makefile(2点)、プログラム(3点)、メリット説明(2点)です。再提出時は、初回が5点以上の人は9点、その他の人は8点を上限とします。


第10回目

■ hash_set の注意点

ハッシュ法では、キー(文字列)を基準にデータを格納します。配列変数で配列番号が基準となることと同じです。つまり、ハッシュキーが同じならば、データは上書きされなければなりません。
int a[10];

 a[3] = 100;
 a[3] = 200;
⇒ a[3] = 200 であり、100 ではない。
Hash *h;

hash_set(h,"apple",100);
hash_set(h,"apple",200);
⇒ hash_get(h, "apple") → 200
   (イメージ) h["apple"] = 200 であり、100 ではない。

■ 採点方法

前回と同様に採点方法を変えました。listの穴埋め5点、hashの穴埋め5点です。reove は3点です。再提出時は、初回に5点以上の人は9点、その他の人は8点が上限になります。


第11回目

■ ファイルから非数字文字の読み取り

ファイルから非数字部分を読むというプログラムでは、数字を読み込んだあと、その数字を読まなかったことにしておきたい。この実現には、通常、ungetc を使う。fseek と ftell を使って、ヘッダの位置を1つ戻す方法もあるが、一般に、ヘッダとは外部記憶装置(ハードディスクなど)のヘッダのことであるので、処理時間がかかってしまう。

非数字を読み込むサブルーチンの終了後には、fgetc をすると、数字が得られることを期待してプログラムを作る。したがって、非数字を読み込むサブルーチンの中で、ungetc を使って、読み過ぎてしまった数字を戻す必要がある。


last update 2005.7/12 by tokuhisa