たかふーのブログ このページをアンテナに追加 RSSフィード Twitter

2010-08-26

初めてのCの続き。

こんにちは。

いまい@童貞に死すです。


先日の自分のブログに頂いたコメントを元にGoogle先生に聞いてみました。

C言語では、文字列は「終端にnul文字('\0')が入っている、文字(char)の配列」で表現する。

(例えばMSX-BASICでは、文字列の内部表現には「文字数」が入っており、終端文字は無い)

したがって、例えば"abc"という文字列は'a','b','c','\0'で構成されており、3文字(strlen("abc")の戻り値は3)だが、実際に使われているのは4バイト(sizeof("abc")の値は4)。

C言語の基礎知識(?)メモ(Hishidama’s C Memo)

おお、知らなかった><

他の言語で当然のように使っていた文字列が無いとか、考えもしなかった><

という訳で、nul文字なるものが文字列内に存在している可能性を考慮しなくてはならない、という事は理解。つまり前回のコード上で5桁の文字列抽出をする為の変数のサイズを5としていたのは間違いだったという事ですね。

さらに、文字列の終端に必ずnul文字が入るという事はどういうことか。こういうコードを書いてみました。

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

int main(void) {
    char list1[6] = {'A', 'B', 'C', 'D', 'E', '\0'};
    char list2[6] = {'A', 'B', '\0', 'D', 'E', '\0'};
    printf("(1) %6s::%lu::%lu\n", list1, sizeof(list1), strlen(list1));
    printf("(2) %6s::%lu::%lu\n", list2, sizeof(list2), strlen(list2));
    char list3[6];
    char list4[6];
    strncpy(list3, list1, 4);
    strncpy(list4, list2, 4);
    printf("(3) %6s::%lu::%lu\n", list3, sizeof(list3), strlen(list3));
    printf("(4) %6s::%lu::%lu\n", list4, sizeof(list4), strlen(list4));
    return EXIT_SUCCESS;
}

結果が

(1)  ABCDE::6::5
(2)     AB::6::2
(3)   ABCD::6::4
(4)     AB::6::2

なるほど、「文字列には終端にnul文字が必ず入る」っつー事は「文字列中のnul文字は終端を意味しちゃう」という事になる、と。そのお陰で、strlen関数戻り値がバッチリ正しくないんですね。


なんだか分かった気になってきましたがここで考えたいのは、前回のコードのどういったトコロでその配慮が必要なのかというところ。

上記コードで、printfでlist4を出力していますがやっぱり中身は「AB」とされてしまっています。strlenの結果も2ですね。宣言時の内容は「'A', 'B', '\0', 'D', 'E', '\0'」ですので、期待としては'D'までを抜き出して欲しいところですし、せっかくの'D'、'E'が空気として扱われていて大変に可哀想です。

もしかして、printfで再び'\0'が文字列の終端として解釈されただけで、strncpyではちゃんと'D'までコピーされたのでは?という気もしたので、以下のような関数を一つ追加してみてchar一つずつ見てみます。

void print_list(char *list) {
    printf("<<print_list>>\n");
    int i = 0;
    for (i = 0; i < 6; i++) {
        char *out;
        if (list[i] == '\0') {
            out = '*';
        } else {
            out = list[i];
        }
        printf("((%d))[%c]\n", i, out);
    }
}

結果

<<print_list>>
((0))[A]
((1))[B]
((2))[*]
((3))[*]
((4))[*]
((5))[*]

ダメみたい、そう世の中甘くはないですね><


そもそも前回のエントリでの実装は、「テキストファイルを読み込んで一行ずつなんかする」というものでした。1行ずつに行われる処理は、tempとしてその一行の先頭6バイトを抜き出す事と、buf2として改行コードを除去した一行を抜き出す事です。

今までの二つの実装から、strncpyの際にbuf2に'\0'が含まれているとtempを抜き出す際に期待する桁数の文字列を正しく抽出できない可能性がある、という事が分かりました。

実はさらに加えてもう一点あります。

今回使ったstrncpyという関数の仕様です。これは「strncpy(char *a, char *b, size_t n)」とするならば、*bの先頭からn文字を抜き出して*aにコピーする、というものです。

(1) nバイト コピーする.

(2) srcの先頭からnバイトの部分にNUL文字(0x00)がひとつもない場合, dstは終端がNUL文字にならないことに注意が必要.

(3) 文字列srcの長さがnより短い場合,dstの残りの部分はNUL文字で塗りつぶされる. したがって,dstの長さは少なくともn以上でなければならない.

strncpy

つまり、この関数戻り値には文字列の終端を表す'\0'がない可能性がある、という事です。

この部分を試してみるのに、またまた以下のような実装をしてみました。終端の'\0'を狙い撃ちしてみます。

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

int main(void) {
    char list[7] = "ABCDEF";
    char list2[7];
    strncpy(list2, list, 6);
    printf(list);
    printf("\n");
    printf(list2);
    return EXIT_SUCCESS;
}
ABCDEF
ABCDEF&#65533;BCDEF

ハイ、結果が良い感じにおかしくなってくれましたね!


これらの発見(wを踏まえて前回のコードを書き直します。

おさらいですが、このプログラムではテキストファイルを1行ずつ読み込んで、「(先頭から5文字)//(読み込んだ1行)」という形式でファイルに書き込んでいくだけのシロモノです。

という事は、くだんの'\0'が含まれたテキストがそもそもありうるのか?という当然の疑問もある訳で、っていうか多分ないよね、と。なので、文字列が途中に含まれるという事はないと考える事にします。でも、strncpyの戻り値には終端に'\0'が付けて貰えません。直前の例のようなアリサマは大変残念なので、これは付けてあげないといけません。

という訳で、前回の実装を改廃。

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

#define PATH_IN "/test.txt"
#define PATH_OUT "/out.txt"

int main(void) {
    FILE *in_file, *out_file;
    char buf1[80] = "";
    char buf2[80] = "";
    char temp[6] = "";
    int total_count = 0;

    // Open file.
    in_file = fopen(PATH_IN, "r");
    while (fgets(buf1, sizeof buf1, in_file)) {
        total_count++;
        // 改行コード排除
        strncpy(buf2, buf1, strlen(buf1) - 1);
        strncpy(temp, buf2, 5);
        temp[6] = '\0';            // いじった
        printf("%s//%s\n", temp, buf2);
    }
    printf("Total=%d\n", total_count);
    // Close file.
    fclose(in_file);

    out_file = fopen(PATH_OUT, "w");
    fprintf(out_file, "%d\n", total_count);
    fclose(out_file);

    return EXIT_SUCCESS;
}

で、良いのかな><

atsushifxatsushifx 2010/08/27 01:23 fgetsまわりで不具合が発生するはずです。
1行の文字列が79文字,80文字,81文字以上の場合はどうなりますか?

文字列バッファのサイズが80なので境界値である80近辺での不具合に気をつける必要があります。

ただ、本気で対策するのはかなりややこしい(今回のロジックでは対応できない)のでバッファの宣言で定数BUFSIZを使ってシステムの制限というのが一般的です。

作法的には、バッファの宣言で80というマジックナンバーを使っているのが気になります。
BUFSIZとは違うサイズのバッファを使うなら、自前で定数宣言をした方がいいです

imai78imai78 2010/09/01 00:14 遅ればせながら。。。
新たにエントリを起こしました><

ありがとうございますありがとうございます

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/imai78/20100826/1282836231