(16)[C] 文字列とポインタの扱い

[ トップ | 目次 | 前ページ | 次ページ ]


同一の操作が可能な値の集合を型と考えるMLと異なり,C言語の型は,メモリー上の領域の大きさとその表現を表す,特にCの基本型は,32ビット整数のように,メモリー上の操作単位に相当する,一方文字列は必要とするメモリー領域の大きさが一定ではないため,基本型としては扱えない,C言語の用語としては一般的ではないかもしれないが,このチュートリアルでは,基本型以外の型を複合型と呼ぶことにする,一般に,複合型は基本型を組み合わせて構成される,本節で,Cのもっとも簡単できかつ重要な複合型である文字列型を学ぼう,

文字列のメモリー上の表現

文字列は文字の列である,Cの基本型の節で学んだように,1文字はchar型の8ビットのデータである,従って文字列はchar型のデータの列であり,メモリー上には連続して配置される,

例えば,"SML#&C"は,配置する先頭のアドレスをaとすると以下のように格納される,

アドレスa+0a+1a+2a+3a+4a+5a+6
内容SML#&C\0

最後の\0は文字列の終了を表す特別な値である,Cで文字列の操作をするコードを書くためには,この構造を理解することが重要である,特に,以下の点に注意しよう,

  • アドレスはバイト単位のアドレスである,
  • 文字列データは,その長さの情報を持たない,
  • 途中から終わりまでの文字列,たとえば#&C\0も文字列の構造をしている,

文字へのポインタ型と文字列定数

Cではこのように格納された文字列を,その先頭アドレスで表す,上記の場合,先頭のアドレスの値aが文字列"SML#&C"を表現するデータである,すなわち,先頭文字Sのアドレスの値aを格納したデータがCにおける文字列型である,これを,文字へのポインタとよび,文字型charに*をつけて

char*

と書く,通常の基本型と同様,以下のような宣言をすると,

char* S;

