Как стандартным способом обрезать начальные / конечные пробелы?


178

Существует ли чистый, желательно стандартный метод обрезания начальных и конечных пробелов из строки в 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
Извините, первый ответ вообще не годится, если вас не волнуют утечки памяти. Теперь у вас есть две перекрывающиеся строки (оригинал, у которого обрезаны завершающие пробелы, и новая). Только исходная строка может быть освобождена, но если вы это сделаете, вторая указывает на освобожденную память.
Дэвид Нехм

7
@nvl: нет выделенной памяти, поэтому нет свободной памяти.
Адам Розенфилд

15
@nvl: No. str- локальная переменная, и ее изменение не изменяет исходный указатель, который передается. Вызовы функций в C всегда передаются по значению, а не по ссылке.
Адам Розенфилд

11
@Raj: Нет ничего плохого в возврате адреса, отличного от того, который был передан. Здесь не требуется, чтобы возвращаемое значение было допустимым аргументом free()функции. Скорее наоборот - я разработал это, чтобы избежать необходимости выделения памяти для эффективности. Если переданный адрес был распределен динамически, то вызывающая сторона по-прежнему отвечает за освобождение этой памяти, и вызывающая сторона должна быть уверена, что не перезаписывает это значение значением, возвращаемым здесь.
Адам Розенфилд

3
Вы должны привести аргумент к isspaceto unsigned char, в противном случае вы вызываете неопределенное поведение.
Роланд Иллиг

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 trim.c -o trim'.


2
Вы должны привести аргумент к isspaceto unsigned char, в противном случае вы вызываете неопределенное поведение.
Роланд Иллиг

@RolandIllig: Спасибо, я так и не понял, что это необходимо. Починил это.
Indiv

@ Симас: Почему ты так говоришь? Вызовы функций, isspace()так почему же существует разница между " "и "\n"? Я добавил модульные тесты для переноса строк и это выглядит КИ для меня ... ideone.com/bbVmqo
Indiv

1
@indiv будет обращаться к недопустимому блоку памяти при ручном распределении. Именно эта строка: *(endp + 1) = '\0';. В примере теста ответа используется буфер 64, что позволяет избежать этой проблемы.
Симас

1
@nolandda: спасибо за детали. Я исправил это и обновил тест, чтобы обнаружить переполнение буфера, поскольку у меня нет доступа к valgrind в данный момент.
Indiv

23

Мое решение. Строка должна быть изменяемой. Преимущество перед некоторыми другими решениями заключается в том, что он перемещает непробельную часть в начало, поэтому вы можете продолжать использовать старый указатель на случай, если вам придется освободить () его позже.

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, поэтому, возможно, вам нужно создать свой собственный strndup () с помощью malloc () и strncpy ().

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 - Восстановить Монику

1
Вы должны привести аргумент к isspaceto unsigned char, в противном случае вы вызываете неопределенное поведение.
Роланд Иллиг

1
следует добавить, if(l==0)return;чтобы избежать нулевой длины str
ch271828n

11

Вот моя библиотека C mini для обрезки левого, правого, как, все, на месте и отдельно, и обрезать набор указанных символов (или пробел по умолчанию).

содержимое 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); }

Одна основная рутина делает все это. Он обрезается на месте, если src == dst , в противном случае он работает как strcpyпроцедуры. Обрезает набор символов, указанных в строке delimили пробел, если ноль. Он обрезает левый, правый, оба и все (как tr). В этом нет ничего особенного, и он перебирает строку только один раз. Некоторые люди могут жаловаться, что обрезка справа начинается слева, однако, нет необходимости в strlen, который начинается в любом случае слева. (Так или иначе, вы должны добраться до конца строки для правильной обрезки, так что вы можете выполнять работу по ходу дела.) Могут быть аргументы о конвейерной обработке и размерах кэша и тому подобное - кто знает , Поскольку решение работает слева направо и повторяется только один раз, его можно расширить и на потоки. Ограничения: он не работает со строками Unicode .


2
Я проголосовал за это, и я знаю его старый, но я думаю, что есть ошибка. dtab[*d]не бросает *dна unsigned intперед ее использованием в качестве индекса массива. В системе с подписанным char это будет считывать, dtab[-127]что приведет к ошибкам и, возможно, к аварийному завершению .
Zan Lynx

