初級C言語Q&A(2)

初出: C MAGAZINE 1995年7月号
Updated: 1996-07-03

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


文字列

C言語では、文字列の扱いに関する質問が比較的多い傾向にあります。これは、 ポインタや配列という初心者にとって壁に相当する概念を理解する必要があるこ とと、実用的プログラムを作ろうとすると、どうしても文字列処理の頻度が高く なることが原因になっていると思われます。

 今回は、文字列に関する質問で、よくあるものをまとめてみました。


文字列

Q 【文字列型】

 C言語には文字列型というものがないと言われた。どういう意味か。

 C言語では、便宜的に char 型の変数配列を使って文字列を表現するのが伝統的仕様となっています。文字列 は、単に char の配列変数に順番に文字が格納され、'¥0'が現れた所で終了とみなす、というル ールで扱われているに過ぎません。「文字列型」という特別な型があるわけでは ないのです。

Q 【文字列のコピー】

 次のようなコードを書いたが思い通りに動作しないのは何故か。
    char b[100];

    b = "abc";

 このコードの意図は、 おそらく、配列bに文字列 "abc" をセットしたいのだと思います。 しかし、C言語の代入演算子' = 'には、文字列を複写する機能はありません。 strcpy(b, "abc"); を使えば期待通りの動作をするでしょう。

Q 【文字列の比較】

 次のようなコードを書いたが思い通りに動作しないのは何故か。
int compare(char *s1, char *s2)
{
    if (s1 == s2) {
        printf("一致しました¥n");
        return 0;
    } else {
        printf("差がありました¥n");
        return 1;
    }
}
 関数 compare に渡される変数 s1s2char へのポインタです。 if の条件判断部分は、二つのポインタの値を比較しているだけで、ポインタの指す 先を比較しているわけではありません。従って、たまたま二つのポインタが同じ アドレスを指している時だけ、この関数は期待通りに動作します。二つのポイン タが指している文字列が内容が同じであっても、ポインタそのものの値が異なる 場合には、期待を裏切る結果になるでしょう。

 文字列の内容を比較したい場合には標準関数の strcmp を使います。

int compare(char *s1, char *s2)
{
    if (strcmp(s1, s2) == 0) {
        printf("Match¥n");
        return 0;
    } else {
        printf("Differ¥n");
        return 1;
    }
}

Q 【カンマ付き表示】

 "1234567" のような文字列にカンマを付けるにはどうすればよいか。

printf で数値を表示する時に、カンマを付けるにはどう指定すればよいか」という典型 的な質問もあります。残念ながら、 printf にはそのように書式を制御する機能がありません。すなわち、カンマを付けて表 示するためには、そのような処理を自作する必要があります。

 実際は、ある整数の変数があって、その値をカンマ付きで出力したいというこ とが多いと思いますが、整数から文字列へ変換するだけであれば、 sprintf という関数を使って極めて簡単に実現することができます。

 リストは、 "1234567" のような文字列に対して "1,234,567" と表示するための関数です。

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

void comma_print(char *s)
{
    int count;
    int i;
    int len;

    len = (int) strlen(s);
    count = len % 3;
    if (count == 0)
        count = 3;

    i = 0;
    for (;;) {
        putchar(s[i]); /* 数値を一文字ずつ表示する */
        i++;
        if (i == len)
            break;
        count--;
        if (count == 0) {
            putchar(',');
            count = 3;
        }
    }
}

Q 【大文字と小文字の変換】

 文字列を大文字化するにはどうすればよいか。

 文字列を一度に全部大文字にしたり、または小文字にするような標準関数は用 意されていません。しかし、一文字だけを対象に大文字、小文字に変換する touppertolower という標準関数が用意されています。これらの関数を使って、一文字ずつ処理す るような関数を自作するのは簡単です。