文字へのポインターを格納するための1語の領域が確保される,文字列データは,MLと同様ダブルクオート(")でか囲んで記述する,

char* S = "SML#&C";

と宣言すると,上記の構造がメモリー上につくられ,変数Sの領域がその先頭アドレスで初期化される,

文字列定数の中には,ML同様のエスケープ文字を使用できる,以下は,初期値付きの文字列変数を定義しプリントするプログラム例である.

main() {
 char* S = "SML#\t&\tC\n";
 printf(S);
}

これを実行すると以下のようにタブで区切られた文字列がプリントされる.

c:\SMLSharpAndC\c>gcc string.c
gcc string.c

c:\SMLSharpAndC\c>a.exe
a.exe
SML#	&	C

ポインタの操作を通じた文字列の参照

ポインタ型に対する操作は,ポインタの指す値の取り出しおよびポインタの更新(移動)である.

ポインタの指す値の取り出し.

Sがポインタ変数なら,

*S

はSが指す値を取り出す式である.

ポインタ型宣言

char* S;

は,Sの指す値を取り出した結果,つまり,*Sがchar型であると宣言していると解釈できる.この宣言は,

char *S;

と書いてもよく,こちらがより一般的な表記である.

Sが

 char *S = "SML#\t&\tC\n";

と宣言された文字列なら,Sは連続して格納された文字の列の最初の文字へのポインタであるから,式*Sは'S'を返すはずである.このことを*Sの値をプリントして確認してみよう.文字の印字書式は%cであるので,以下のプログラムで*Sの値を印字できる.

main() {
 char* S = "SML#\t&\tC\n";
 printf("The first character of S is: %c\n",*S);
}

を実行すると,以下のような結果が得られる.

C:\SMLSharpAndC\c>a.exe
a.exe
The first character of S is: S

ポインタの更新(移動)

ポインタは符合無しの自然数と同一のメモリー表現をしているため,自然数の加算と減算を行うことができる.実行文

S = S + 1;

はポインタに1を足す.この結果,Sが指す値を格納する領域分ポインタが加算される.Sが文字列であれば,*Sはもともと指していた文字列の2番目の文字を返すはずである.

main() {
 char* S = "SML#\t&\tC\n";
 S = S + 1;
 printf("The first character of S is: %c\n",*S);
}

を実行すると,以下のような結果が得られる.

C:\SMLSharpAndC\c>a.exe
a.exe
The first character of S is: M

ポインタ操作になれるために,文字列の長さの計算を,種々の仕方で書いてみよう.

  char* S = "SML#&C";
  char* temp;
  int counter = 0;

が宣言されているとする.


文字列の長さを計算するには,ポインターの指す先頭文字が'\0'になるまで,ポインタを更新しその回数を数えることである.以下のコードで実現できる.

  for (; *S != '\0'; counter++, S++) {};

ポインタSに整数iを加えた値は,Sの指すさらにi個先のエレメントを指すポインタである.そこで,上記のコードは以下のようにも書ける.

  for (; *(S+counter) != '\0'; counter++) {};

逆に,あるポインタS1が別のポインターSのi個先のエレメントを指すポインタなら,S1 - Sはiである.この性質を利用すると,以下のようなコード化も可能である.

  for (temp = S; *temp != '\0'; temp++) {};
  counter = temp - S;

ループの最初の代入文temp = SでポインターSの値をtempにコピーし,tempを更新し,最後にその結果これら式の実行結果を以下に示す.

main() {
 char* S = "SML#&C";
 char* temp;
 int counter;
 for (temp = S; *temp != '\0'; counter++, temp++) {};
 printf("The value of counter is:%d\n", counter);
 for (counter = 0; *(S+counter) != '\0'; counter++) {};
 printf("The value of counter is:%d\n", counter);
 for (temp = S; *temp != '\0'; temp++) {};
 counter = temp - S;
 printf("The value of counter is:%d\n", counter);
}
C:\SMLSharpAndC\c>a.exe
a.exe
The value of counter is:6
The value of counter is:6
The value of counter is:6

文字列を操作するプログラム例

文字および文字列操作の基本を復習しておこう.

  • 文字は符合なしの整数である.
  • 文字のプリント書式は%cである.
  • 文字列はnull文字'\0'で終了する文字の並びであり,Cプログラムでは最初の文字が格納された領域へのポインタとして扱う.
  • Sがポインタ変数なら式*SはSが指す値を取り出す式であり,S+1はSが指す次の要素を指すポインタである.

以上の基本機能を使えば,文字列を操作する種々の関数を書くことができる.以下,種々の関数を書いて,ポインタの扱いになれよう.

文字列の長さの計算

上の例を関数にすると以下のようになる.

int stringLength (char *S) {
  int counter = 0;
  for (; *S != '\0'; counter++, S++) {};
  return (counter);
}

文字列を比較する関数

文字列にふくまれる文字がすべて同じ場合は真(つまり1)を返しそれ以外は0を返す関数である.以下のような方針でプログラムする.

  • 結果表す変数を1に初期化する.
  • 2つの文字列の先頭のいずれもが'\0'でなければ比較を行う.
  • 先頭の文字が等しければそれぞれのポインタを更新し繰り返す.
  • 先頭の文字が異なれば,結果を表す変数を0に変更し直ちに終了する.

最後の処理は,繰り返し構文から抜け出すbreak文を使い実現する.以上の処理は,以下のようにコードできる.

int stringCompare(char* s1,  char* s2) {
  int result = 1;
  while (*s1 != '\0' || *s2 != '\0')
    { if (*s1 != *s2) {
       result = 0;
       break;
      }
      else {s1++; s2++;}
    };
  return(result);
}

whileの条件式

(*s1 != '\0' || *s2 != '\0)

は二つの条件式*s1 != '\0'と*s2 != '\0の論理和,すなわち二つのどちらかが成り立つとき真となる式である.

英字を大文字に変換する関数

文字は符合なしの整数であるから,加算や減算を行い,べつの文字に変換できる.ASCII文字コードでは,英字アルファベットは続いており,かつ大文字は小文字より先にある.この性質を使えば,英字を大文字に変換する関数を以下のように定義でききる.

int toUpper(char c) {
  if (c >= 'a' && c <= 'z') {
   c = c - ('a' - 'A');
  };
  return (c);
}

大文字小文字の違いを無視して文字列を比較する関数

上記の関数toUpperを使えば,以下のように実現できる.

int stringCompare(char* s1,  char* s2) {
  int result = 1;
  while (*s1 != '\0' || *s2 != '\0')
    { if (toUpper(*s1) != toUpper(*s2)) {
        result = 0;
        break;
      }
      else {s1++; s2++;}
    };
  return(result);
}

配列型による文字列の表現

以上の文字列をポインタとして表現する方法により,文字列のスキャンはプリントを行うことができた.しかしこの方法では,文字列のコピーや文字列の変換など,プログラムで文字列を生成する処理には対応できない.生成すべき文字列を

 char *result;

と宣言しても,それによって確保される領域は,文字列の先頭を指し1語のポインタ領域のみであり,文字列そのものを格納する領域は確保されない.MLの場合は,メモリー領域の確保は自動的に行われるが,C言語では,プログラムで必用な領域を明示的に確保する必用がある.

配列を用いた文字列格納領域の確保

文字の並びとしての文字列は,文字の配列としても表現できる.さらに配列の大きさを指定すれば,その配列に対する領域が確保される.文字列を生成するプログラムは,生成する文字列が入る十分に大きな配列を以下の宣言によって確保する.

char result[100];

これによって,100文字分の領域が確保され,その先頭にresultという名前が付けられる.初期値も,以下のように与えることができる.

char result[100] = "SML#&C"

この場合,100文字確保された領域の先頭7文字分に指定した(終了文字'\0'を含む)文字列が格納される.

配列要素の参照

この様にして定義した配列の要素は,以下形の配列要素式で参照できる.

result[i];

配列の先頭要素は0番目であるから,この式ははi+1番目の文字を表す配列の要素の参照式,さらに配列宣言された変数は,その先頭要素のポインタとしても振る舞うので,その先頭要素は

*result

i+1番目の要素は

*(result + i)

として参照できる.また,ポインタ型(char *型)宣言された変数へ代入したり,ポインタ型を受け取る関数にも渡すことができる.以下は,種々の方法で文字数を数えた例である.

main(){
  char S1[100] = "SML#&C";
  char *S2;
  int i;
  for(i=0; S1[i] != '\0'; i++) {}
  printf("The value of i is: %d\n", i);
  for(S2 = S1, i=0; *(S2 + i) != '\0'; i++) {}
  printf("The value of i: %d\n", i);
  for(S2 = S1; *S2 != '\0'; S2++) {}
  printf("The  value of S2 - S1: %d\n", S2 - S1);
  S1[1] = 'm';
  printf("The value of S1: %s\n", S1);
  *(S1 + 1) = 'M';
  printf("The value of S1: %s\n", S1);
}

実行結果は以下の通りである.

c:\SMLSharpAndC\c>a.exe
a.exe
The value of i is: 6
The value of i: 6
The  value of S2 - S1: 6
The value of S1: SmL#&C
The value of S1: SML#&C

配列の要素の変更

上の例で学んだとおり,

char result[100];

のように宣言された変数resultは,そこから始まる領域が実際に確保されているので,その要素の領域を指し式を使って要素の変更を行うことができる.

result[i] = c;
*(result + 1) = c;

はともにresult配列のi+1番目の文字をcが表す文字に変更する文である.このように要素を表す式に代入するとその値が更新される.以下は簡単例である.

main(){
  char S1[100] = "SML#&C";
  S1[1] = 'm';
  printf("The value of S1: %s\n", S1);
  *(S1 + 1) = 'M';
  printf("The value of S1: %s\n", S1);
}

実行結果は以下の通りである.

c:\SMLSharpAndC\c>a.exe
a.exe
The value of S1: SmL#&C
The value of S1: SML#&C

これら式は,S1の値ではなく,S1が指す配列の要素を変更していることに注意.上記のコードを以下のように変更すると,システムエラーなどの未定義の動作をする.

main(){
  char S1 = "SML#&C";
  S1[1] = 'm';
  printf("The value of S1: %s\n", S1);
  *(S1 + 1) = 'M';
  printf("The value of S1: %s\n", S1);
}

この場合,S1はシステムが確保した文字定数へのポインタであり,その領域をユーザプログラムが書き換えることは許されない.

文字列の生成を含む種々の関数

文字列の生成を含む操作の基本を復習しておこう.

  • 文字列を生成するにはその結果を格納する文字配列として確保する必用がある.
  • 文字配列変数は文字列のポインタとして扱うことができる.
  • 文字配列の要素はs[i]や*(s + i)で参照できる.
  • 文字配列の要素式s[i]や*(s + i)に代入すれば,文字列の中の一文字を変更できる.

ここで,この1番目の原則を補足しておこう.文字配列宣言によって確保された領域の生存期間は,通常の変数宣言と同様,その関数が起動されてから終了するまでである.この事実は,文字配列宣言をした関数が終了すれば,その領域は参照できないことを意味する.したがって,文字列を生成するような関数は,通常呼び出し側で領域を確保し,そのポインターを関数に渡さなければならない.この点に注意し,以上の機能を使って文字列の生成を含む種々の関数を書いてみよう.

文字列をコピーする関数

コピー元文字列列とコピー先の領域を受け取り,コピー先文字列の文字を一文字づつコピー先の領域に書いていけばよい.以下はその一例である.

void strcopy1(char *to, char *from) {
  while (*from != '\0') {
    *(to++) = *(from++);
  }
  *to = '\0';
}

配列要素式を使えば以下のようなコードとなる.

void strcopy2(char to[], char *from) {
  int i = 0;
  while (*(from + i) != '\0') {
    to[i] = *(from + i);
    i++;
  }
  to[i] = '\0';
}

これらの関数は,第一引数に文字列を生成する領域が用意されていると仮定して,その領域に第二引数の文字列をコピーしている.したがって,これら関数の呼び出し元は,第一引数に,配列宣言で確保した文字配列のポインタを渡す必用がある.例えば,以下のように使用する.

main(){
  char *from = "SML#&C";
  char to[100];
  strcopy1(to, from);
  printf("The value of to:%s\n", to);
  strcopy2(to, from);
  printf("The value of to:%s\n", to);
}

実行結果は以下の通りである.

C:\SMLSharpAndC\c>a.exe
a.exe
The value of to:SML#&C
The value of to:SML#&C

英字をすべて大文字に変換する関数

コピー処理で文字列を書き出す前に,以前定義したtoUpper関数を使い大文字に変換すればよい.以下はその一例である.

void toUpperString(char *to, char *from) {
  while (*from != '\0') {
    *(to++) = toUpper(*(from++));
  }
  *to = '\0';
}

使用例とその実行結果は以下の通りである.

main(){
  char *from = "sml#&c";
  char to[100];
  toUpperString(to, from);
  printf("The value of to:%s\n", to);
}
C:\SMLSharpAndC\c>a.exe
a.exe
The value of to:SML#&C

二つの文字列を連結する関数

二つの文字列とコピー先の領域を受け取り,最初の文字列に続き2番目の文字列を連結してできる文字列をコピー先の領域に書き込む処理を行う.まず,最初の文字列をコピーし,次にコピー先ポインタ最初の文字列の('\0' を除いた)文字数分進め,そこに2番目の文字列をコピーすればよい.このために,まず.コピーした文字数を返すようにコピー関数を変更する.

int strcopy3(char to[], char *from) {
  int i = 0;
  while (*(from + i) != '\0') {
    to[i] = *(from + i);
    i++;
  }
  to[i] = '\0';
  return(i);
}

文字列の先頭は0であるから,この関数が返すiは'\0'文字を除いた文字数となる.二つの文字列を連結する関数は,この関数を2回呼び出すことによって実現できる.

int catString(char *result, char *s1, char *s2) {
  int i;
  i = strcopy3(result, s1);
  i = i + strcopy3(result + i, s2);
  return(i);
}

使用例とその実行結果は以下の通りである.

main(){
  char to[100];
  char *s1 = "Prgramming with  ";
  char *s2 = "SML# and C";
  catString(to, s1, s2);
  printf("The result of catString(s1, s2) is :%s\n", to);
}
C:\SMLSharpAndC\c>a.exe
a.exe
The result of catString(s1, s2) is :Prgramming with  SML# and C

以上を理解したら,文字列に関する演習問題をやってみよう.

ポインタと配列は,文字列処理に限らず種々の型と一緒に使用されるC言語の最も重要なデータ構造である.そこでポインタと配列の種々の扱い方を学ぼう.


[ トップ | 目次 | 前ページ | 次ページ ]