標準の方法で先頭/末尾の空白を削除するにはどうすればよいですか?


177

Cの文字列から先頭と末尾の空白を削除する、きれいで、できれば標準的な方法はありますか?私は自分でロールバックしますが、これは同様に一般的な解決策での一般的な問題だと思います。

回答:


164

文字列を変更できる場合:

// Note: This function returns a pointer to a substring of the original string.
// If the given string was allocated dynamically, the caller must not overwrite
// that pointer with the returned value, since the original pointer must be
// deallocated using the same allocator with which it was allocated.  The return
// value must NOT be deallocated using free() etc.
char *trimwhitespace(char *str)
{
  char *end;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
    return str;

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;

  // Write new null terminator character
  end[1] = '\0';

  return str;
}

文字列を変更できない場合は、基本的に同じ方法を使用できます。

// Stores the trimmed input string into the given output buffer, which must be
// large enough to store the result.  If it is too small, the output is
// truncated.
size_t trimwhitespace(char *out, size_t len, const char *str)
{
  if(len == 0)
    return 0;

  const char *end;
  size_t out_size;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
  {
    *out = 0;
    return 1;
  }

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;
  end++;

  // Set output size to minimum of trimmed string length and buffer size minus 1
  out_size = (end - str) < len-1 ? (end - str) : len-1;

  // Copy trimmed string and add null terminator
  memcpy(out, str, out_size);
  out[out_size] = 0;

  return out_size;
}

6
メモリリークを気にしない限り、最初の答えはまったく良くありません。これで、2つの重複する文字列があります(元の文字列の末尾のスペースが削除された元の文字列と新しい文字列)。解放できるのは元の文字列だけですが、解放した場合、2番目の文字列は解放されたメモリを指します。
David Nehme 2008年

7
@nvl:割り当てられているメモリがないため、解放するメモリがありません。
Adam Rosenfield、2010年

15
@nvl:いいえ。 strローカル変数であり、変更しても渡される元のポインターは変更されません。Cの関数呼び出しは常に値渡しであり、参照渡しではありません。
Adam Rosenfield、2010年

11
@Raj:渡されたアドレスとは異なるアドレスを返すことには本質的に問題はありません。ここで、戻り値がfree()関数の有効な引数である必要はありません。まったく逆です-効率を上げるためにメモリを割り当てる必要がないように設計しました。渡されたアドレスが動的に割り当てられた場合でも、呼び出し元はそのメモリを解放する責任があり、呼び出し元はここで返された値でその値を上書きしないようにする必要があります。
アダムローゼンフィールド2013年

3
引数をisspaceto にキャストする必要があります。キャストしunsigned charないと、未定義の動作が発生します。
Roland Illig 2016

37

これは、文字列をバッファの最初の位置にシフトするものです。文字列を動的に割り当てた場合でも、trim()が返すのと同じポインタでそれを解放できるように、この動作が必要になる場合があります。

char *trim(char *str)
{
    size_t len = 0;
    char *frontp = str;
    char *endp = NULL;

    if( str == NULL ) { return NULL; }
    if( str[0] == '\0' ) { return str; }

    len = strlen(str);
    endp = str + len;

    /* Move the front and back pointers to address the first non-whitespace
     * characters from each end.
     */
    while( isspace((unsigned char) *frontp) ) { ++frontp; }
    if( endp != frontp )
    {
        while( isspace((unsigned char) *(--endp)) && endp != frontp ) {}
    }

    if( frontp != str && endp == frontp )
            *str = '\0';
    else if( str + len - 1 != endp )
            *(endp + 1) = '\0';

    /* Shift the string so that it starts at str so that if it's dynamically
     * allocated, we can still free it on the returned pointer.  Note the reuse
     * of endp to mean the front of the string buffer now.
     */
    endp = str;
    if( frontp != str )
    {
            while( *frontp ) { *endp++ = *frontp++; }
            *endp = '\0';
    }

    return str;
}

正当性をテストする:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* Paste function from above here. */

int main()
{
    /* The test prints the following:
    [nothing to trim] -> [nothing to trim]
    [    trim the front] -> [trim the front]
    [trim the back     ] -> [trim the back]
    [    trim front and back     ] -> [trim front and back]
    [ trim one char front and back ] -> [trim one char front and back]
    [ trim one char front] -> [trim one char front]
    [trim one char back ] -> [trim one char back]
    [                   ] -> []
    [ ] -> []
    [a] -> [a]
    [] -> []
    */

    char *sample_strings[] =
    {
            "nothing to trim",
            "    trim the front",
            "trim the back     ",
            "    trim front and back     ",
            " trim one char front and back ",
            " trim one char front",
            "trim one char back ",
            "                   ",
            " ",
            "a",
            "",
            NULL
    };
    char test_buffer[64];
    char comparison_buffer[64];
    size_t index, compare_pos;

    for( index = 0; sample_strings[index] != NULL; ++index )
    {
        // Fill buffer with known value to verify we do not write past the end of the string.
        memset( test_buffer, 0xCC, sizeof(test_buffer) );
        strcpy( test_buffer, sample_strings[index] );
        memcpy( comparison_buffer, test_buffer, sizeof(comparison_buffer));

        printf("[%s] -> [%s]\n", sample_strings[index],
                                 trim(test_buffer));

        for( compare_pos = strlen(comparison_buffer);
             compare_pos < sizeof(comparison_buffer);
             ++compare_pos )
        {
            if( test_buffer[compare_pos] != comparison_buffer[compare_pos] )
            {
                printf("Unexpected change to buffer @ index %u: %02x (expected %02x)\n",
                    compare_pos, (unsigned char) test_buffer[compare_pos], (unsigned char) comparison_buffer[compare_pos]);
            }
        }
    }

    return 0;
}

