1. ホーム
  2. c

[解決済み] C言語の文字列を反転させる

2022-03-05 06:35:35

質問

文字列の逆引きプログラムを開発しました。これを行うためのより良い方法があるかどうか、また私のコードに潜在的な問題があるかどうか、疑問に思っています。私はC言語の高度な機能を練習したいと思っています。

char* reverse_string(char *str)
{
    char temp;
    size_t len = strlen(str) - 1;
    size_t i;
    size_t k = len;

    for(i = 0; i < len; i++)
    {
        temp = str[k];
        str[k] = str[i];
        str[i] = temp;
        k--;

        /* As 2 characters are changing place for each cycle of the loop
           only traverse half the array of characters */
        if(k == (len / 2))
        {
            break;
        }
    }
}

解決方法は?

C言語の高度な機能を練習したいなら、ポインターはどうでしょう? マクロやxor-swapも混ぜて楽しもう!

#include <string.h> // for strlen()

// reverse the given null-terminated string in place
void inplace_reverse(char * str)
{
  if (str)
  {
    char * end = str + strlen(str) - 1;

    // swap the values in the two given variables
    // XXX: fails when a and b refer to same memory location
#   define XOR_SWAP(a,b) do\
    {\
      a ^= b;\
      b ^= a;\
      a ^= b;\
    } while (0)

    // walk inwards from both ends of the string, 
    // swapping until we get to the middle
    while (str < end)
    {
      XOR_SWAP(*str, *end);
      str++;
      end--;
    }
#   undef XOR_SWAP
  }
}

A ポインタ (例 char * として、右から左に読みます。 へのポインタ char ) は、C言語のデータ型で、以下のように使用されます。 他の値のメモリ上の位置を参照するために使用します。 この場合 が存在する場所 char が格納されています。 私たちは 参照 ポインタの前に * この値は に格納されている。 つまり str*str .

ポインタを使った簡単な算術演算ができる。 インクリメント(またはデクリメント)するとき ポインターを移動して、次の(または前の)ポインターを参照します。 メモリ上のそのタイプの値に対応する場所です。 のポインタをインクリメントすると 異なるタイプのポインタは、異なる数だけ移動する可能性があります。 C言語では、値によってバイトサイズが異なるため、バイト単位で表示します。

ここでは、1つのポインタを使用して、最初の未処理の char の文字列 ( str )、そしてもうひとつは最後を参照するためのものです ( end ). それらの値を入れ替えて ( *str*end を実行し、ポインタを移動させます。 を文字列の途中まで内側に入れる。 一度 str >= end のどちらか、または を指し、どちらも同じ char ということは、元の文字列が 奇数の長さ(そして、真ん中の char は反転させる必要はない)、あるいは すべて処理しました。

入れ替えを行うために、私は マクロ . マクロはテキスト置換 Cプリプロセッサによって行われます。 これらは関数とは全く異なるものです。 その違いを知っておくことは重要です。 関数を呼び出すとき 関数は、あなたが与えた値のコピーに対して操作します。 呼び出すと マクロは単にテキストを置き換えるだけなので、与えた引数は が直接使用されます。

を使っただけなので XOR_SWAP マクロは一度だけなので、定義するのはやりすぎだったかもしれません。 しかし、その方が何をやっているのかがより明確になります。 Cプリプロセッサがマクロを展開した後。 whileループはこのようになります。

    while (str < end)
    {
      do { *str ^= *end; *end ^= *str; *str ^= *end; } while (0);
      str++;
      end--;
    }

マクロ引数は、それらが マクロの定義です。 これは非常に便利な機能ですが、コードが壊れてしまう可能性もあります。 というのは、間違った使い方をした場合です。 例えば、インクリメント/デクリメントを圧縮していたら のように、命令とマクロの呼び出しを1行にまとめています。

      XOR_SWAP(*str++, *end--);

そして、これは次のように展開されます。

      do { *str++ ^= *end--; *end-- ^= *str++; *str++ ^= *end--; } while (0);

どれが トリプル というのは、インクリメント/デクリメント操作のことであり、実際には は、本来行うべきスワップを行う。

この話題に触れたついでに xor ( ^ )を意味します。 それは、基本的な 足し算、引き算、掛け算、割り算のような算数の演算です。 小学校ではあまり習わない。 これは2つの整数をビットごとに結合する - 足し算のようなものですが、キャリーオーバーを気にすることはありません。 1^1 = 0 , 1^0 = 1 , 0^1 = 1 , 0^0 = 0 .

よく知られているのは、xorを使って2つの値を入れ替えるという方法です。 これは、次の3つの基本的な理由によるものです。 xorの特性です。 x ^ 0 = x , x ^ x = 0x ^ y = y ^ x すべての値に対して xy . ということで、2つの 変数 ab という2つの値が初期状態で格納されている va そして vb .

  // 最初は
  // a == v
<サブ

a


  // b == v
<サブ

b


  a ^= b;
  // 現在: a == v
<サブ

a

 ^ v
<サブ

b


  b ^= a;
  // 今: b == v
<サブ

b

 ^ (v
<サブ

a

 ^ v
<サブ

b

)
  // == v
<サブ

a

 ^ (v
<サブ

b

 ^ v
<サブ

b

)
  // == v
<サブ

a

 ^ 0
  // == v

<サブ

a


  a ^= b;
  // 今: a == (v
<サブ

a

 ^ v
<サブ

b

) ^ v
<サブ