Q 【配列とポインタ】

 次の二つの書き方は、何が違うのか。 printf の表示結果は全く同じように見える。
    char str1[] = "string";
    char *str2 = "string";

    printf("%s¥n", str1);
    printf("%s¥n", str2);

 最初の書き方は、 str1 という名前の文字配列を定義しています。この配列は、 "string" という文字列がちょうど入る大きさで作られ、内容が "string" で初期化した状態になります。つまり、 str1 に関しては、次のように書いたのと同じことになります。
    char str1[7];

    strcpy(str1, "string");
 後の書き方は、 str2 という名前の「 char へのポインタ」を定義しています。定義された変数は単なるポインタであり、配 列ではありません。従って、 str2 というポインタそのものの値を変更して、別の対象を指すようにすることができ ます。リストの例では、このポインタは、文字列 "string" を指す値で初期化されます。 "string" という文字列の実体は、どこか別の場所にあるのですが、プログラマーはこの内 容を勝手に書き換えてはいけません。

 両者の差は、それぞれの内容を変更する場合を考えてみると、より明確になり ます。

void foo(void)
{
    char str1[] = "string";
    char *str2 = "string";

    printf("%s¥n", str1);
    printf("%s¥n", str2);

    strcpy(str1, "STRING");
/*  str1 = "STRING"; は間違い */
    str2 = "STRING";
/*  strcpy(str2, "STRING"); は間違い */

    printf("%s¥n", str1);
    printf("%s¥n", str2);
}

Q 【配列の初期化】

 次のようなコードを含むコンパイルしたらエラーが発生した。
void foo(void)
{
    char str1[] = "string";
	...
}

 おそらく、あなたの使っているコンパイラは、かなり古いものなのでしょう。

 ANSIのC言語の規格では、自動変数の配列は初期化できることになっていますが 、以前のコンパイラは、なぜかANSI準拠と堂々と書いて販売されていたにもかか わらず、自動変数の配列を初期化するようなコードはエラーになってしまうもの がありました。

 ANSI Cが広まる前の、いわゆるTraditionalなC言語の仕様では、自動変数の配 列は初期化できませんでした。従って、初期化が必要な場合には、次のように関 数の最初で strcpy などを使って初期化に相当する処理を書くか、あるいは static と指定することによって初期化できるようにする必要がありました。

    char str1[7];

    strcpy(str1, "string");
 ただし、 static の変数はプログラム実行時の最初に一度だけ初期化され、二度目以降はその関数 が呼ばれても直前の状態のままなので、自動変数を初期化するような処理とは若 干異なる振る舞いとなります。

Q【strcat】

strcat とはどういう処理をする関数なのか。なぜこのような奇妙な名前が付いているのか。

 strcat は、既にある文字列の後に、新たな文字列を連結する処理をします。catというの はconcatenateの略と言われています。

 UNIXというOSには、catというコマンドがあります。このコマンドは、引数に与 えたファイルを連結するというものです。例えばcat f1 f2 f3というコマンドは、 f1、f2、f3の内容をこの順番に標準出力に出力します。catを普段から使い慣れて いる人にとっては、 strcat という名前は大変分かりやすいものなのかもしれません。


Q【strcatの結果を保持する領域】

strcat がうまく動かない。以下のプログラムを書いたら、変な答えが返ってきた。
    char *s1 = "Hello, ";
    char *s2 = "world!";
    strcat(s1, s2);

strcat は、与えられた文字列を連結する関数ですが、追加する文字列を最初の引数に指 定した文字列の後に何も考えずに格納しようとします。従って、この関数を使う 場合は、連結した結果を格納するための領域を、プログラマーが責任をもって確 保しておかなければなりません。すなわち、 s1 の指す先は、 s2 をその後に連結しても問題がないだけの十分な大きさの領域である必要があります。

 例のプログラムは、 s1 が指している先は文字列リテラルです。規格上、文字列リテラルは変更してはな らない領域で、変更した結果は未定義となります。従って、変な答が返ってきた わけです。

 このトラブルを解決するための一つの簡単な方法は、連結した結果を格納する 領域を文字配列とすることです。

    char s1[20] = "Hello, ";

