web-dev-qa-db-ja.com

strtokセグメンテーション違反

次のコードスニペットがセグメンテーション違反を引き起こしている理由を理解しようとしています。

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をトークン化するものはありますか?

13
user1162954

問題は、文字列リテラルを変更しようとしていることです。これを行うと、プログラムの動作が未定義になります。

文字列リテラルを変更することは許可されていないと言うのは単純化しすぎです。文字列リテラルが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_stringsが指す文字列を変更することを許可されていないという事実を強制する方法はありませんでした。 ANSICで文字列リテラルをconstにすると、既存のコードが壊れてしまい、ANSIC委員会はそれを避けるために非常に懸命に努力しました。それ以来、そのような言語の変更を行う良い機会はありませんでした。 (C++の設計者、主にBjarne Stroustrupは、Cとの下位互換性についてそれほど心配していませんでした。)

23
Keith Thompson

あなたが言ったように、文字列リテラルを変更することはできません。これはstrtokが行うことです。あなたはしなければならない

char str[] = "this is a test";
tokenize(str);

これにより、配列strが作成され、this is a test\0で初期化され、その配列へのポインタがtokenizeに渡されます。

2
Seth Carnegie

コンパイル時の定数文字列をトークン化しようとすると、セグメンテーション違反が発生するという非常に正当な理由があります。定数文字列は読み取り専用メモリにあります。

Cコンパイラはコンパイル時定数文字列を実行可能ファイルに焼き付け、オペレーティングシステムはそれらを読み取り専用メモリ(* nix ELFファイルの.rodata)にロードします。このメモリは読み取り専用としてマークされており、strtokは渡した文字列に書き込むため、読み取り専用メモリへの書き込みでセグメンテーション違反が発生します。

2
Adam Mihalcin

"...内部的にはchar"の配列であるということで、どのような点を指摘しようとしていますか?

事実 "this is a test"は内部的にcharの配列であり何も変更しません。それはまだ文字列リテラルです(すべての文字列リテラルはcharの変更不可能な配列です)。 strtokは、文字列リテラルをトークン化しようとします。これがクラッシュする理由です。

2
AnT

これについてはきっと殴られるでしょう...しかし、「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);
}
1
paulsm4

Strokは、トークン化するために最初の引数を変更します。したがって、タイプがconst char *であり、変更できないため、リテラル文字列を渡すことはできません。したがって、未定義の動作になります。文字列リテラルを変更可能なchar配列にコピーする必要があります。

1
user529758

トークンがNULLになった後、printfを使用してトークン(この場合はcmd)を印刷しようとしたときに、セグメンテーション違反エラーが発生しました。

0
Ninjaxor