2011年2月6日日曜日

C言語の文字列コピーが危険でござる。

前にもちろっと書いたと思いますが、C言語の勉強をしております。
C言語のむずかしいところっていうのは
概念が
「アドレス」
っていうところですね。
「アドレス」と「値(AとかBとかの値)」だと扱いが違うので
頭の中がこんがらがってたまに「うはー!!(汗)」ということがおこります。
うはー!!ってなによ?と思われそうですが、値をいれたいのにアドレス入れちゃったとか
アドレスに変な値をいれて変なアドレスになってしまう
つまり
メモリ破壊が発生します。

今のパソコン様はかしこいので壊れないですが、昔のものはちょっと危ないそうです。
昔といっても、相当昔なのであんまりこのへんは考えなくてもいいです。が!!
メモリに厳密にならないといけない世界では超重要な世界です。
これを理解しないと、いかんみたいです。C言語は。

というわけで、
身近なところからせめていくわけです。
文字列コピーが危険というお話しをします。でも知識がにわかです。
って言い訳をして逃げるあたりがかっこわるい。

C言語における文字列の扱いは難しいです。
文字型の配列を使用する もしくは 文字型のポインタを利用するしかないです。
(って考えると、JavaのString型って本当にありがたい)

問題なのが 配列 は 値
ポインタは アドレス

というわけで「概念」が違うのです。この違いって話すとむずかしいっつか、
私もよくわかってない箇所が多いので、
日本語と英語ぐらいにおもってください。
故に、これはダメな例です。

#include

void main() {
char work[5] = "ABC";

char *copy;

/* これ、大変 */
*copy = work;

printf("orignal:%s",work);
printf("copy:%s",copy);

};


コンパイルエラーで怒られます。なぜか?
アドレスに値を渡そうとしているので通じないのです。
英語の人にひたすら日本語で話しかけてる感じですかね。通じません。

だから、コピーの際には翻訳が必要になるのです。

配列にあわせてやるとか、ポインタにあわせてやるとか。
で、ここで登場するのがstrcpyという関数。

C言語は文字列コピーの際にstrcpyという関数を使います。
軽く書いたらこんな感じです。

#include
#include

void main() {
char *work;
work = "ABC";
char copy[50];

/* strcpy (char *s1,const char *s2)
/* *s1:複写先
/* *s2:複写元 */
strcpy(copy,work);

printf("original:%s\n",work);
printf("copy:%s\n",copy);

};


コメントでちょこっとかいてますが、これ、アドレスをコピーするんですよね。
が、このstrcpyとやらはやっかいなことに
「コピー先のこと、考えてません」

ためしにさっきのコードをこうしてみましょう。


#include
#include

void main() {
char *work;
work = "copy de copy";
char copy[10];

/* strcpy (char *s1,const char *s2)
/* *s1:複写元
/* *s2:複写先 */
strcpy(copy,work);

printf("original:%s\n",work);
printf("copy:%s\n",copy);

};


コンパイルは通りますが、実行したら即終了します。
コピー元のほうが、コピー先より大きいからです。サイズが。
strcpy関数はこれを考えてないので死にます。

そういう時の回避としてstrncpyを使用します。

strncpy(copy,work,10);

最後に「サイズ」を指定するので死ぬことは防げます。
かしこいことに、大きくても、文字列の終わりである「\0」は絶対確保してくれるので元が長くても少なくとも落ちることはありません。
が、変な文字にはなります。サイズはあたまにいれておきましょう。

「って、おめー!!文字数がきまったぶんだけってのはむずかしいべや!!」(何弁?)

ってもあるかとおもいます。
そういう時は、strcpyを自作すればいいだけです。ちょっとめんどいですけど。
要するにコピーなんで、
配列を1文字ずつみてやって、アドレスに渡してやればいいだけですね。
で、終了文字がやってきたら、終了すればいいのです。
コードにするとこう。

文字型同士(値同士)

#include

void main(){
char work[50] = "ABC";
char copy[50];
int i;

for(i = 0;work[i] != '\0'; i++) {
copy[i] = work[i];
};

printf("orignal:%s\n",work);
printf("copy:%s\n",copy);

};


ポインタと値でやってみた。

#include

void main() {
char *work;
work = "ABC";
char copy[10];
int i;
int j;

for(i = 0;work[i] != '\0';i++) {
copy[i] = work[i];
};

printf("orignal:%s\n",work);

/* \0までコピーしているので、直前の長さまで表示 */
printf("copy:");
j = 0;
while(j < i) {
printf("%c",copy[j]);
j++;
};
printf("\n");


};


上記2つは厳密には間違いですね。
だって、strcpy関数はポインタとポインタを渡すので
(アドレスをわたすってことね)
というわけで正しく書くと・・・


#include
#include
#include

void main(){
char *work;
char *copy;
int i;

work = "ABCDEFGHIJK";
copy = malloc (strlen(work) +1);

i = 0;

while(work[i] != '\0') {
*(copy+i) = *(work +i);
i++;

};
printf("orignal:%s\n",work);
printf("copy:%s\n",copy);

free(copy);

};


となります。
じつはこれ、もうちょっと簡単にかけるってことを
コード書いた後に調べて愕然としました(涙)
苦労したのに。
mallocとか
値とアドレスの渡しにはつまづかなくて、ずっとおちてたのは
iの初期化を忘れてたという
超凡ミス・・・。
だって、サクラエディタでやってるしなぁ・・・。

さて、

スペシャル簡易コード、
そのあたりは
このページの下のあたりにのってます。ご参考にどうぞ。


あー、それにしてもこういう時間は楽しい。本当はsplit関数自作したかったぜ。


追記
最初のコードと最後のコードにミスがあってツイッターでフルボッコにあったでござる。
文字列ってこわいわーね。

最初の copy = work はただしいので通ります。
*copy = work がとおりません。

orignalのスペルちがいました。originalですね。英語だめっす!!

えーと、最後のコード、mallocしているので
最後にfreeが必要です。

みなさんありがとう。


まだまだつっこみがきた。
sizeof って()つけたらだめなんですね。
そしてsizeofでなくて、文字数とるならstrlenなんですね。
あう、string.h使わないとだめなんか。あうあうあ。

ご参考 → http://www.ne.jp/asahi/hishidama/home/tech/c/sizeof.html

あと、最初のところ、アドレスに値を渡してどーん!!ですね。


ツッコミ歓迎ですー。勉強になるので!!
@morihimeまでどうぞ。

・・・でも「初心者しね」とか言わないでね(けっこう凹むんで)