Q 【文字列を戻す関数】

 引数の値に対応した文字列を返す関数を作ろうとした。次のコードはなぜ思い 通りに動作しないのか。
char *yes_or_no(int yn)
{
    char tmp[16];

    if (yn == 0) {
        strcpy(tmp, "No");
    } else {
        strcpy(tmp, "Yes");
    }
    return tmp;
}

 変数 tmp がこの関数内の自動変数であるためです。 tmp の内容は、関数が呼ばれてからその外に出るまで保証されていますが、その後は どうなるか分かりません。次のリストと比較してください。
char yes_or_no(int yn)
{
    char c;

    if (yn == 0) {
        c = 'N';
    } else {
        c = 'Y';
    }
    return c;
}
 この関数は char の値を返します。最後の return 文は、 c そのものを返すというよりは、 c がその時持っている値だけを返すと解釈すべきです。同様に解釈すれば、最初に 書いた関数が返す値は tmp の値、すなわち文字が入った配列の先頭アドレスとなります。このアドレスは、 関数を出る直前までは意味がありますが、関数を出てしまった所で、変数自体が 消滅してしまうため、意味を失ってしまうのです。

 値に応じて文字列を戻すためには、次のような書き方をします。

  1. tmpをstaticとする。
    static char tmp[16]; と定義しておけば、この変数はプログラムが終了するまで存続します。

  2. 呼び出し側で、文字列を格納する変数を用意する。
    void yes_or_no(int yn, char *result)
    {
        if (yn == 0) {
            strcpy(result, "No");
        } else {
            strcpy(result, "Yes");
        }
    }
    
    /* 呼び出す側の関数の例 */
    foo()
    {
        char tmp[16];
    
        yes_or_no(0, tmp);
    }
    

  3. malloc を使って動的にメモリを獲得する。
    この場合、使いおわった時にメモリを開放する処理が必要になります。
    char *yes_or_no(int yn)
    {
        char *s;
    
        s = malloc(4);
        if (s != NULL) {
            if (yn == 0) {
                strcpy(s, "No");
            } else {
                strcpy(s, "Yes");
            }
        }
        return s;
    }
    
    /* 呼び出す側の関数の例 */
    foo()
    {
        char *s;
    
        s = yes_or_no(0);
        if (s != NULL) {
            printf("%s¥n", s);
            free(s); /* 使いおわったので、後始末 */
        }
    }
    

Q 【サイズを指定した配列の初期化】

 次のコードが思った通りに動作しない。
    char str[6] = "string";

    printf("%s¥n", str);

 配列 str[6] は、6個の要素を入れる量のメモリが割り当てられていますが、 "string" という文字列は、各文字を格納するための6文字分のメモリに加えて、文字列の終 了を意味する '¥0' を格納しなければならず、すなわち7個の要素に対するメモリが必要です。

 ところが、C言語の規格では、この例のような書き方が許されています。この場 合は、最後の '¥0' は格納しないで、 str[0] から str[5] にそれぞれ 's' から 'g' の文字が入ることになります。この場合、文字列終了の '¥0' は付加されませんから、 printf で表示しようとすると、 string の後にゴミが表示されるかもしれないし、最悪の場合はプログラムが異常終了す るかもしれません。

str[6] ではなく str[7] と定義すれば問題は解決します。


Q 【文字の配列としての初期化】

 なぜ先程の質問のように、文字列の終端を格納できないようなサイズの配列に 対して文字列リテラルで初期化する必要があるのか。

 文字列型というものがないことを思い出してください。C言語では、文字列とい うのは単なる文字の配列です。従って、文字列ではなく単に文字の配列として処 理を行いたい場合もあり得ます。典型的な例として、10進16進の変換の処理があ ります。
char dec_to_hexchar(int i)
{
    if (i < 10)
        return (char) (i + '0');
    else
        return (char) (i - 10 + 'A');
}
 これは次のように、配列を使って書くことができます。
