KID's World
 

おしながき
トップページ
プログラミング
研究
いんすぴ
色日記
ソフトウェア
KID's Worldの歴史
リンク
小出俊夫について


C言語で文字列を扱う

scanf関数で文字列を得るにはどうしたらいいのか、文字列を扱う時の危険性を考慮する

 

再確認、"ABC"

 さて、「"ABC"」というのが文中に出てくると、一体どういう意味をなすのか。これは重要です。前にも言いましたが、C言語には文字列という型はありません。それじゃぁ一体、文法上どういう意味になるのでしょうか。

 じつはプログラム中に「"ABC"」が出てくると、これ全体がアドレスの値になります。何のアドレスの値かというと、'A', 'B', 'C', '\0' と連続してメモリ上に置かれた時の'A'へのポインタです。メモリ上のどこに置かれるかは分かりません、これはコンパイラやOSがうまく配置してくれます。

 'A'はchar型、"ABC"は'A'のアドレス

これを頭の中で何度も唱えてください。すると、こんなことができると分かるはずです。

    char *s;
    s = "ABC";

この例は、char型へのポインタ変数sを作って、そいつにどこかに用意された"ABC"の先頭アドレスを代入させる、と言う事なります。では次は、

 "ABC"はchar型へのポインタだ

です。こう考えると、いままで何気なく書いてきた

  printf("ABC");

は、

  char *s = "ABC";
  printf(s);

と書いても同じだ、と言うことになりますね。 ん? だんだんC言語の真髄が見えてきた?

 

いよいよ、scanf関数で「文字列」を得る

 これらの話をふまえ、scanf関数を使って文字列をユーザに入力してもらいましょう。文字列は、printf関数と同じように「%s」で得ることができます。

 
#include <stdio.h>

main()
{
  char s[100];

  scanf("%s", s);
  printf("あなたが入力した文字列は、\n「%s」です。\n", s);
}

 文字列を記憶する変数を「*s」ではなく、「s[100]」と宣言しているのはなぜかは、分かりますね? 問題は、受け付ける文字列のためのメモリを確保しているか否かです。 scanf関数は、そんなことお構いなしにどんどんそのアドレスに書きこんじゃいますから。 メモリ管理の責任はプログラマにあるのです。

 さあ結果はどうでしたか?(水色の字は、入力した文字列です)

実行例:

私の名前は、小出 俊夫です。
あなたが入力した文字列は、
「私の名前は、小出」です。

 あれ、「俊夫です。」がないぞ??? scanf関数は、空白文字を区切りとみなすんでしたよね。だから空白前までの「私の名前は、小出」しか得ることができなかったのです。それじゃ、'\n'までを配列sに書きこむようにプログラムを作ってみましょう。ちょっと複雑になりますが。

 
#include <stdio.h>

main()
{
  char c, s[100];
  int i;

  for(i = 0; i < 100; i++){
    scanf("%c", &c);
    if ( c == '\n'){
      /* もし'\n'だったらヌル文字を書いてループを抜ける */
      s[i] = '\0';
      break;
    }
    else
      s[i] = c;
  }
  printf("あなたが入力した文字列は、\n「%s」です。\n", s);
}
 

 for文を使って int型変数iを0〜99まで増やしていきます。その過程でscanf関数で一文字ずつ読んでいき、同時に配列sに書き込んでいきます。もしscanf関数で読み込んだ値が'\n'つまりEnterキーだったら、その場で最後に「ここが最後ですよ」という意味のヌル文字'\0'を書き込んでループを抜けます。こうして空白もちゃんととることができます。

 一文字だけ得たい時は、scanf関数ではなく、getchar関数を使うといいでしょう。これを使って、上のプログラムを改造し、さらに、わざと、できるだけ行数の少ないプログラムを作るとこうなります。

 
#include <stdio.h>

main()
{
  char s[100];
  int i=0;

  while( ( s[i++] = getchar() ) != '\n') ;

  s[i-1] = '\0';
  printf("あなたが入力した文字列は、\n「%s」です。\n", s);
}

 こういう書き方は見難いのであまりしないのですが、勉強のために、どうしてこれでうまく行くのか考えてみましょう。解説はしません。

 わざわざこのようにプログラムせずとも、空白を区切り文字として見ないで文字列の入力を受け付けるには、gets関数が使えます。

 

文字列をいろいろ扱ってみる

文字列のコピー

 ここで、ポインタという概念をもう一度振り返ってみましょう。

 
  char s[] = "ABCDE";
  char *p;

  p = s;   /* これはどんな代入をしているのか? */

  s[1] = 'b'; 
  printf(p);

