1

次のように定義された2つの文字列を連結したい:

char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };

私は、最初の文字列を実行し、'\0'記号を見つけて、代わりに 2 番目の文字列を開始する必要があることを理解しています。機能strcatは同じように機能していますか?

私が使用しているコード:

for (int i = 0; i < 6; i++) {
    if (hello[i] == '\0') {
        for (int j = 0; j < 9; j++) {
            int index = 5 + j;
            hello[index] = world[j];
        }
    }
}

コンパイル後、次のようなエラーが発生します。

* スタック破壊が検出されました * : ./run が終了しました

私は何を間違っていますか?

4

5 に答える 5

14

私の答えは、最初は文字列を正しく連結することに焦点を当てていません。むしろ、現状のコードのいくつかの問題に対処しようとし、C で物事を考える方法を明確にするのに役立つ背景となる考えをいくつか提供します。次に、文字列の連結について見ていきます。

始める前に、C ストリングの構造についていくつかの考えを述べます。

C で考えるのは、コンピュータ (CPU、メモリなど) のように考えるのとよく似ています。したがって、CPU でネイティブに機能するデータ型の場合、C には文字 (1 バイトのもの)、short (2 バイトの単語)、long (4 バイトの単語)、int、float、および double があり、CPU がネイティブに理解できるすべてのものがあります。そして、これらのものの配列またはこれらの型が存在するメモリ位置へのポインターを作成する機能。

では、文字列を作成するにはどうすればよいでしょうか。新しいタイプを作成しますか?

まあ、CPU は文字列を理解しないし、C も理解しないので... いずれにせよ、最も原始的な形式ではありません (C パーサーには、文字列に関連付けられた型はありません)。

しかし、文字列は非常に便利なので、文字列を決定する際にどのような文字列を使用するかについて、合理的に単純な概念が必要でした。

すべての C 文字列は、NUL 文字を含まないシーケンシャル メモリ内のバイトです。

NUL ( noolのように発音します) は、値が 0 のメモリ内のバイトの値に付ける名前です。C では、これは で表され\0ます。したがって、NULと書くと文字を意味します\0

注 1: これは、値がゼロのメモリ アドレスである C NULLとは異なります。

注 2: NULはもちろん、値が 48 の文字ゼロ ('0') ではありません。

そのため、文字列に対して機能する関数はすべて、char * (char ポインターの読み取り) が指すメモリ位置から開始します。文字列の終わりを示すバイトの値が 0 になるまで、バイト (文字) ごとに操作を実行し続けます。その時点で、文字列が終了し、操作の結果が返されるため、実行していることを停止することを願っています。

したがって、文字列を 0 で終わる文字の配列として定義すると、それ以上の人工的な文字列の概念を作成する必要が完全になくなります。

そして、それはまさに C が行うことです。使用する規則としてこの概念に落ち着きました。コンパイラは、二重引用符を使用して NUL で終了する文字の配列を宣言するための簡単なショートカットを提供するだけです。C には、文字列に特別な型はありません。

これらすべてを念頭に置いて、コードを見てみましょう。

char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };

シングル バイト (char) の 2 つの配列を宣言し、\0 で終了しました。これは、次の C ステートメントと同じです。

char hello[] = "Hello";
char world[] = ", World!";

64 ビット Intel コンピューターで実行されている Linux マシンでコンパイルすると、ペアと上記のペアの両方が次の (同一の) マシン コード出力を生成します。

Disassembly of section .data:
0000000000000000 <hello>:
   0:    48 65 6c 6c 6f 00                                   Hello.
0000000000000006 <world>:
   6:    2c 20 57 6f 72 6c 64 21 00                          , World!.

Linux を使用している場合は、それを試すことができます。以下の補遺としてコマンドを示します。

00どちらの場合も、最後にバイトが表示されることに注意してください。あなたの場合、配列で明示的に宣言されました。<hello>2 番目のケースでは、および<world>シンボルに対応するデータを発行するときに、C コンパイラによって暗黙的に挿入されました。

これで、それがどのように機能するかを理解できました。あなたはそれを見ることができます:

// This is bad: :-)

for (int i = 0; i < 6; i++) {
    if (hello[i] == '\0') {
        for (int j = 0; j < 9; j++) {
            int index = 5 + j;
            hello[index] = world[j];
        }
    }
}

上記のループは非常に奇妙です。実際には、多くの問題があります (たとえば、外側のループ内にネストされたforループが間違っています)。

しかし、問題を指摘するのではなく、基本的な正しい解決策を見てみましょう。

文字列をプログラムするとき、文字列の大きさはわかりません。そのため、文字列を処理するループi < N内のフォームの状態は、通常の方法ではありません。for

