[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
今回は、文字列に関する質問で、よくあるものをまとめてみました。
char
型の変数配列を使って文字列を表現するのが伝統的仕様となっています。文字列
は、単に
char
の配列変数に順番に文字が格納され、'¥0'が現れた所で終了とみなす、というル
ールで扱われているに過ぎません。「文字列型」という特別な型があるわけでは
ないのです。
char b[100];
b = "abc";
"abc"
をセットしたいのだと思います。
しかし、C言語の代入演算子'
=
'には、文字列を複写する機能はありません。
strcpy(b, "abc");
を使えば期待通りの動作をするでしょう。
int compare(char *s1, char *s2)
{
if (s1 == s2) {
printf("一致しました¥n");
return 0;
} else {
printf("差がありました¥n");
return 1;
}
}
関数
compare
に渡される変数
s1
、
s2
は
char
へのポインタです。
if
の条件判断部分は、二つのポインタの値を比較しているだけで、ポインタの指す
先を比較しているわけではありません。従って、たまたま二つのポインタが同じ
アドレスを指している時だけ、この関数は期待通りに動作します。二つのポイン
タが指している文字列が内容が同じであっても、ポインタそのものの値が異なる
場合には、期待を裏切る結果になるでしょう。
文字列の内容を比較したい場合には標準関数の
strcmp
を使います。
int compare(char *s1, char *s2) { if (strcmp(s1, s2) == 0) { printf("Match¥n"); return 0; } else { printf("Differ¥n"); return 1; } }
"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; } } }
toupper
、
tolower
という標準関数が用意されています。これらの関数を使って、一文字ずつ処理す
るような関数を自作するのは簡単です。
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); }
void foo(void) { char str1[] = "string"; ... }
ANSIのC言語の規格では、自動変数の配列は初期化できることになっていますが 、以前のコンパイラは、なぜかANSI準拠と堂々と書いて販売されていたにもかか わらず、自動変数の配列を初期化するようなコードはエラーになってしまうもの がありました。
ANSI Cが広まる前の、いわゆるTraditionalなC言語の仕様では、自動変数の配
列は初期化できませんでした。従って、初期化が必要な場合には、次のように関
数の最初で
strcpy
などを使って初期化に相当する処理を書くか、あるいは
static
と指定することによって初期化できるようにする必要がありました。
char str1[7]; strcpy(str1, "string");ただし、
static
の変数はプログラム実行時の最初に一度だけ初期化され、二度目以降はその関数
が呼ばれても直前の状態のままなので、自動変数を初期化するような処理とは若
干異なる振る舞いとなります。
strcat
とはどういう処理をする関数なのか。なぜこのような奇妙な名前が付いているのか。
strcat
は、既にある文字列の後に、新たな文字列を連結する処理をします。catというの
はconcatenateの略と言われています。
UNIXというOSには、catというコマンドがあります。このコマンドは、引数に与
えたファイルを連結するというものです。例えばcat f1 f2 f3というコマンドは、
f1、f2、f3の内容をこの順番に標準出力に出力します。catを普段から使い慣れて
いる人にとっては、
strcat
という名前は大変分かりやすいものなのかもしれません。
strcat
がうまく動かない。以下のプログラムを書いたら、変な答えが返ってきた。
char *s1 = "Hello, "; char *s2 = "world!"; strcat(s1, s2);
strcat
は、与えられた文字列を連結する関数ですが、追加する文字列を最初の引数に指
定した文字列の後に何も考えずに格納しようとします。従って、この関数を使う
場合は、連結した結果を格納するための領域を、プログラマーが責任をもって確
保しておかなければなりません。すなわち、
s1
の指す先は、
s2
をその後に連結しても問題がないだけの十分な大きさの領域である必要があります。
例のプログラムは、
s1
が指している先は文字列リテラルです。規格上、文字列リテラルは変更してはな
らない領域で、変更した結果は未定義となります。従って、変な答が返ってきた
わけです。
このトラブルを解決するための一つの簡単な方法は、連結した結果を格納する 領域を文字配列とすることです。
char s1[20] = "Hello, ";
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
の値、すなわち文字が入った配列の先頭アドレスとなります。このアドレスは、
関数を出る直前までは意味がありますが、関数を出てしまった所で、変数自体が
消滅してしまうため、意味を失ってしまうのです。
値に応じて文字列を戻すためには、次のような書き方をします。
static char tmp[16];
と定義しておけば、この変数はプログラムが終了するまで存続します。
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); }
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); /* 使いおわったので、後始末 */ } }
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]
と定義すれば問題は解決します。
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]; }
printf("%s", s);
のように呼び出す書き方があるが、
printf(s);
ではなぜいけないのか。
"%s"
という文字列を表示させるつもりで次のコードを実行すると、多分思った通りに
表示されないし、最悪の場合はプログラムが暴走するかもしれません。
s = "%s"; printf(s);
printf
を使って
'%'
を出力するにはどうすればよいか。
'¥%'
としてもうまくいかなかった。
'%%'
と書けば
'%'
が表示されます。
atoi
の逆の処理をしたい。数を文字列に変換するにはどうすればよいか。
itoa
という関数はなかった。
sprintf
を使えば簡単にできます。ただし、結果を格納することのできる領域はあらかじ
め確保しておかねばなりません。
sprintf
の処理は、仕様が汎用的であり、また様々な書式指定に対応するため、コードが
複雑になるし、また、処理自体も遅くなると指摘する人もいます。これは一見正
論のようですが、
sprintf
の代わりに
itoa
を作って使った場合に節約できるコードの量と速度が向上する割合を考えてみる
と、無駄な努力になってしまう場合もあるだろうと言わざるを得ません。とりあ
えず
sprintf
を使っておくのが、保守性の見地からも妥当なことが多いでしょう。
char *p = "Hello, world!"; p[0] = tolower(p[0]);
文字列の内容を変更したいのであれば、配列を用意すべきです。
char a[] = "Hello, world!";(参考) Q&A(7)-3【未定義】, Q&A(7)-4【未定義の例】
gets
を使ったコードを書いたら、先輩から「危ないから
fgets
を使えと指示された。一体
gets
の何が危ないのか。
gets
は、読み込む文字列の長さが、与えるバッファの大きさよりも短いということを
前提としています。多くの場合、一行の長さはたかが知れているので、これは問
題ないわけですが、希には、バッファの大きさよりも長い文字列があるかもしれ
ません。データ中に1MBの長さの文字列がないとは誰も保証できないのです。
このような予期しないデータに遭遇した時、getsはどのような被害をもたらす
か予想できません。これに対して、
fgets
を使うと、あらかじめ指定した大きさより長い文字列があった場合には、指定し
た長さ(厳密にいえば長さ-1)までをとりあえず読み込んでくれるので、プログラ
ムは安全に処理することができます。