さて、どんな文字が表示されたでしょうか。表示されるのは「ABCDE」ではなく、「AbCDE」でしたね。つまり、文字列の複製がしたいからといって、このようにただの代入をすると、コピー元(s)の値が変わると、コピー先(p)の値まで変わってしまうのです。なぜなら、pもsも、同じアドレスを指すからです。そのため、C言語では特別に文字列をコピーするためにstrcpy関数を用意しています。こんな感じで使います。

  strcpy(p,s);

 この関数は stdio.h ではなく、string.hで定義されているので、それもインクルードする必要があります。strcpy関数は、簡単に言えばpの指すアドレス以降に、sの指すアドレス以降の文字列をコピーする関数です。最後の'\0'までコピーします。ですが、

  p = s;

のところを単純にこの関数で置き換えればいいということにはなりません。なぜなら、ポインタ変数pは、どこのアドレスを指し示しているか分かりませんね。他の変数が使っているアドレスを指し示していたら、その変数の内容が破壊されてしまいます。そのままstrcpy関数を使ってコピーしようとすると、どこにコピーされるか分からないので、普通は次のようにします。

 
#include <stdio.h>
#include <string.h>  /* strcpy関数が定義されている */

main()
{
    char c1[] = "ABCDE", c2[6];

    strcpy(c2,c1);
    printf(c1);
}

 配列を宣言すれば、まだ誰も使っていない安全なメモリが確保できます。その上でstrcpy関数を使う分には問題が無いわけです。また、安全な領域をメモリ上に提供するmallocという関数もありますが、これは自分で研究してみてください。(そのうちこのサイトでも解説するようにしたいと思いますが)

文字列の比較

 ところで、今度は、入力された文字が「TOSHIO」だったら、特別な処理をする、というプログラムを書いてみましょう。ポインタがどうのこうのという考えを持たなければ、判定に使う if文 は、次のように書くかもしれません。

 
#include <stdio.h>

main()
{
    char s[100];

    printf("文字を入力してください:");
    scanf("%s", s);

    if (s == "TOSHIO"){
        printf("「俊夫」が入力されました。\n");
    }
    else{
        printf("「%s」が入力されました。\n",s);
    }
}

 ここでの「特別な処理」というのは、「TOSHIO」と打たれたら「俊夫」と漢字で表示し、それ以外だったらそのまま表示するということです。この例は、なんの問題も無くコンパイルされ、sが「TOSHIO」以外だったら、ちゃんと表示してくれます。しかし、「TOSHIO」と入力しても、「俊夫」と漢字になりません。なぜでしょう。

 scanf関数によって、確かに配列変数sには「TOSHIO」という内容が入ります。しかし、ただたんに「s」と指定しただけでは、何度もしっつこく言っているように配列変数sの先頭アドレスにしかならないのです。つまり

  s == "TOSHIO"

という比較は、「配列変数sの先頭アドレスと、別に用意した"TOSHIO"の先頭アドレスが同じか?」という比較になります。当然、これはいつでも「偽」になりますね。

 ではどうやって文字列の比較をすればいいのでしょうか。配列を一字一字調べなくてはならないのでしょうか? 実は、それしか方法がありません。ですが、文字列の比較というのはよくする事なので、C言語ではそれを関数にしてプログラマに提供しています。strcmp関数です。

 strcmp関数には、2つの文字列の先頭アドレスを指定します。すると自動的にそのアドレスによって指定された文字列を比較し、同じなら0を返します。というわけで、先のプログラムは、こう書き換えることによって正しく動くようになります。

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

main()
{
    char s[100];

    printf("文字を入力してください:");
    scanf("%s", s);

    if (strcmp(s,"TOSHIO") == 0){
        printf("「俊夫」が入力されました。\n");
    }
    else{
        printf("「%s」が入力されました。\n",s);
    }
}
 

まとめ。

  1. C言語には文字列という型はない。
  2. "ABC"はchar型へのポインタ。
  3. scanf関数で文字列を得るには、%s指定子を指定する。
  4. 1文字だけ得るには、getchar関数を使う。
  5. 文字列をコピーするにはstring.hで定義されているstrcpy関数を使う。
  6. 文字列を比較するにはstring.hで定義されているstrcmp関数を使う。
 

質問

(1997/02/16 公開, 1999/03/13 改)

[目次へ] [次へ]

 著作権は全て小出 俊夫にあります。KID's World © 1996-2003 Toshio Koide.

 対応ブラウザについて