2
Потенциальное неопределенное поведение включено, dtab[*delim++]потому что charзначения индекса должны быть приведены к unsigned char. Код предполагает 8-битный char. delimдолжен быть объявлен как const char *. dtab[0xFF & (unsigned int)*d]понятнее как dtab[(unsigned char)*d]. Код работает с строками в кодировке UTF-8, но не удаляет последовательности без ASCII.
Chqrlie

@ Майкл-Плейер, это выглядит интересно. Почему бы вам не протестировать и не поместить его на 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
Предложить изменение, чтобы while ((end >= begin) && isspace(str[end]))предотвратить UB, когда str is "" . Prevents str [-1] `.
chux - Восстановить Монику

Кстати, я должен изменить это на str [i - begin + 1], чтобы работать
truongnm

1
Вы должны привести аргумент к isspaceto unsigned char, в противном случае вы вызываете неопределенное поведение.
Роланд Иллиг,

@RolandIllig, почему это было бы неопределенным поведением? Функция предназначена для работы с символами.
wovano

@wovano Нет, это не так. Функции из <ctype.h>предназначены для работы с целочисленными значениями, которые представляют либо unsigned charспециальное значение, либо EOF. См. Stackoverflow.com/q/7131026/225757 .
Роланд Иллиг

8

До поздней вечеринки

Особенности:
1. Обрежьте начало быстро, как и в ряде других ответов.
2. Пройдя до конца, обрежьте вправо только с 1 тестом на петлю. Как @ jfm3, но работает в течение всего белого пространства строки)
3. Для того, чтобы избежать неопределенное поведение , когда charэто знаковое char, приведение *sк unsigned char.

Обработка символов «Во всех случаях аргумент является аргументом int, значение которого должно быть представлено как unsigned charили должно быть равно значению макроса EOF. Если аргумент имеет любое другое значение, поведение не определено». C11 §7.4 1

#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.
Роланд Иллиг

2
@chux Я думаю, что это должно быть len = (size_t) (пс) +1; в противном случае последняя буква перекрывается.
19

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: unsigned x = 0x20202020; char s [4] = ""; без знака у = 0х20202020; printf ("& x, & s, & y =% p,% p,% p \ n", & x, & s, & y); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y); trim_whitespace (ы); printf ("x, [s], y =% 08x, [% s],% 08x \ n", x, s, y);
Villemoes

@ Villemoes, спасибо за сообщение об ошибке. Я обновил логику, чтобы избежать выхода из левой части буфера, когда строка содержит только пробелы. Эта новая версия решает ваши проблемы?
Рис Улерих

Языковые юристы, вероятно, будут кричать на вас за простую мысль о том, чтобы создать указатель на символ, предшествующий тому, на который указывает «а» (что и будет делать ваш «--p»). В реальном мире вы, вероятно, в порядке. Но вы также можете просто изменить '> =' на '>' и переместить декремент p в 'isspace (* - p)'.
Villemoes

Я думаю, что с юристами все будет в порядке, так как он просто сравнивает адрес, не касаясь его, но мне также нравится ваше предложение относительно постановления. Я обновил его соответственно. Спасибо.
Рис Улерих,

1
doukremt, вы обеспокоены тем, что весь буфер после foobar не заполнен нулями? Если это так, было бы немного полезнее, если бы вы сказали об этом прямо, а не бросали туманные камни.
Рис Улерих,

3

Другой, с одной линией, выполняющей настоящую работу:

#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спецификатором преобразования, и, в конце концов, я просто боюсь сделать это вручную.
Питер - Восстановить Монику

Очень полезно, когда вы хотите, чтобы первое слово строки игнорировало любые начальные пробелы.
J ... S

3

Мне не понравились большинство из этих ответов, потому что они сделали одно или несколько из следующих действий ...

  1. Возвращает другой указатель внутри строки исходного указателя (боль в том, чтобы совмещать два разных указателя на одну и ту же вещь).
  2. Сделано безвозмездное использование таких вещей, как strlen (), которые предварительно повторяют всю строку.
  3. Используются непереносимые для ОС функции lib.
  4. Backscanned.
  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, в противном случае вы вызываете неопределенное поведение.
Роланд Иллиг,

Поскольку этот ответ касается «потраченных впустую циклов», обратите внимание, что код без необходимости копирует весь код, когда нет места. Ведущий while (isspace((unsigned char) *szWrite)) szWrite++;помешал бы этому. Код также копирует все оставшиеся пробелы.
chux - Восстановить Монику

@chux эта реализация мутирует на месте с отдельными указателями чтения и записи (в отличие от возврата нового указателя в другом месте), поэтому предложение по переходу szWrite к первому незаполненному в первой строке оставит начальный пробел в оригинальная строка.
Джейсон Стюарт

@chux, вы правы, что он копирует конечный пробел (до добавления нуля после последнего непробельного символа), но я выбрал цену, чтобы избежать предварительного сканирования строки. Для небольшого количества конечных WS дешевле просто скопировать байты, чем предварительно сканировать всю строку для последнего символа без WS. Для большого количества конечных WS предварительное сканирование, вероятно, стоило бы уменьшить количество записей.
Джейсон Стюарт

@chux, для ситуации «копии, когда нет места», выполнение только *szWrite = *szReadтогда, когда указатели не равны, в этом случае пропускает записи, но затем мы добавили еще одну ветвь сравнения / сравнения. С современным CPU / MMU / BP я понятия не имею, будет ли эта проверка проигрышной или выигрышной. С более простыми процессорами и архитектурой памяти дешевле просто скопировать и пропустить сравнение.
Джейсон Стюарт

2

Я не уверен, что вы считаете "безболезненным".

Струны C довольно болезненны. Мы можем найти первую позицию непробельного символа тривиально:

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

Мы можем найти последнюю позицию непробельного символа с двумя похожими тривиальными ходами:

while (* q) q ++;
do {q--; } while (isspace (* q));

(Я избавлен вам боль с помощью *и ++операторов одновременно.)

Вопрос сейчас в том, что вы делаете с этим? Тип данных, который мы имеем в виду, на самом деле не является большим здравым абстракцией, Stringо котором легко думать, а просто не больше, чем массив байтов хранения. Не имея надежного типа данных, невозможно написать функцию, которая будет выполнять те же функции, что и chompфункция PHperytonby . Что бы могла вернуть такая функция в C?


Это работает хорошо, если строка не состоит из всех пробелов. Нужна однократная проверка, прежде чем do { q--; } ...узнать *q != 0.
chux - Восстановить Монику

2

Очень поздно на вечеринку ...

Однопроходное решение для сканирования вперед без возврата. Каждый символ в исходной строке проверяется ровно один раз дважды. (Так что это должно быть быстрее, чем большинство других решений здесь, особенно если в исходной строке много пробелов.)

Это включает два решения: одно для копирования и обрезки исходной строки в другую строку назначения, а другое для обрезки исходной строки на месте. Обе функции используют один и тот же код.

Строка (модифицируемая) перемещается на место, поэтому исходный указатель на нее остается неизменным.

#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
Каждый символ в исходной строке проверяется ровно один раз : на самом деле большинство символов в исходной строке проверяются дважды: по сравнению с '\0'и затем проверяется с помощью isspace(). Кажется расточительным проверять всех персонажей isspace(). Откат от конца строки должен быть более эффективным для непатологических случаев.
Chqrlie

@chqrlie - Да, каждый персонаж проходит тестирование дважды. Мне бы хотелось, чтобы этот код был действительно протестирован, особенно с учетом строк с большим количеством пробелов по сравнению с другими алгоритмами здесь.
Дэвид Р. Триббл

trim()ХОРОШО. Угловой корпус: trim2(char *d, const char *s)возникают проблемы при d,sналожении и s < d.
chux - Восстановить Монику

@chux - Как trim()вести себя в этом угловом случае ? Вы просите обрезать и скопировать строку в память, занятую самой строкой. В отличие от memmove()этого, это требует определения длины исходной строки перед выполнением самой обрезки, что требует дополнительного сканирования всей строки. Лучше написать другую rtrim2()функцию, которая знает, как скопировать источник в место назначения в обратном направлении, и, вероятно, принимает дополнительный аргумент длины строки источника.
Дэвид Р Триббл

1

Используйте библиотеку строк , например:

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

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

... как вы говорите, это "распространенная" проблема, да, вам нужно включить #include или около того, и он не включен в libc, но не изобретайте свою собственную хакерскую работу, хранящую случайные указатели, а size_t таким образом приводит только к переполнение буфера.



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

@chqrlie вы правы, просто включили ваши предложения. Я понимаю, что копирование, когда исходное и целевое перекрытия могут вызвать неопределенное поведение, но просто хочу указать, что в данном конкретном случае это не должно вызывать никаких проблем, поскольку мы всегда будем копировать из более поздней позиции памяти в начало, Спасибо за ответ.
wallek876

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 10.10.16

@chqrlie хорошая точка зрения, строка со всеми пробелами должна была бы зацикливаться после начала, об этом не думали.
wallek876

На самом деле мое предложение было неверным, так как endне указывало на завершающий нулевой байт, и у вас end = ++i;все еще была проблема со строками, содержащими все пробельные символы. Я только что исправил код.
Chqrlie

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 ".
Дэвид Р. Триббл

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 помогает обрезать все пробелы.

  • Запустите первый цикл, чтобы проверить из последнего байта символ пробела и уменьшить переменную длины
  • Запустите второй цикл, чтобы проверить из первого байта символ пробела и уменьшить переменную длины и увеличить указатель на символ.
  • Наконец, если переменная длины больше 0, используйте strndupдля создания нового строкового буфера, исключив пробелы.

Просто немного придирка, strndup()не является частью стандарта C, а только Posix. Но так как это довольно легко реализовать, это не имеет большого значения.
Патрик Шлютер

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 поверх последнего пробела, считая от конца до конца. Избавление от пробелов означает движение вещей вокруг. Если вы хотите сделать это на месте (вероятно, разумно), вы можете просто перемещать все назад на один символ, пока не будет пробела. Или, чтобы быть более эффективным, вы можете найти индекс первого непробельного символа и сдвинуть все обратно на это число. Или вы можете просто использовать указатель на первый непробельный символ (но тогда вам нужно быть осторожным так же, как вы делаете с 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 отредактировал тестовую строку, добавив в нее слово «отстой». Нет. Оригинальный автор просто честен.
Джеймс Моррис

1
Не используйте этот код. Это производит переполнение буфера.
Роланд Иллиг

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()чтобы (unsigned char)избежать неопределенного поведения потенциально отрицательных значений. Также избегайте перемещения строки, если в ней ltrim()нет необходимости.
Chqrlie

0

Большинство ответов до сих пор делают одно из следующего:

  1. Возврат в конце строки (то есть найти конец строки и затем искать назад, пока не будет найден непробельный символ), или
  2. strlen()Сначала позвоните , сделав второй проход через всю строку.

Эта версия делает только один проход и не возвращается. Следовательно, он может работать лучше, чем другие, хотя только в том случае, если распространены сотни пробелов (что весьма обычно при работе с выводом запроса 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

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

0

Что вы думаете об использовании функции StrTrim, определенной в заголовке Shlwapi.h.? Это прямолинейно, скорее, самоопределение.
Подробности можно найти по адресу :
http://msdn.microsoft.com/en-us/library/windows/desktop/bb773454(v=vs.85).aspx

Если у вас есть
char ausCaptain[]="GeorgeBailey ";
StrTrim(ausCaptain," ");
Это даст ausCaptainкак "GeorgeBailey"нет "GeorgeBailey ".


0

Чтобы обрезать мои струны с обеих сторон, я использую oldie, но gooody;) Он может обрезать что угодно с 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 int. Код имеет множество избыточных тестов и вызывает неопределенное поведение, strncpy(strData,&strData[S],L)потому что массивы источника и назначения перекрываются. Используйте memmove()вместо strncpy().
Chqrlie

В этом случае это нормально, так как адрес получателя всегда имеет меньший индекс, чем источник, но да, memmove действительно будет лучше.
Деян Добромиров

нет это не хорошо. не имеет значения, как перекрываются массивы источника и назначения, он вызывает неопределенное поведение, потому что вы не можете безопасно делать предположения относительно реализации функций библиотеки за пределами их стандартной спецификации. Современные компиляторы имеют тенденцию использовать несправедливое преимущество в ситуациях с потенциально неопределенным поведением, избегать рискованных действий и не позволять новичкам делать небезопасные предположения.
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)определено как ложное, вы можете упростить обе функции. Также переместите memmove()внутрь ifблока.
Chqrlie

0

Здесь я использую динамическое выделение памяти для обрезки входной строки до функции trimStr. Сначала мы выясним, сколько непустых символов существует во входной строке. Затем мы выделяем массив символов с таким размером и заботимся о нулевом символе в конце. Когда мы используем эту функцию, нам нужно освободить память внутри основной функции.

#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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.