a


  // == (v
<サブ

a

 ^ v
<サブ

a

) ^ v
<サブ

b


  // == 0 ^ v
<サブ

b


  // == v
<サブ

b

つまり、値が入れ替わっているのです。 これには一つバグがあります。 ab は同じ変数です。

  // 最初は
  // a == v
<サブ

a


  a ^= a;
  // 現在: a == v
<サブ

a

 ^ v
<サブ

a


  // == 0
  a ^= a;
  // 現在: a == 0 ^ 0
  // == 0
  a ^= a;
  // 現在: a == 0 ^ 0
  // == 0

私たちは str < end 上記のコードでは、このようなことは起こりませんので、大丈夫です。

正しさにこだわる一方で、エッジケースをチェックする必要があります。 それは if (str) という行が与えられていないことを確認する必要があります。 NULL のポインタを使用します。 空の文字列はどうでしょう "" ? さて strlen("") == 0 を初期化します。 end として str - 1 ということになります。 while (str < end) の条件は決して真にならないので、何もしない。 どちらが正しいのでしょう。

C言語には、探求すべきことがたくさんあります。 楽しんでください。

更新しました。 mmw は、インプレースで動作するため、呼び出し方に少し注意する必要があるということです。

 char stack_string[] = "This string is copied onto the stack.";
 inplace_reverse(stack_string);

これは問題なく動作します。 stack_string は配列であり、その内容は与えられた文字列定数で初期化されます。 しかし

 char * string_literal = "This string is part of the executable.";
 inplace_reverse(string_literal);

実行時にコードが炎上して死んでしまいます。 それは string_literal は単に実行ファイルの一部として保存されている文字列を指しているだけで、通常はOSによって編集が許可されていないメモリです。 幸せな世界では、コンパイラはこのことを知っていて、コンパイルしようとするとエラーを出し、こう告げるでしょう。 string_literal の型である必要があります。 char const * 中身を変更することができないからです。 しかし、これは私のコンパイラが生きている世界ではありません。

あるメモリがスタックやヒープにある(つまり編集可能である)ことを確認するために試せるハックはいくつかありますが、必ずしもポータブルではなく、かなり醜いことになるかもしれません。 しかし、私はこの責任を関数の呼び出し元に投げつけても構わないと考えています。 私はこの関数がインプレースメモリ操作を行うことを彼らに伝えました、それを可能にする引数を私に与えることは彼らの責任です。