ソースファイルはtrim.cでした。'cc -Wall tr​​im.c -o trim'でコンパイルされます。


2
引数をisspaceto にキャストする必要があります。キャストしunsigned charないと、未定義の動作が発生します。
Roland Illig 2016

@RolandIllig:ありがとう、それが必要だとは思いもしませんでした。修正しました。
2016

@Simas:どうしてそんなこと言うの?関数呼び出しはisspace()、なぜ違いがあるでしょう" "との"\n"?改行用の単体テストを追加しましたが、問題ありません... ideone.com/bbVmqo
indiv

1
@indiv手動で割り当てた場合、無効なメモリブロックにアクセスします。つまり、この行:*(endp + 1) = '\0';。回答のテスト例では、この問題を回避する64のバッファを使用しています。
Simas

1
@nolandda:詳細をありがとう。現時点ではvalgrindにアクセスできないため、修正してバッファオーバーランを検出するようにテストを更新しました。
個人

23

私の解決策。文字列は変更可能でなければなりません。後でfree()する必要がある場合に、古いポインターを使い続けることができるように、スペース以外の部分を先頭に移動する他のいくつかのソリューションよりも優れています。

void trim(char * s) {
    char * p = s;
    int l = strlen(p);

    while(isspace(p[l - 1])) p[--l] = 0;
    while(* p && isspace(* p)) ++p, --l;

    memmove(s, p, l + 1);
}   

このバージョンでは、文字列を編集する代わりにstrndup()で文字列のコピーを作成します。strndup()には_GNU_SOURCEが必要であるため、malloc()とstrncpy()を使用して独自のstrndup()を作成する必要がある場合があります。

char * trim(char * s) {
    int l = strlen(s);

    while(isspace(s[l - 1])) --l;
    while(* s && isspace(* s)) ++s, --l;

    return strndup(s, l);
}

4
trim()UBは場合呼び出しs""最初のようにisspace()呼び出しがあろうisspace(p[-1])p[-1]必ずしも法的位置を参照しません。
chux-モニカを2013年

1
引数をisspaceto にキャストする必要があります。キャストしunsigned charないと、未定義の動作が発生します。
Roland Illig 2016

1
if(l==0)return;長さゼロのstrを回避するために追加する必要があります
ch271828n

11

左、右、両方、すべて、所定の位置で分離してトリミングし、指定した文字のセット(またはデフォルトでは空白)をトリミングするための私のCミニライブラリを次に示します。

strlib.hの内容:

#ifndef STRLIB_H_
#define STRLIB_H_ 1
enum strtrim_mode_t {
    STRLIB_MODE_ALL       = 0, 
    STRLIB_MODE_RIGHT     = 0x01, 
    STRLIB_MODE_LEFT      = 0x02, 
    STRLIB_MODE_BOTH      = 0x03
};

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 );

char *strtriml(char *d, char *s);
char *strtrimr(char *d, char *s);
char *strtrim(char *d, char *s); 
char *strkill(char *d, char *s);

char *triml(char *s);
char *trimr(char *s);
char *trim(char *s);
char *kill(char *s);
#endif

strlib.cの内容:

#include <strlib.h>

char *strcpytrim(char *d, // destination
                 char *s, // source
                 int mode,
                 char *delim
                 ) {
    char *o = d; // save orig
    char *e = 0; // end space ptr.
    char dtab[256] = {0};
    if (!s || !d) return 0;

    if (!delim) delim = " \t\n\f";
    while (*delim) 
        dtab[*delim++] = 1;

    while ( (*d = *s++) != 0 ) { 
        if (!dtab[0xFF & (unsigned int)*d]) { // Not a match char
            e = 0;       // Reset end pointer
        } else {
            if (!e) e = d;  // Found first match.

            if ( mode == STRLIB_MODE_ALL || ((mode != STRLIB_MODE_RIGHT) && (d == o)) ) 
                continue;
        }
        d++;
    }
    if (mode != STRLIB_MODE_LEFT && e) { // for everything but trim_left, delete trailing matches.
        *e = 0;
    }
    return o;
}

// perhaps these could be inlined in strlib.h
char *strtriml(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_LEFT, 0); }
char *strtrimr(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_RIGHT, 0); }
char *strtrim(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_BOTH, 0); }
char *strkill(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_ALL, 0); }

char *triml(char *s) { return strcpytrim(s, s, STRLIB_MODE_LEFT, 0); }
char *trimr(char *s) { return strcpytrim(s, s, STRLIB_MODE_RIGHT, 0); }
char *trim(char *s) { return strcpytrim(s, s, STRLIB_MODE_BOTH, 0); }
char *kill(char *s) { return strcpytrim(s, s, STRLIB_MODE_ALL, 0); }

1つのメインルーチンがすべてを行います。src == dstの場合は適切にトリミングされ、それ以外の場合はstrcpyルーチンのように機能します。文字列delimで指定された文字のセットをトリミングしますます。nullの場合は空白文字です。左、右、両方、およびすべて(trなど)をトリミングします。それほど多くはなく、文字列を1回だけ反復します。一部の人々は右トリムが左から始まると文句を言うかもしれませんが、とにかく左から始まるstrlenは必要ありません。(何らかの方法で、右トリムの文字列の最後に到達する必要があるため、作業を進めながら行うこともできます。)パイプライン処理やキャッシュサイズなどについては、議論の余地があるかもしれません。 。ソリューションは左から右に機能し、1回だけ繰り返されるため、ストリームで機能するように拡張できます。制限事項:それはないないで作業ユニコード 文字列。


2
私はこれに賛成し、古いことは知っていますが、バグがあると思います。配列インデックスとして使用する前にdtab[*d]キャスト*dしませんunsigned int。署名されたcharを持つシステムでは、dtab[-127]これはバグを引き起こし、場合によってはクラッシュする可能性があります。
Zan Lynx