int dec_to_hexchar(int i)
{
    static char table[16] = {'0', '1', '2', '3', '4', '5', '6', '7',
                             '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    return table[i];
}
 しかし、次のように書いた方が明解です。
int dec_to_hexchar(int i)
{
    static char table[16] = "0123456789ABCDEF";

    return table[i];
}
 これを次のように書くと、 table の大きさは "0123456789ABCDEF" という文字列が格納できるサイズになります。これは17バイトとなり、最後に付 加されたNUL文字は、この処理ではおそらくアクセスされることがないため無駄 です。
int dec_to_hexchar(int i)
{
    static char table[] = "0123456789ABCDEF";

    return table[i];
}

Q 【printfの%s指定】

printf("%s", s); のように呼び出す書き方があるが、 printf(s); ではなぜいけないのか。

 文字列の中に書式制御文字が含まれていると、予期しない結果となるかもしれ ないからです。 "%s" という文字列を表示させるつもりで次のコードを実行すると、多分思った通りに 表示されないし、最悪の場合はプログラムが暴走するかもしれません。
    s = "%s";
    printf(s);

Q 【printfで%を出力】

printf を使って '%' を出力するにはどうすればよいか。 '¥%' としてもうまくいかなかった。

'%%' と書けば '%' が表示されます。

Q 【漢字の文字数】

漢字を含む文字列の文字数を調べるにはどうすればよいか。

 一般的かつ簡単な方法はありません。処理系によっては、漢字を含む文字列の 文字数を戻すような非標準のライブラリ関数が用意されているかもしれません。 やっかいな問題は、漢字の表現方法が一通りでないということです。JIS、シフト JIS、EUCなどの種類によって、文字の数え方を変えてやらなければなりません。

Q 【itoa】

 atoi の逆の処理をしたい。数を文字列に変換するにはどうすればよいか。 itoa という関数はなかった。

 sprintf を使えば簡単にできます。ただし、結果を格納することのできる領域はあらかじ め確保しておかねばなりません。

 sprintf の処理は、仕様が汎用的であり、また様々な書式指定に対応するため、コードが 複雑になるし、また、処理自体も遅くなると指摘する人もいます。これは一見正 論のようですが、 sprintf の代わりに itoa を作って使った場合に節約できるコードの量と速度が向上する割合を考えてみる と、無駄な努力になってしまう場合もあるだろうと言わざるを得ません。とりあ えず sprintf を使っておくのが、保守性の見地からも妥当なことが多いでしょう。


Q【文字列リテラルの変更】

 次のプログラムを実行すると異常が発生した。
    char *p = "Hello, world!";
    p[0] = tolower(p[0]);

 ANSI Cでは、文字列リテラルを変更した時のふるまいは未定義となっています。 従って、実行した結果何が起きるか分かりません。

 文字列の内容を変更したいのであれば、配列を用意すべきです。

    char a[] = "Hello, world!";
(参考) Q&A(7)-3【未定義】, Q&A(7)-4【未定義の例】

Q【gets】

 文字列をファイルから読み込む処理に gets を使ったコードを書いたら、先輩から「危ないから fgets を使えと指示された。一体 gets の何が危ないのか。

gets は、読み込む文字列の長さが、与えるバッファの大きさよりも短いということを 前提としています。多くの場合、一行の長さはたかが知れているので、これは問 題ないわけですが、希には、バッファの大きさよりも長い文字列があるかもしれ ません。データ中に1MBの長さの文字列がないとは誰も保証できないのです。

 このような予期しないデータに遭遇した時、getsはどのような被害をもたらす か予想できません。これに対して、 fgets を使うと、あらかじめ指定した大きさより長い文字列があった場合には、指定し た長さ(厳密にいえば長さ-1)までをとりあえず読み込んでくれるので、プログラ ムは安全に処理することができます。


(C) 1995-1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。