文字列とポインタ

文字列とポインタ

教科書「やさしいC」の「10.3 文字列とポインタ」を読んで、文字列とポインタの関係について確認しよう。

C 言語の「文字列」は文字の配列

ここで「文字列」と言っているが、C 言語には「文字列」の型はない。実際には、文字の配列で、末尾にヌル文字 '\0' が入ったものである。なので例えば、

  char str[] = "Hello";
という記述は、次のように書いてもよい。
  char str[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; /* 末尾のヌル文字に注意 */
また、C 言語の「文字列」はあくまでも配列なので、配列のときと同じ注意が必要である。例えば、「文字列をコピーしたい」と思ったとき、
  char str1[] = "Hello";
  char str2[10];

  str2 = str1;	/* コンパイルエラー */
のような書き方はできない (第 2 回で学んだ通り、文字列名は配列名を表し、配列名は配列の先頭のアドレスを持っており、このアドレスは書き換え不可)。配列のときと同じように、配列の要素、つまり文字をひとつずつコピーする必要がある。普通の配列のコピーのときは、配列の要素数が必要だったが、文字列の場合は「末尾は必ずヌル文字 '\0'」という約束があるので、ある文字がヌル文字かどうかで終端に達したかどうかを判断できる。また、自分で文字列を生成するときは、末尾にヌル文字を入れるのを忘れないようにすること。末尾のヌル文字がないと例えば printf 関数などの関数は正しく動作しない。

文字列をポインタで扱う

教科書にある通り、上に書いた

  char str[] = "Hello";
は、char 型のポインタを使って
  char *str = "Hello";
とも書ける。ただし両者は意味が異なる。前者は「配列 str[ ] を文字列 (char 型の配列) "Hello" で初期化」という意味だが、後者は「メモリ上のどこかに配置された文字列 (char 型の配列) の先頭アドレスを、ポインタ str に代入」という意味である。メモリの配置も次の図のように異なる。前者の str[ ] については配列名なのでアドレスの書き換えはできないが、後者の *str の方はポインタなのでアドレスを書き換えられる。

両者のどちらを使った場合でも、変数 str が文字列の先頭アドレスを持つことには変わりない。printf 関数などの関数の引数として同じように与えることができる。

[str が配列の場合]

  char str[] = "Hello";
  printf("%s\n", str);	/* OK */

[str がポインタの場合]

  char *str = "Hello";
  printf("%s\n", str);	/* OK */

文字列を操作するためのライブラリ関数

文字列を操作するには、string.h ヘッダに定義されている関数を使うのが便利である。次の表に関数の例を示す。詳しい使い方や他の関数については、教科書やインターネットで各自調べてほしい。

string.h の文字列操作用の関数の例

関数名仕様
strcpy文字列をコピーする
strncpy文字列を指定文字数分だけコピーする
strcat文字列を連結する
strncat文字列を指定文字数分だけ連結する
strcmp文字列を比較する (※返り値に注意… 0 なら一致)
strncmp文字列を指定文字数分だけ比較する (※返り値に注意… 0 なら一致)
strstr文字列の中からある文字列を探索する
strtok文字列を区切り文字で分割する
strlen文字列の長さを取得する

配列とポインタの違いについて確認しよう。
  1. 次のプログラムをコピー&ペーストして入力し、コンパイルしてみよ。結果について説明せよ。
    #include <stdio.h>
    
    int main(void)
    {
      char str[] = "Hello";
    
      printf("%s\n", str);	/* 文字列を出力 */
      
      str = "Goodbye";	/* 配列に文字列を代入 */
    
      return 0;
    }
    
  2. 次のプログラムをコピー&ペーストして入力し、コンパイル、実行してみよ。a のプログラムとの違いを説明せよ。
    #include <stdio.h>
    
    int main(void)
    {
      char *str = "Hello";
    
      printf("%s\n", str);	/* 文字列を出力 */
      
      str = "Goodbye";	/* ポインタのさす場所を変更 */
    
      printf("%s\n", str);	/* 文字列を出力 */
    
      return 0;
    }
    
  3. 次のプログラムをコピー&ペーストして入力し、コンパイル、実行してみよ。
    #include <stdio.h>
    
    int main(void)
    {
      char str[256];
    
      /* 文字列を入力 */
      printf("Input string => ");
      scanf("%s", str);
      
      printf("%s\n", str);	/* 文字列を出力 */
    
      return 0;
    }
    
  4. 次のプログラムをコピー&ペーストして入力し、コンパイル、実行してみよ (※ c のプログラムと同じ結果になる場合はコンパイル時に最適化オプション -O2 をつけてみよ)。c のプログラムとの違いを説明せよ。
    #include <stdio.h>
    
    int main(void)
    {
      char *str;
    
      /* 文字列を入力 */
      printf("Input string => ");
      scanf("%s", str);
      
      printf("%s\n", str);	/* 文字列を出力 */
    
      return 0;
    }
    

  1. 以下のプログラムをコピー&ペーストして入力し、コンパイル、実行せよ。

    [array2d.c]

    #include <stdio.h>
    
    int main(void)
    {
    	int i;
    	char name[3][10] = { "first", "second", "third" };
    
    	/* 文字列ごとに出力 */
    	for (i = 0; i < 3; i++) {
    		printf("%s\n", name[i]);
    	}
    
    	return 0;
    }
    
  2. [array2d.c] のコードに対して、i 番目の文字列の先頭文字 (例えば 1 番目の文字列なら 's') のアドレスと name[i] のアドレスを表示するコードを追加して、両者が一致することを確かめるプログラムを作成せよ。
  3. [array2d.c] の 2 次元配列 name のメモリ上のイメージを図示せよ。
  4. (チェック問題) strlen() を使って、それぞれの文字の長さを求めて表示するプログラムを追加せよ。なお、必要なヘッダファイルをインクルードすることを忘れずに。

  1. 1 つの文字列を入力させ、その文字列の長さを表示するプログラムを作成せよ。練習として、ライブラリ関数 strlen() は用いないこと。
  2. (チェック問題) 3 つの文字列を入力させ、配列を用意して 3 つの文字列を連結した文字列を作り、それを出力するプログラムを作成せよ。練習として、ライブラリ関数 strcat() は用いないこと。

  1. 以下のプログラムをコピー&ペーストして入力し、コンパイル、実行せよ。

    [arraystring.c]

    #include <stdio.h>
    
    int main(void)
    {
    	int i;
    	char *name[3] = { "first", "second", "third" };
    
    	/* 文字列ごとに出力 */
    	for (i = 0; i < 3; i++) {
    		printf("%s\n", name[i]);
    	}
    
    	return 0;
    }
    
  2. [arraystring.c] のプログラム中の文字列やポインタのメモリ上のイメージを図示せよ。
  3. [arraystring.c] において、printf() により文字列を出力する前 (6 行目と for 文の間) に、
              name[1] = name[2];
    
    を入れて、コンパイル、実行せよ。また、結果について説明せよ。
  4. (チェック問題) [array2d.c] の文字列の出力の前に、同様の文を入れて実行を試み、なぜそのような結果になるかを説明せよ。

これまで main 関数の引数は void としてきたが、実は main 関数には文字列のポインタ配列とその要素数を引数として渡すことができる。
 int main(int argc, char *argv[])
ここで、argc が配列の要素数、*argv[] が文字列のポインタ配列である。これらを使うと、UNIX のコマンドラインオプションを実現できる。 argv[0] にはプログラムの (パスつきの) 実行ファイル名が入り、argv[1] 〜 argv[argc-1] には、プログラムへのコマンドライン引数が入る。
  1. 次のプログラムをコピー&ペーストして入力し、コンパイル、実行してみよ。

    [argv_dump.c]

    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
    	int i;
    
    	printf("argc = %d\n", argc);
    
    	for (i=0; i<argc; i++) {
    		printf("argv[%d] = %s\n", i, argv[i]);
    	}		
    
    	return 0;
    }
    
    [実行例]
     $ cc argv_dump.c -o argv_dump
     
     $ ./argv_dump
     (※何か表示される)
    
     $ ./argv_dump -f hoge -l -o hage
     (※何か表示される)
    
     $ ./argv_dump IMAGINE THE FUTURE.
     (※何か表示される)
    

  2. (チェック問題) 次の実行例のように、コマンドライン引数で指定した文字列を出力するプログラムを作成せよ。
    [実行例]
     $ ./my_message		←※引数の数が 1 でないとき
     Usage: ./my_message name
    
     $ ./my_message Mikkio
     Hello, Mikkio! (^-^)