2
インデックス値をにキャストする必要があるdtab[*delim++]ため、未定義の動作が発生する可能性がcharありますunsigned char。コードは8ビットを想定していますchardelimとして宣言する必要がありますconst char *dtab[0xFF & (unsigned int)*d]としてより明確になりdtab[(unsigned char)*d]ます。コードはUTF-8でエンコードされた文字列で機能しますが、非ASCIIのスペースシーケンスを削除しません。
chqrlie

@ michael-plainer、これは面白そうだ。テストしてGitHubに配置してみませんか?
荒巻大輔

9

これは、シンプルでありながら正しいインプレーストリム機能での私の試みです。

void trim(char *str)
{
    int i;
    int begin = 0;
    int end = strlen(str) - 1;

    while (isspace((unsigned char) str[begin]))
        begin++;

    while ((end >= begin) && isspace((unsigned char) str[end]))
        end--;

    // Shift all characters back to the start of the string array.
    for (i = begin; i <= end; i++)
        str[i - begin] = str[i];

    str[i - begin] = '\0'; // Null terminate string.
}

2
"" str [-1] `のwhile ((end >= begin) && isspace(str[end]))ときにUBを回避するように変更することを提案します。str is . Prevents
chux-モニカを2013年

ところで、機能させるには、これをstr [i-begin + 1]に変更する必要があります
truongnm

1
引数をisspaceto にキャストする必要があります。キャストしunsigned charないと、未定義の動作が発生します。
Roland Illig 2016

@RolandIllig、なぜ未定義の動作になるのでしょうか?この関数は文字を処理することを目的としています。
wovano

@wovanoいいえ、そうではありません。からの関数<ctype.h>は、unsigned charまたは特別な値のいずれかを表すintで機能することを目的としていますEOFstackoverflow.com/q/7131026/225757を参照してください。
Roland Illig

8

トリムパーティーの後半

機能:
1.他の多くの回答と同様に、最初をすばやくトリミングします。
2.最後まで行った後、ループごとに1つのテストのみで右をトリミングします。@ jfm3と同様ですが、すべて空白文字列に対して機能します)
3. charがに署名char*sれてunsigned charいる場合に未定義の動作を回避するには、にキャストします。

文字処理 「すべての場合において、引数はでありint、その値はとして表現可能であるunsigned charか、マクロの値と等しいものでなければなりませんEOF。引数に他の値がある場合、動作は未定義です。」C11§7.41

#include <ctype.h>

// Return a pointer to the trimmed string
char *string_trim_inplace(char *s) {
  while (isspace((unsigned char) *s)) s++;
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
  }

  // If desired, shift the trimmed string

  return s;
}

@chqrlieは、上記はトリムされた文字列をシフトしないとコメントしました。そうするために....

// Return a pointer to the (shifted) trimmed string
char *string_trim_inplace(char *s) {
  char *original = s;
  size_t len = 0;

  while (isspace((unsigned char) *s)) {
    s++;
  } 
  if (*s) {
    char *p = s;
    while (*p) p++;
    while (isspace((unsigned char) *(--p)));
    p[1] = '\0';
    // len = (size_t) (p - s);   // older errant code
    len = (size_t) (p - s + 1);  // Thanks to @theriver
  }

  return (s == original) ? s : memmove(original, s, len + 1);
}

3
いや、やっとctypeの未定義の振る舞いを知っている人。
Roland Illig 2016

2
@chux len =(size_t)(ps)+1; そうでない場合、最後の文字が重複します。
河川

4

これは、@ adam-rosenfieldsのインプレース変更ルーチンに似たソリューションですが、不必要にstrlen()に頼ることはありません。@jkramerと同様に、文字列はバッファ内で左寄せされるため、同じポインタを解放できます。memmoveを使用しないため、大きな文字列には最適ではありません。@ jfm3が言及する++ /-演算子を含みます。 FCTXベースの単体テストが含まれています。

#include <ctype.h>

void trim(char * const a)
{
    char *p = a, *q = a;
    while (isspace(*q))            ++q;
    while (*q)                     *p++ = *q++;
    *p = '\0';
    while (p > a && isspace(*--p)) *p = '\0';
}

/* See http://fctx.wildbearsoftware.com/ */
#include "fct.h"

FCT_BGN()
{
    FCT_QTEST_BGN(trim)
    {
        { char s[] = "";      trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "   ";   trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "\t";    trim(s); fct_chk_eq_str("",    s); } // Trivial
        { char s[] = "a";     trim(s); fct_chk_eq_str("a",   s); } // NOP
        { char s[] = "abc";   trim(s); fct_chk_eq_str("abc", s); } // NOP
        { char s[] = "  a";   trim(s); fct_chk_eq_str("a",   s); } // Leading
        { char s[] = "  a c"; trim(s); fct_chk_eq_str("a c", s); } // Leading
        { char s[] = "a  ";   trim(s); fct_chk_eq_str("a",   s); } // Trailing
        { char s[] = "a c  "; trim(s); fct_chk_eq_str("a c", s); } // Trailing
        { char s[] = " a ";   trim(s); fct_chk_eq_str("a",   s); } // Both
        { char s[] = " a c "; trim(s); fct_chk_eq_str("a c", s); } // Both

        // Villemoes pointed out an edge case that corrupted memory.  Thank you.
        // http://stackoverflow.com/questions/122616/#comment23332594_4505533
        {
          char s[] = "a     ";       // Buffer with whitespace before s + 2
          trim(s + 2);               // Trim "    " containing only whitespace
          fct_chk_eq_str("", s + 2); // Ensure correct result from the trim
          fct_chk_eq_str("a ", s);   // Ensure preceding buffer not mutated
        }

        // doukremt suggested I investigate this test case but
        // did not indicate the specific behavior that was objectionable.
        // http://stackoverflow.com/posts/comments/33571430
        {
          char s[] = "         foobar";  // Shifted across whitespace
          trim(s);                       // Trim
          fct_chk_eq_str("foobar", s);   // Leading string is correct

          // Here is what the algorithm produces:
          char r[16] = { 'f', 'o', 'o', 'b', 'a', 'r', '\0', ' ',                     
                         ' ', 'f', 'o', 'o', 'b', 'a', 'r', '\0'};
          fct_chk_eq_int(0, memcmp(s, r, sizeof(s)));
        }
    }
    FCT_QTEST_END();
}
FCT_END();

この解決策は実に危険です!元の文字列に空白以外の文字が含まれていない場合、それらのバイトに「空白」バイトが含まれていると、トリムの最後の行でaより前の文字が上書きされます。これを最適化せずにコンパイルし、yに何が起こるかを確認します。符号なしx = 0x20202020; char s [4] = ""; 符号なしy = 0x20202020; printf( "&x、&s、&y =%p、%p、%p \ n"、&x、&s、&y); printf( "x、[s]、y =%08x、[%s]、%08x \ n"、x、s、y); trim_whitespace(s); printf( "x、[s]、y =%08x、[%s]、%08x \ n"、x、s、y);
Villemoes 2013

@Villemoes、バグレポートをありがとう。文字列に空白のみが含まれている場合にバッファの左側から外れないようにロジックを更新しました。この新しいバージョンはあなたの懸念に対処しますか?
Rhys Ulerich 2013

言語弁護士はおそらく、 'a'が指す1つに先行するcharへのポインターを作成することについて推測するだけの考えであなたに向かって叫ぶでしょう(これは '--p'が行うことです)。現実の世界では、おそらく大丈夫です。ただし、 '> ='を '>'に変更して、pのデクリメントを 'isspace(*-p)'に移動することもできます。
Villemoes 2013年

弁護士はそれを触れずに住所を比較するだけなので大丈夫だと思いますが、私は減少についてのあなたの提案も好きです。それに応じて更新しました。ありがとう。
Rhys Ulerich 2013年

1
doukremt、foobarの後のバッファ全体がゼロで満たされていないという懸念はありますか?もしそうなら、漠然とした岩を投げるのではなく、はっきりと言った方がはるかに役立つでしょう。
Rhys Ulerich 2014年

3

もう1つは、1つの行が実際の作業を行うものです。

#include <stdio.h>

int main()
{
   const char *target = "   haha   ";
   char buf[256];
   sscanf(target, "%s", buf); // Trimming on both sides occurs here
   printf("<%s>\n", buf);
}

1
scanfを使用することをお勧めします。しかし、彼は、OPが望んだものとは異なる可能性がある単一の単語でのみ機能します(つまり、「abc」をトリミングすると「ab c」になるはずですが、単一のscanfは「a」になります)。したがって、ループと、%n変換指定子を使用したスキップされた文字のカウンターが必要です。結局のところ、手動​​で実行する方が簡単です。
ピーター-モニカを2016年

最初のスペースを無視して文字列の最初の単語が必要な場合に非常に役立ちます。
J ... S

3

次の1つ以上を行ったため、これらの回答のほとんどが気に入らなかった...

  1. 元のポインターの文字列内に別のポインターを返しました(同じものへの2つの異なるポインターを混同するのはちょっと面倒)。
  2. 文字列全体を事前反復するstrlen()のようなものを不必要に使用しました。
  3. 移植性のないOS固有のlib関数を使用。
  4. バックスキャン。
  5. isspace()代わりに''との比較を使用して、TAB / CR / LFが保持されるようにしました。
  6. 大きな静的バッファでメモリを浪費する。
  7. sscanf / sprintfのような高コスト関数による無駄なサイクル。

これが私のバージョンです:

void fnStrTrimInPlace(char *szWrite) {

    const char *szWriteOrig = szWrite;
    char       *szLastSpace = szWrite, *szRead = szWrite;
    int        bNotSpace;

    // SHIFT STRING, STARTING AT FIRST NON-SPACE CHAR, LEFTMOST
    while( *szRead != '\0' ) {

        bNotSpace = !isspace((unsigned char)(*szRead));

        if( (szWrite != szWriteOrig) || bNotSpace ) {

            *szWrite = *szRead;
            szWrite++;

            // TRACK POINTER TO LAST NON-SPACE
            if( bNotSpace )
                szLastSpace = szWrite;
        }

        szRead++;
    }

    // TERMINATE AFTER LAST NON-SPACE (OR BEGINNING IF THERE WAS NO NON-SPACE)
    *szLastSpace = '\0';
}

2
引数をisspaceto にキャストする必要があります。キャストしunsigned charないと、未定義の動作が発生します。
Roland Illig 2016

この回答は「浪費されたサイクル」に関係しているため、スペースがない場合、コードはスティング全体を不必要にコピーすることに注意してください。リーディングwhile (isspace((unsigned char) *szWrite)) szWrite++;はそれを防ぎます。コードは、末尾の空白もすべてコピーします。
chux-モニカを

@chuxこの実装は、別の場所に新しいポインターを返すのとは対照的に、別の読み取りおよび書き込みポインターでインプレースで変化します。そのため、1行目の最初の非スペースにszWriteをジャンプする提案は、先頭のスペースをそのままにします。元の文字列。
Jason Stewart

@chux、あなたはそれが末尾の空白をコピーする(最後の非空白文字の後にnullを追加する前に)ことは正しいですが、それは文字列の事前スキャンを避けるために支払うことを選んだ価格です。末尾のWSの量が少ない場合は、文字列全体をスキャンして最後の非WS文字を探すよりも、バイトをコピーする方が安価です。後続のWSが大量にある場合、事前スキャンはおそらく書き込みを減らす価値があります。
Jason Stewart

@chux、「スペースがない場合のコピー」の状況では*szWrite = *szRead、ポインターが等しくない場合にのみ実行すると、その場合は書き込みがスキップされますが、別の比較/ブランチが追加されました。最近のCPU / MMU / BPでは、そのチェックが損失か利益かはわかりません。単純なプロセッサとメモリアーキテクチャでは、コピーを実行して比較をスキップする方が安価です。
Jason Stewart

2

パーティーに非常に遅れています...

バックトラックのないシングルパス順方向スキャンソリューション。ソース文字列のすべての文字が正確 2回1回テストされます。(したがって、特にソース文字列に末尾のスペースがたくさんある場合は、他のほとんどのソリューションよりも高速になるはずです。)

これには2つのソリューションが含まれます。1つはソース文字列を別の宛先文字列にコピーしてトリミングするもので、もう1つはソース文字列を所定の位置でトリミングするものです。どちらの関数も同じコードを使用します。

(変更可能な)文字列はインプレースで移動されるため、その文字列への元のポインターは変更されません。

#include <stddef.h>
#include <ctype.h>

char * trim2(char *d, const char *s)
{
    // Sanity checks
    if (s == NULL  ||  d == NULL)
        return NULL;

    // Skip leading spaces        
    const unsigned char * p = (const unsigned char *)s;
    while (isspace(*p))
        p++;

    // Copy the string
    unsigned char * dst = (unsigned char *)d;   // d and s can be the same
    unsigned char * end = dst;
    while (*p != '\0')
    {
        if (!isspace(*dst++ = *p++))
            end = dst;
    }

    // Truncate trailing spaces
    *end = '\0';
    return d;
}

char * trim(char *s)
{
    return trim2(s, s);
}

1
ソース文字列のすべての文字が正確に1回テストされます。実際には、ソース文字列のほとんどの文字が2回テストされます:と比較され'\0'てからテストされisspace()ます。すべてのキャラクターをでテストするのは無駄isspace()です。文字列の終わりからバックトラックすることは、非病理学的なケースではより効率的です。
chqrlie

@chqrlie-はい、各キャラクターは2回テストされます。ここで他のアルゴリズムと比較して、このコードが実際にテストされ、特に末尾のスペースが多い文字列が与えられた場合に確認してください。
デビッドRトリブル

trim()OK。コーナーケース:とtrim2(char *d, const char *s)d,s重なると問題が発生しs < dます。
chux-モニカを

@chux-そのコーナーケースでは、どのtrim()ように振る舞うべきですか?文字列自体を占有しているメモリに文字列をトリミングしてコピーするように求めています。とは異なりmemmove()、これはトリム自体を行う前にソース文字列の長さを決定する必要があり、さらに文字列全体をスキャンする必要があります。rtrim2()ソースを宛先に逆方向にコピーすることを認識しており、おそらく追加のソース文字列長の引数をとる別の関数を書く方が良いでしょう。
David R Tribble

1

「無痛」とはどういう意味かわかりません。

C文字列はかなり痛いです。最初の空白以外の文字の位置を簡単に見つけることができます。

while(isspace(* p))p ++;

最後の空白以外の文字の位置は、2つの同様の自明な動きで見つけることができます。

while(* q)q ++;
する{q--; } while(isspace(* q));

*++演算子を同時に使用する手間を省きました。)

質問はこれで何をするのですか?手元にあるデータ型は、実際Stringに考えるのが容易で強力なアブストラクトではなく、実際にはストレージバイトの配列以上のものではありません。堅牢なデータ型がないため、PHperytonbyのchomp関数と同じ機能を実行する関数を作成することは不可能です。Cのそのような関数は何を返しますか?


文字列がすべての空白で構成されていない限り、これはうまく機能します。do { q--; } ...知る前に一度のチェックが必要*q != 0です。
chux-モニカを2014年

1

たとえば、文字列ライブラリを使用します。

Ustr *s1 = USTR1(\7, " 12345 ");

ustr_sc_trim_cstr(&s1, " ");
assert(ustr_cmp_cstr_eq(s1, "12345"));

...これは「一般的な」問題であると言っているので、はい、#includeなどを含める必要があります。これはlibcには含まれていませんが、ランダムポインターを格納する独自のハックジョブを考案してはいけません。バッファオーバーフロー。



1

この成長を維持するために、変更可能な文字列を使用したもう1つのオプション:

void trimString(char *string)
{
    size_t i = 0, j = strlen(string);
    while (j > 0 && isspace((unsigned char)string[j - 1])) string[--j] = '\0';
    while (isspace((unsigned char)string[i])) i++;
    if (i > 0) memmove(string, string + i, j - i + 1);
}

1
strlen()size_tの範囲を超えることができるを返しますint。空白はスペース文字に限定されません。最後に、しかし最も重要:strcpy(string, string + i * sizeof(char));ソースと宛先の配列がオーバーラップするため、未定義の動作。のmemmove()代わりに使用しますstrcpy()
chqrlie 2016年

@chqrlieあなたは正しい、ちょうどあなたの提案が含まれています。コピー元とコピー先がオーバーラップする場合のコピーは未定義の動作を引き起こす可能性があることを理解していますが、この特定のケースでは、メモリの新しい位置から最初に常にコピーするため、問題が発生しないことを指摘したいだけです。フィードバックをお寄せいただきありがとうございます。
wallek876

1
ソース配列とデスティネーション配列がどのようにオーバーラップするかは関係ありません。これは未定義の動作です。コピーはアドレスの増加に沿って一度に1バイトずつ行われるという仮定に依存しないでください。またwhile (isspace((int)string[i])) string[i--] = '\0';、文字列の先頭を超えてループする可能性があることも忘れていました。このループをwhile (i > 0 && isspace((unsigned char)string[--i])) { string[i] = '\0'; } size_t end = i;
前後の

@chqrlie良い点は、すべての空白を含む文字列は、最初からループしてしまうだろうと考えていました。
wallek876

実際、end末尾のnullバイトを指さなかったため、私の提案は正しくありませんでしたend = ++i;が、すべての空白文字を含む文字列に関して問題がまだありました。コードを修正しました。
chqrlie 2016年

1

私はたくさんの答えがあることを知っていますが、私の答えがここに投稿されて私の解決策が十分であるかどうかを確認します。

// Trims leading whitespace chars in left `str`, then copy at almost `n - 1` chars
// into the `out` buffer in which copying might stop when the first '\0' occurs, 
// and finally append '\0' to the position of the last non-trailing whitespace char.
// Reture the length the trimed string which '\0' is not count in like strlen().
size_t trim(char *out, size_t n, const char *str)
{
    // do nothing
    if(n == 0) return 0;    

    // ptr stop at the first non-leading space char
    while(isspace(*str)) str++;    

    if(*str == '\0') {
        out[0] = '\0';
        return 0;
    }    

    size_t i = 0;    

    // copy char to out until '\0' or i == n - 1
    for(i = 0; i < n - 1 && *str != '\0'; i++){
        out[i] = *str++;
    }    

    // deal with the trailing space
    while(isspace(out[--i]));    

    out[++i] = '\0';
    return i;
}

2
注:isspace(*str)UBの場合*str < 0
chux-モニカを

1
の使用size_t nは適切ですが、nトリムされた文字列に対して小さすぎる場合、インターフェイスは呼び出し元に通知しません。検討してくださいtrim(out, 12, "delete data not")
chux-モニカを復活させる

1

文字列の先頭のスペースをスキップする最も簡単な方法は、imho、

#include <stdio.h>

int main()
{
char *foo="     teststring      ";
char *bar;
sscanf(foo,"%s",bar);
printf("String is >%s<\n",bar);
    return 0;
}

1
これは、のように中央にスペースがある文字列では機能しません" foo bar "
デビッドRトリブル2018

1

さて、これは私の質問に対する私の見解です。文字列freeを適切に変更し(機能する)、UBを回避する最も簡潔なソリューションだと思います。小さな文字列の場合は、memmoveを含むソリューションよりもおそらく高速です。

void stripWS_LT(char *str)
{
    char *a = str, *b = str;
    while (isspace((unsigned char)*a)) a++;
    while (*b = *a++)  b++;
    while (b > str && isspace((unsigned char)*--b)) *b = 0;
}

b > strテストは一度だけ必要とされています。 *b = 0;一度だけ必要です。
chux-モニカを

1
#include <ctype.h>
#include <string.h>

char *trim_space(char *in)
{
    char *out = NULL;
    int len;
    if (in) {
        len = strlen(in);
        while(len && isspace(in[len - 1])) --len;
        while(len && *in && isspace(*in)) ++in, --len;
        if (len) {
            out = strndup(in, len);
        }
    }
    return out;
}

isspace すべての空白を削除するのに役立ちます。

  • 最初のループを実行して、最後のバイトからスペース文字をチェックし、変数の長さを減らします
  • 2番目のループを実行して、最初のバイトからスペース文字をチェックし、変数の長さを減らし、charポインターをインクリメントします。
  • 最後に、長さ変数が0より大きい場合は、を使用strndupして、スペースを除外して新しい文字列バッファーを作成します。

ちょっとしたちょっとしたひねりstrndup()は、C標準の一部ではなく、Posixだけです。しかし、実装は非常に簡単なので、大したことではありません。
PatrickSchlüter19年

trim_space("")を返しますNULL。へのポインタを期待します""int len;する必要がありますsize_t len;isspace(in[len - 1])UBのときin[len - 1] < 0
chux-モニカを

イニシャルのwhile (isspace((unsigned char) *in) in++;前のlen = strlen(in);方が後の方より効率的ですwhile(len && *in && isspace(*in)) ++in, --len;
chux-モニカの復活

0

個人的には自分で転がします。strtokを使用することはできますが、メモリが何であるかがわかるように(特に先頭の文字を削除する場合は)注意する必要があります。

末尾のスペースを削除するのは簡単で、かなり安全です。最後から数えて、最後のスペースの上部に0を置くだけです。先頭のスペースを取り除くことは、物事を移動することを意味します。それをその場で(おそらく賢明な方法で)実行したい場合は、先頭のスペースがなくなるまですべてを1文字ずつシフトし続けることができます。または、より効率的にするために、最初の非スペース文字のインデックスを見つけて、その数だけすべてをシフトバックすることができます。または、最初のスペース以外の文字へのポインタを使用することもできます(ただし、strtokの場合と同じように注意する必要があります)。


4
strtokは一般に、使用するのにあまり適したツールではありません。特に、再入可能ではないためです。単一の関数内にとどまれば安全に使用できますが、それ自体がstrtokを使用する可能性のあるスレッドや他の関数を呼び出す可能性がある場合、問題が発生します。
ジョナサンレフラー

0
#include "stdafx.h"
#include "malloc.h"
#include "string.h"

int main(int argc, char* argv[])
{

  char *ptr = (char*)malloc(sizeof(char)*30);
  strcpy(ptr,"            Hel  lo    wo           rl   d G    eo rocks!!!    by shahil    sucks b i          g       tim           e");

  int i = 0, j = 0;

  while(ptr[j]!='\0')
  {

      if(ptr[j] == ' ' )
      {
          j++;
          ptr[i] = ptr[j];
      }
      else
      {
          i++;
          j++;
          ptr[i] = ptr[j];
      }
  }


  printf("\noutput-%s\n",ptr);
        return 0;
}

3
dreamlaxがテスト文字列を編集して「sucks big time」を含めるように考えていたので、これは私を笑わせました。いいえ。原作者は正直です。
James Morris

1
このコードは使用しないでください。バッファオーバーフローが発生します。
Roland Illig 2016

0

ゲームには少し遅れますが、私は自分のルーティンを争いに投げ込みます。それらはおそらく最も効率的なものではありませんが、私はそれらが正しいと信じており、それらは単純です(rtrim()複雑さの範囲を広げる)。

#include <ctype.h>
#include <string.h>

/*
    Public domain implementations of in-place string trim functions

    Michael Burr
    michael.burr@nth-element.com
    2010
*/

char* ltrim(char* s) 
{
    char* newstart = s;

    while (isspace( *newstart)) {
        ++newstart;
    }

    // newstart points to first non-whitespace char (which might be '\0')
    memmove( s, newstart, strlen( newstart) + 1); // don't forget to move the '\0' terminator

    return s;
}


char* rtrim( char* s)
{
    char* end = s + strlen( s);

    // find the last non-whitespace character
    while ((end != s) && isspace( *(end-1))) {
            --end;
    }

    // at this point either (end == s) and s is either empty or all whitespace
    //      so it needs to be made empty, or
    //      end points just past the last non-whitespace character (it might point
    //      at the '\0' terminator, in which case there's no problem writing
    //      another there).    
    *end = '\0';

    return s;
}

char*  trim( char* s)
{
    return rtrim( ltrim( s));
}

1
char引数をisspace()to (unsigned char)にキャストして、潜在的に負の値に対する未定義の動作を回避する必要があります。また、ltrim()必要がない場合は、ストリングを移動しないでください。
chqrlie 2016年

0

これまでの答えのほとんどは、次のいずれかを実行します。

  1. 文字列の終わりに戻る(つまり、文字列の終わりを見つけ、スペース以外の文字が見つかるまで逆方向にシークする)、または
  2. strlen()最初に呼び出し、文字列全体を2回通過します。

このバージョンは1つのパスのみを作成し、バックトラックしません。したがって、後続のスペースが数百もあるのが一般的である場合に限り、他のパフォーマンスよりもパフォーマンスが向上する可能性があります(SQLクエリの出力を処理する場合、これは珍しいことではありません)。

static char const WHITESPACE[] = " \t\n\r";

static void get_trim_bounds(char  const *s,
                            char const **firstWord,
                            char const **trailingSpace)
{
    char const *lastWord;
    *firstWord = lastWord = s + strspn(s, WHITESPACE);
    do
    {
        *trailingSpace = lastWord + strcspn(lastWord, WHITESPACE);
        lastWord = *trailingSpace + strspn(*trailingSpace, WHITESPACE);
    }
    while (*lastWord != '\0');
}

char *copy_trim(char const *s)
{
    char const *firstWord, *trailingSpace;
    char *result;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    result = malloc(newLength + 1);
    memcpy(result, firstWord, newLength);
    result[newLength] = '\0';
    return result;
}

void inplace_trim(char *s)
{
    char const *firstWord, *trailingSpace;
    size_t newLength;

    get_trim_bounds(s, &firstWord, &trailingSpace);
    newLength = trailingSpace - firstWord;

    memmove(s, firstWord, newLength);
    s[newLength] = '\0';
}

1
あなたはパフォーマンスに関係している場合は、使用していないstrspn()strcspn()タイトなループインチ これは非常に非効率的であり、オーバーヘッドによって、単一のフォワードパスの実証されていない利点が小さくなります。 strlen()通常は、非常に効率的なコードでインライン展開されますが、本当の問題ではありません。文字列の最初と最後のトリミングは、文字列のすべての文字の白色度をテストするよりも、非文字がほとんどないかまったくない文字列の特別な場合でも、はるかに速くなります。
chqrlie 2016年

0

これは私が考えることができる最も短い実装です:

static const char *WhiteSpace=" \n\r\t";
char* trim(char *t)
{
    char *e=t+(t!=NULL?strlen(t):0);               // *e initially points to end of string
    if (t==NULL) return;
    do --e; while (strchr(WhiteSpace, *e) && e>=t);  // Find last char that is not \r\n\t
    *(++e)=0;                                      // Null-terminate
    e=t+strspn (t,WhiteSpace);                           // Find first char that is not \t
    return e>t?memmove(t,e,strlen(e)+1):t;                  // memmove string contents and terminator
}

1
これはどうですか:char *trim(char *s) { char *p = s, *e = s + strlen(s); while (e > s && isspace((unsigned char)e[-1])) { *--e = '\0'; } while (isspace((unsigned char)*p)) { p++; } if (p > s) { memmove(s, p, e + 1 - p); } return s; }
chqrlie

0

これらの関数は元のバッファを変更するため、動的に割り当てられた場合、元のポインタを解放できます。

#include <string.h>

void rstrip(char *string)
{
  int l;
  if (!string)
    return;
  l = strlen(string) - 1;
  while (isspace(string[l]) && l >= 0)
    string[l--] = 0;
}

void lstrip(char *string)
{
  int i, l;
  if (!string)
    return;
  l = strlen(string);
  while (isspace(string[(i = 0)]))
    while(i++ < l)
      string[i-1] = string[i];
}

void strip(char *string)
{
  lstrip(string);
  rstrip(string);
}

rstrip()空の文字列に対して未定義の動作を呼び出します。lstrip()空白文字の最初の部分が長い文字列では、が不必要に遅くなります。 isspace()とはchar異なる負の値で未定義の動作を呼び出すため、引数を渡すべきではありませんEOF
chqrlie 2016年


0

両側から文字列をトリミングするには、オールディーを使用しますが、グーディーを使用します;)スペースよりも小さいASCIIで何でもトリミングできます。つまり、制御文字もトリミングされます。

char *trimAll(char *strData)
{
  unsigned int L = strlen(strData);
  if(L > 0){ L--; }else{ return strData; }
  size_t S = 0, E = L;
  while((!(strData[S] > ' ') || !(strData[E] > ' ')) && (S >= 0) && (S <= L) && (E >= 0) && (E <= L))
  {
    if(strData[S] <= ' '){ S++; }
    if(strData[E] <= ' '){ E--; }
  }
  if(S == 0 && E == L){ return strData; } // Nothing to be done
  if((S >= 0) && (S <= L) && (E >= 0) && (E <= L)){
    L = E - S + 1;
    memmove(strData,&strData[S],L); strData[L] = '\0';
  }else{ strData[0] = '\0'; }
  return strData;
}

size_t代わりに使用する必要がありますunsigned intstrncpy(strData,&strData[S],L)ソースと宛先の配列が重複しているため、コードには多くの冗長なテストがあり、未定義の動作を呼び出します。のmemmove()代わりに使用しますstrncpy()
chqrlie 2016年

この場合、宛先アドレスのインデックスは常にソースよりも小さいので問題ありませんが、memmoveの方が確かに優れています。
ДеянДобромиров

いいえ、それは大丈夫ではありません。ソース配列と宛先配列がどのようにオーバーラップするかは関係ありません。標準仕様を超えてライブラリ関数の実装を安全に想定できないため、未定義の動作を引き起こします。 最近のコンパイラーは、未定義の動作が発生する可能性のある状況を不当に利用し、安全に再生し、UBから離れて、初心者に安全でない仮定をさせない傾向があります。
chqrlie

0

これまでに投稿されたコードは最適ではないように見えるため、コードのみを含めています(まだコメントする担当者がいません)。

void inplace_trim(char* s)
{
    int start, end = strlen(s);
    for (start = 0; isspace(s[start]); ++start) {}
    if (s[start]) {
        while (end > 0 && isspace(s[end-1]))
            --end;
        memmove(s, &s[start], end - start);
    }
    s[end - start] = '\0';
}

char* copy_trim(const char* s)
{
    int start, end;
    for (start = 0; isspace(s[start]); ++start) {}
    for (end = strlen(s); end > 0 && isspace(s[end-1]); --end) {}
    return strndup(s + start, end - start);
}

strndup()GNU拡張機能です。あなたがそれまたは同等のものを持っていない場合は、自分でロールしてください。例えば:

r = strdup(s + start);
r[end-start] = '\0';

1
isspace(0)がfalseと定義されている場合、両方の機能を簡略化できます。またmemmove()ifブロックの内側を移動します。
chqrlie 2016年

0

ここでは、動的メモリ割り当てを使用して、入力文字列を関数trimStrにトリミングします。まず、入力文字列に空でない文字がいくつあるかを調べます。次に、そのサイズの文字配列を割り当て、nullで終了する文字を処理します。この関数を使用するときは、main関数内のメモリを解放する必要があります。

#include<stdio.h>
#include<stdlib.h>

char *trimStr(char *str){
char *tmp = str;
printf("input string %s\n",str);
int nc = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
  nc++;
 }
 tmp++;
}
printf("total nonempty characters are %d\n",nc);
char *trim = NULL;

trim = malloc(sizeof(char)*(nc+1));
if (trim == NULL) return NULL;
tmp = str;
int ne = 0;

while(*tmp!='\0'){
  if (*tmp != ' '){
     trim[ne] = *tmp;
   ne++;
 }
 tmp++;
}
trim[nc] = '\0';

printf("trimmed string is %s\n",trim);

return trim; 
 }


int main(void){

char str[] = " s ta ck ove r fl o w  ";

char *trim = trimStr(str);

if (trim != NULL )free(trim);

return 0;
}

0

これが私のやり方です。文字列を適切にトリミングするため、返された文字列の割り当てを解除したり、割り当てられた文字列へのポインタを失う心配はありません。それは可能な限り最短の回答ではないかもしれませんが、それはほとんどの読者にとって明白であるはずです。

#include <ctype.h>
#include <string.h>
void trim_str(char *s)
{
    const size_t s_len = strlen(s);

    int i;
    for (i = 0; i < s_len; i++)
    {
        if (!isspace( (unsigned char) s[i] )) break;
    }

    if (i == s_len)
    {
        // s is an empty string or contains only space characters

        s[0] = '\0';
    }
    else
    {
        // s contains non-space characters

        const char *non_space_beginning = s + i;

        char *non_space_ending = s + s_len - 1;
        while ( isspace( (unsigned char) *non_space_ending ) ) non_space_ending--;

        size_t trimmed_s_len = non_space_ending - non_space_beginning + 1;

        if (s != non_space_beginning)
        {
            // Non-space characters exist in the beginning of s

            memmove(s, non_space_beginning, trimmed_s_len);
        }

        s[trimmed_s_len] = '\0';
    }
}

読者には絶対に明らかですが、strlenは別のループを実行します.. :)
ingconti

0
char* strtrim(char* const str)
{
    if (str != nullptr)
    {
        char const* begin{ str };
        while (std::isspace(*begin))
        {
            ++begin;
        }

        auto end{ begin };
        auto scout{ begin };
        while (*scout != '\0')
        {
            if (!std::isspace(*scout++))
            {
                end = scout;
            }
        }

        auto /* std::ptrdiff_t */ const length{ end - begin };
        if (begin != str)
        {
            std::memmove(str, begin, length);
        }

        str[length] = '\0';
    }

    return str;
}

2
このコードは質問に答えることがありますが、問題を解決する方法や理由に関する追加のコンテキストを提供すると、回答の長期的な価値が向上します。
Nic3500 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.