\0文字列 ( で終わる char 配列)内の文字をループする方法を次に示します。

 char *p; /* Points to the characters in strings */
 char str[] = "Hello";

 for ( p = str; *p != 0; p++ ) {
     printf("%c\n", *p);
 }

それでは、ここで何が起こっているのかを理解しましょう。

  for ( p = str; ...
        ^^^^^^^^^

p文字ポインタです。最初に、hello(プログラムを実行したときに変数 hello がメモリにロードされる場所) をポイントし、このメモリ位置 ( によって取得*p) の値が '\0' に等しいかどうかを確認します。

  for (p = str; *p != 0; ...)
                   ^^^^^^^

そうでない場合forは、条件が true であるため、ループを実行します。この場合*p=='H'、ループに入ります。

  for (p = str; *p != 0; p++)
                         ^^^

ここで、インクリメント/デクリメント/その他を最初に実行します。ただし、この場合、++演算子は;に後置されています。pso p(メモリ アドレス) は、ループ内のステートメントの END でその値をインクリメントします。これでループに入り、{ ... }その処理を実行し、最後に++が発生し、条件チェックに再び入ります。

  for (p = str; *p != 0; p++)
                ^^^^^^^

pしたがって、これは'H' 'e' 'l' 'l' 'o' '\0' のメモリ位置を指すように設定されることがわかります。そして '\0' にヒットすると終了します。

文字列の連結:

これで、"Hello" と ", World!" を連結したいことがわかりました。

最初に終わりを見つける必要がありHello、次に「、世界!」に固執し始める必要があります。最後まで:

for上記のループが hello の終わりを検出することはわかっています。そのため、最後に何もしない場合、最後の*p'\0' がどこにあるかを指しHelloます:

char str1[] = "Hello";
char str2[] = ", World";

char *p; /* points str1 */
char *q; /* points str2 */


for (p = str1; *p!=0; p++) {
  /* Skip along till the end */
}
/* Here p points to '\0' in str1 */

/* Now we start to copy characters from str2 to str1 */
for (q = str2; *q != 0; p++, q++ ) {
   *p = *q;
}

最初のパス*pでは str1 の最後にある '\0' を指していたことに注意してください*p = *q。最後に挿入する必要がある str1 から '\0' が完全に消えます。p最後にandをインクリメントqし、 while をループし続ける必要があることに注意してください*q != 0

ループが終了したので、最後に '\0' を付けます:

*p = 0;

そして連結です。

メモリに関する重要な部分

上記のアセンブラの出力に気付いた場合は、次のようになります。Hello\06 バイトを使用し、データ セグメントの, World\0アドレス0000000006(hello は 000000000 から開始) から開始しました。

つまり、str1[] のバイト数を超えて書き込み、十分なスペースがない場合 (理由は以下で説明します)、別のものに属するメモリの一部 (str2 []) 例えば;

十分なメモリがない理由は、初期化値を保持するのに十分な大きさの文字配列を宣言したためです。

char str[] = "Foofoo";

str を正確に 7 バイトにします。

しかし、初期化値だけでなく、より多くのスペースを与えるように C に依頼することもできstrます。例えば、

char str[20] = "Foofoo";

これによりstr20 バイトが得られ、最初の 7 バイトが "Foofoo\0" に設定されます。残りも通常同様に設定さ\0れます。

したがって、上記の逆アセンブルは次のようになります。

Disassembly of section .data:

0000000000000000 <str>:
   0:    48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00     Foofoo..........
  10:    00 00 00 00                                         ....

C では、コンピュータのように考えなければならないことを思い出してください。明示的にメモリを要求しない場合、メモリはありません。したがって、連結を行う場合は、明示的にそのように宣言したため、十分な大きさの配列を使用する必要があります。

  char foo[1000]; /* Lots of room */

mallocまたは、(別の投稿のトピック) を使用して、実行時にメモリの場所を要求します。

次に、実際のソリューションを見てみましょう。

concat.c:

#include <stdio.h>

char str1[100] = "Hello";
char str2[] = ", World!"; /* No need to make this big */

int main()
{
    char *p;
    char *q;

    printf("str1 (before concat): %s\n", str1);

    for (p = str1; *p != 0; p++) {
        /* Skip along to find the end */
    }

    for (q = str2; *q != 0; p++, q++ ) {
        *p = *q;
    }
    *p = 0; /* Set the last character to 0 */

    printf("str1 (after concat): %s\n", str1);

    return 0;
}

Linux での逆アセンブル:

上記を JUST オブジェクト ファイルにコンパイルし、それを実行可能ファイルにリンクしないと、混乱が少なくなります。

  gcc -c concat.c -o concat.o

オブジェクト ダンプを使用して concat.o を逆アセンブルできます。

  objdump -d concat.o

printf ステートメントを処理するダンプには、多くの不要なコードがあることに気付くでしょう。

   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    48 83 ec 10              sub    $0x10,%rsp
   8:    be 00 00 00 00           mov    $0x0,%esi
   d:    bf 00 00 00 00           mov    $0x0,%edi
  12:    b8 00 00 00 00           mov    $0x0,%eax
  17:    e8 00 00 00 00           callq  1c <main+0x1c>

したがって、それを取り除くには、コード内の printf をコメントアウトするだけです。次に、次の行を使用して再コンパイルします

gcc -O3 -c concat.c  -o concat.o

また。これで、はるかにクリーンな出力が得られます。

これ-O3により、フレーム ポインター (かなり後の主題) 関連の命令の一部が削除され、アセンブラーはコード ベースに固有のものになります。

上記を使用してコンパイルし、次を使用してダンプした場合の concat.o 出力は次のとおりです。

objdump -S -s concat.o


concat.o:     File format elf64-x86-64

Contents of section .text:
 0000 803d0000 000000b8 00000000 740b6690  .=..........t.f.
 0010 4883c001 80380075 f70fb615 00000000  H....8.u........
 0020 84d2741d b9000000 000f1f80 00000000  ..t.............
 0030 4883c101 88104883 c0010fb6 1184d275  H.....H........u
 0040 efc60000 31c0c3                      ....1..
Contents of section .data:
 0000 48656c6c 6f000000 00000000 00000000  Hello...........
 0010 00000000 00000000 00000000 00000000  ................
 0020 00000000 00000000 00000000 00000000  ................
 0030 00000000 00000000 00000000 00000000  ................
 0040 00000000 00000000 00000000 00000000  ................
 0050 00000000 00000000 00000000 00000000  ................
 0060 00000000 2c20576f 726c6421 00        ...., World!.
Contents of section .comment:
 0000 00474343 3a202844 65626961 6e20342e  .GCC: (Debian 4.
 0010 342e352d 38292034 2e342e35 00        4.5-8) 4.4.5.
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 14000000 1c000000  ................
 0020 00000000 47000000 00000000 00000000  ....G...........

Disassembly of section .text:

0000000000000000 <main>:
   0:    80 3d 00 00 00 00 00     cmpb   $0x0,0x0(%rip)        # 7 <main+0x7>
   7:    b8 00 00 00 00           mov    $0x0,%eax
   c:    74 0b                    je     19 <main+0x19>
   e:    66 90                    xchg   %ax,%ax
  10:    48 83 c0 01              add    $0x1,%rax
  14:    80 38 00                 cmpb   $0x0,(%rax)
  17:    75 f7                    jne    10 <main+0x10>
  19:    0f b6 15 00 00 00 00     movzbl 0x0(%rip),%edx        # 20 <main+0x20>
  20:    84 d2                    test   %dl,%dl
  22:    74 1d                    je     41 <main+0x41>
  24:    b9 00 00 00 00           mov    $0x0,%ecx
  29:    0f 1f 80 00 00 00 00     nopl   0x0(%rax)
  30:    48 83 c1 01              add    $0x1,%rcx
  34:    88 10                    mov    %dl,(%rax)
  36:    48 83 c0 01              add    $0x1,%rax
  3a:    0f b6 11                 movzbl (%rcx),%edx
  3d:    84 d2                    test   %dl,%dl
  3f:    75 ef                    jne    30 <main+0x30>
  41:    c6 00 00                 movb   $0x0,(%rax)
  44:    31 c0                    xor    %eax,%eax
  46:    c3                       retq
于 2013-05-29T07:38:28.347 に答える
0

その配列の範囲外にデータを保存しようとしています。

char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };

に何char個収納できますhelloか?確認しましょう。

#include <stdio.h>
int main(void) {
    char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    printf("%zu\n", sizeof hello);
}

出力: 6. これはhello[0]、 ~ ~hello[5]が有効なインデックスであることを意味します。hello[6]以上は無効です。次のように、連結の結果を格納するのに十分な大きさの配列を宣言する必要があります。

#include <stdio.h>
#include <string.h>
int main(void) {
    char hello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    char world[] = { ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' };

    /* Don't forget to add 1 for NUL */
    char hello_world[strlen(hello) + strlen(world) + 1];

    strcpy(hello_world, hello);
    strcat(hello_world, world);
    puts(hello_world);
}
于 2013-05-29T15:16:33.110 に答える