次のコードスニペットがセグメンテーション違反を引き起こしている理由を理解しようとしています。
void tokenize(char* line)
{
char* cmd = strtok(line," ");
while (cmd != NULL)
{
printf ("%s\n",cmd);
cmd = strtok(NULL, " ");
}
}
int main(void)
{
tokenize("this is a test");
}
Strtok()が実際には文字列リテラルをトークン化しないことは知っていますが、この場合、line
は内部的にchar
の配列である文字列"this is a test"
を直接指します。配列にコピーせずにline
をトークン化するものはありますか?
問題は、文字列リテラルを変更しようとしていることです。これを行うと、プログラムの動作が未定義になります。
文字列リテラルを変更することは許可されていないと言うのは単純化しすぎです。文字列リテラルがconst
であると言うのは誤りです。彼らはそうではありません。
警告:余談が続きます。
文字列リテラル"this is a test"
は、タイプchar[15]
の式です(長さは14、終了'\0'
は1)。これを含むほとんどのコンテキストでは、このような式は、型char*
の配列の最初の要素へのポインターに暗黙的に変換されます。
文字列リテラルによって参照される配列を変更しようとする動作は未定義です-それがconst
であるためではなく(そうではありません)、C標準が特にそれが未定義であると言っているためです。
一部のコンパイラでは、これを回避できる場合があります。コードは、リテラルに対応する静的配列を実際に変更する可能性があります(後で大きな混乱を引き起こす可能性があります)。
ただし、最新のコンパイラのほとんどは、配列を読み取り専用メモリ(物理ROMではなく、仮想メモリシステムによる変更から保護されているメモリ領域)に格納します。このようなメモリを変更しようとすると、通常、セグメンテーション違反とプログラムのクラッシュが発生します。
では、なぜは文字列リテラルconst
ではないのですか?あなたは本当にそれらを変更しようとすべきではないので、それは確かに理にかなっています-そしてC++は文字列リテラルをconst
にします。その理由は歴史的です。 const
キーワードは、1989 ANSI C標準によって導入される前は存在していませんでした(ただし、それ以前に一部のコンパイラによって実装された可能性があります)。したがって、ANSI以前のプログラムは次のようになります。
#include <stdio.h>
print_string(s)
char *s;
{
printf("%s\n", s);
}
main()
{
print_string("Hello, world");
}
print_string
がs
が指す文字列を変更することを許可されていないという事実を強制する方法はありませんでした。 ANSICで文字列リテラルをconst
にすると、既存のコードが壊れてしまい、ANSIC委員会はそれを避けるために非常に懸命に努力しました。それ以来、そのような言語の変更を行う良い機会はありませんでした。 (C++の設計者、主にBjarne Stroustrupは、Cとの下位互換性についてそれほど心配していませんでした。)
あなたが言ったように、文字列リテラルを変更することはできません。これはstrtok
が行うことです。あなたはしなければならない
char str[] = "this is a test";
tokenize(str);
これにより、配列str
が作成され、this is a test\0
で初期化され、その配列へのポインタがtokenize
に渡されます。
コンパイル時の定数文字列をトークン化しようとすると、セグメンテーション違反が発生するという非常に正当な理由があります。定数文字列は読み取り専用メモリにあります。
Cコンパイラはコンパイル時定数文字列を実行可能ファイルに焼き付け、オペレーティングシステムはそれらを読み取り専用メモリ(* nix ELFファイルの.rodata)にロードします。このメモリは読み取り専用としてマークされており、strtokは渡した文字列に書き込むため、読み取り専用メモリへの書き込みでセグメンテーション違反が発生します。
"...内部的にはchar
"の配列であるということで、どのような点を指摘しようとしていますか?
事実 "this is a test"
は内部的にchar
の配列であり何も変更しません。それはまだ文字列リテラルです(すべての文字列リテラルはcharの変更不可能な配列です)。 strtok
は、文字列リテラルをトークン化しようとします。これがクラッシュする理由です。
これについてはきっと殴られるでしょう...しかし、「strtok()」は本質的に安全ではなく、アクセス違反などの傾向があります。
ここで、答えはほぼ確実に文字列定数を使用しています。
代わりにこれを試してください:
void tokenize(char* line)
{
char* cmd = strtok(line," ");
while (cmd != NULL)
{
printf ("%s\n",cmd);
cmd = strtok(NULL, " ");
}
}
int main(void)
{
char buff[80];
strcpy (buff, "this is a test");
tokenize(buff);
}
Strokは、トークン化するために最初の引数を変更します。したがって、タイプがconst char *
であり、変更できないため、リテラル文字列を渡すことはできません。したがって、未定義の動作になります。文字列リテラルを変更可能なchar配列にコピーする必要があります。
トークンがNULLになった後、printfを使用してトークン(この場合はcmd
)を印刷しようとしたときに、セグメンテーション違反エラーが発生しました。