計算機言語 II 第 5 回 文字列とポインタ
http://www.math.u-ryukyu.ac.jp/~suga/gengo/2018-2/05.pdf
1
文字列とポインタ
これまでに次のことを学びました.
1. プログラム実行時のオブジェクト(変数や関数などの識別子でソースコード内では識別子(名前)で区別 されるもの)がある場所のことを総称して「ポインタ」という.
2. 配列型変数では,配列名の参照は配列の先頭要素へのポインタが得られる. 3. Cではポインタを値にとる変数,「ポインタ型変数」が存在する.
4. Cでは, (Ascii)文字列は,’\0’で終わるchar型配列である. 5. Cでは,ポインタ変数を利用して,参照渡しを実現する.
今回は上の2., 3., 4.,の内容について詳しく述べます. すなわち,配列名の参照は配列の先頭要素へのポイ ンタ値が得られる;ということ,文字列はchar型の配列であるということ, ポインタを利用した参照渡しがで きるということを利用して,文字列操作をする処理を書くのが, Cの開発当初の考え方だったのです.
ただし, 文字列となっていますが, 実際には’\0’で終わるバイト列の話とも重なります. すなわち,「文字 列をコピーする」などの動作は, 文字列の終端を表す’\0’を目印に, 順に代入していくという動作ですから,
Ascii文字でなくても, UTF–8 な文字列でも同じです. Cの解説本に書かれているのは, Ascii文字の内容し
かありません. これは,以下の理由があるからです.
• そもそもの原典, Kernighan – Ritchie, The C Programing LanguageにAscii文字列のポインタや配 列を利用した操作が多く記述されており, それがほぼそのまま Unixという OS の開発で利用されて いた.
• Ascii 文字列では,「文字数=配列の大きさ−1」が成立して易しいが,他の文字コードでは,配列の大
きさと文字数の関係が複雑になる.
オブジェクトのアドレス int main()
{
int a;
...
}
のようなプログラムを実行すると,識別子aに対してメモリ上のint型を確保する領域が取られ,変数aへの 参照は, そのアドレスを利用すると述べました. 実行中は,aのアドレスを変更することはできません. なぜな ら,変更するとaの値を参照できなくなるからです.
すなわち,上のプログラムではaの値は代入によって変更できますが,&aの値は変更できないのです. この ことから,実行中のプログラムの中には, 実行中に変更することができない「定数領域」と呼べるものが存在 します.
今回の教科書のプログラム, List 11–1では, char str[] = "ABC";
char *ptr = "123";
という宣言があります. このとき,変数としては,次の5つが宣言されています. ()内はその初期値です. str[0](=’A’), str[1](=’B"), str[2](=’C"), str[3](=’\9’), ptr(=&"123")
すなわち, str[0], str[1], str[2], str[3], ptrの値は,変数として宣言されていますから, その値を
代入によって変更できますが,配列名strや,文字列リテラル"123"あるいはそれへのポインタ&"123"は定 数です.
ポインタ変数の宣言部分
char *ptr = "123";
は,次のように読みます.
1. ptrは,文字型を指すポインタ変数である.
2. ptrの初期値は, 文字列リテラル”123” の先頭アドレスである.
3. このように宣言すると,文字列リテラル”123”は定数である.
このうち3. の部分は, Cの初期版では,「定数である」という部分が曖昧でした. 従って, 古いCの解説本で は,定数でないとしているものがあるかも知れません. プログラミング言語は,時代に従って仕様が変化して行 きますので,古い本を読むときには注意が必要です.
ptrは変数ですから, ポインタ値を代入して変更できます. List 11-1.cは次のページのように変更して実行 してみてください. さらに, List 11-2, List 11-3も実行してください. List 11-2でエラーになるのは,配列名 という定数値に別の値を代入しようとするからです. 定数は,変更できません.
教科書p. 289の下の方に述べられていますが, List 11–3のようなプログラムを書いた場合,代入文
p = "456";
を実行した後で, 再びpの初期値の ”123” の先頭アドレスをプログラムで参照することはできません. すな わち,このプログラムでは,文字列リテラル”123” は上の代入文の後は,プログラムが終了するまで,どこから も参照することができない無駄なメモリ領域を占めることになります.
このような,「プログラムの終了まで解放できない無駄なメモリ領域」を「メモリリーク(memory leak,メ モリ漏れ)」と言います. この講義で扱うような小さなプログラムでは問題になりませんが,ネットワークサー
ム全体のメモリを食いつぶすことも起こります(実際に過去にそのようなことがあった). 従って, List 11–3 のようなプログラムを書いてはいけません.
/* List 11-1.c */
#include <stido.h>
int main() {
char str[] = "ABC";
char *ptr = "123";
printf("str = \"%s\"\n", str);
printf("ptr = \"%s\"\n", ptr);
ptr = str; /* ptr は str[0] を指す */
printf("ptr = \"%s\"\n", ptr);
return 0;
}
”123”が定数であることを確かめます. List 11–1を次のように変えると,実行時エラーになります.
/* List 11-1A.c */
#include <stido.h>
int main() {
char str[] = "ABC";
char *ptr = "123";
ptr[0] = ’Z’;
printf("str = \"%s\"\n", str);
printf("ptr = \"%s\"\n", ptr);
return 0;
}
しかし,次は,エラーになりません. ptr[0] = str[0]で,str[0]は変数と宣言されているからです.
/* List 11-1B.c */
#include <stido.h>
int main() {
char str[] = "ABC";
char *ptr = "123";
printf("str = \"%s\"\n", str);
printf("ptr = \"%s\"\n", ptr);
ptr = str; /* ptr は str[0] を指す */
ptr[0] = ’Z’;
printf("ptr = \"%s\"\n", ptr);
return 0;
}
2
ポインタ変数の配列
教科書では,「文字列の配列」というタイトルになっていますが,ここで解説すべきは, ポインタ変数の配列 です. これをうまく利用することで「効率的なプログラムが書ける」というのが, Cのセールスポイントです.
教科書p. 290, List 11–4に文字列の配列を,「2重配列」として宣言したものと, 文字列へのポインタ変数
の配列として宣言したプログラムの例があります. ソースコードの中で, 次の部分が文字列へのポインタ変数 の配列の宣言と初期化になります.
char *p[] = {"PAUL", "X", "MAC"};
これは,次のように読みます.
• p[0], p[1]...は文字へのポインタを保持する変数である. 特にp[]はポインタを要素とする配列変
数である.
• p[0] = &"PAUL", p[1]= &"X", p[2] = &"MAC"と初期化されている.
この宣言で注意すべきは, p[0], p[1], p[2]は変数であるということです. すなわち,値が変えられるの
です. 例えば, ”PAUL”と”X”の指す順を変更することができるのです. 同じことを2重配列 str[][]で実
現しようとすると, 途中に文字列のコピーという動作を書く必要がありますが,ポインタ変数にすると, 単にポ インタ値の交換だけで済むのです. 教科書では,a とpが似ているように述べられていますが,「全く異なる」
という認識を持って下さい.
List 11-4を次のように変更して実行してみてください. プログラムで注意して欲しいのは,「初期値のリテ ラルの場所は変えていない」です. あくまでも,ポインタ変数の指す場所が変わっているだけです. また,pで はこのようなことができますが,aではできないのです. a[0]のは最初の文字列”LISP”の先頭アドレスです が,これは定数で変更できません.
/* list 11-4.c */
#include <stdio.h>
int main() {
int i;
char a[][5] = {"LISP", "C", "Ada"};
char *p[] = {"PAUL", "X", "MAC"};
char *q;
for ( i = 0; i < 3 ; i++){
printf("a[%d] = \"%s\"\n", i, a[i]);
}
for ( i = 0; i < 3 ; i++){
printf("p[%d] = \"%s\"\n", i, p[i]);
}
q = p[0];
p[0]=p[1];
p[1] = q;
for ( i = 0; i < 3 ; i++){
printf("p[%d] = \"%s\"\n", i, p[i]);
}
return 0;
}
教科書p. 292からこの章の終わりまで, Kernighan – Ritchie の本の中身の解説のようなものになってい
ます. 以前に述べましたが,これらは, 文字列操作関数としてライブラリにある内容で,初期のUnixで用いら れていたものです. 現在のOS(ライブラリ)では, 実行時エラーがセキュリティホールの原因になるという理 由で,文字列のコピーなどの操作は,教科書に述べられているような単純な内容ではなくなっています.
レポート問題: 締め切り 11月8日(木)
送り先は, [email protected] 1. 教科書,演習11-1. 件名Enshu 11-1.
テキストの置き場所: ftp://ftp.math.u-ryukyu.ac.jp/pub/gengo/2018-2/