Як стандартним чином обрізати пробіли / пробіли?


178

Чи існує чистий, бажано, стандартний метод обрізання пробілу провідних та кінцевих пробілів від рядка в С? Я б закрутив свою, але я думаю, що це спільна проблема з не менш поширеним рішенням.

Відповіді:


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: Ні. str- це локальна змінна, і її зміна не змінює вихідний покажчик, що передається. Виклики функцій у C завжди проходять через значення, ніколи не проходять через посилання.
Адам Розенфілд

11
@Raj: Немає нічого поганого у поверненні адреси, відмінної від тієї, яку було передано. Тут немає жодної вимоги, щоб повернене значення було дійсним аргументом free()функції. Зовсім навпаки - я створив це, щоб уникнути необхідності розподілу пам'яті для ефективності. Якщо передана адреса була розподілена динамічно, то абонент все ще несе відповідальність за звільнення цієї пам'яті, і абонент повинен бути впевнений, щоб не перезаписати це значення зі значенням, поверненим тут.
Адам Розенфілд

3
Ви повинні кинути аргумент для , isspaceщоб unsigned char, в іншому випадку ви закликаєте до невизначеному поведінки.
Roland Illig

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. Складено з 'куб.см.оздоблення стін.c -о обробкою'.


2
Ви повинні кинути аргумент для , isspaceщоб unsigned char, в іншому випадку ви закликаєте до невизначеному поведінки.
Roland Illig

@RolandIllig: Спасибі, я ніколи не розумів, що це необхідно. Виправлено це.
indiv

@Simas: Чому ти це кажеш? Функція викликає, 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
Ви повинні кинути аргумент для , isspaceщоб unsigned char, в іншому випадку ви закликаєте до невизначеному поведінки.
Roland Illig

1
слід додати, if(l==0)return;щоб уникнути нульової довжини str
ch271828n

11

Ось моя міні-бібліотека С для обрізки ліворуч, праворуч, обидва, всі, на місці та окремо, та обрізання набору вказаних символів (або пробіл за замовчуванням).

вміст 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підпрограми. Він обрізає набір символів, вказаний у роздільній строціабо пробіл, якщо нуль. Він обрізає ліворуч, праворуч і те, і все (як тр). Це не так багато, і він повторюється над рядком лише один раз. Деякі люди можуть скаржитися, що обрізка праворуч починається ліворуч, однак, жоден стринг не потрібен, який починається зліва. (Так чи інакше вам доведеться дістати до кінця рядка для правильних обрізків, щоб ви могли також виконати роботу, як ви йдете.) Можливо, слід зробити аргументи щодо розмірів конвеєра та кешу, і таке - хто знає . Оскільки рішення працює зліва направо і повторюється лише один раз, його також можна розширити для роботи на потоках. Обмеження: він не працює на рядках unicode .


2
Я підтримав це, і я знаю його старе, але я думаю, що тут є помилка. dtab[*d]не звертається *dдо того, unsigned intяк використовувати його як індекс масиву. У системі з підписаним знаком цього означення буде прочитано, dtab[-127]що спричинить помилки та, можливо, збій.
Зан Лінкс

2
Потенційне невизначене поведінка, dtab[*delim++]оскільки charзначення індексу повинні бути надані unsigned char. Код передбачає 8-розрядну char. delimслід оголосити як 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
Запропонуйте змінити, щоб while ((end >= begin) && isspace(str[end]))запобігти UB, коли str is "" . Prevents str [-1] `.
chux

Btw, я повинен змінити це на str [i - початок + 1], щоб працювати
truongnm

1
Ви повинні кинути аргумент для , isspaceщоб unsigned char, в іншому випадку ви закликаєте до невизначеному поведінки.
Roland Illig

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

@wovano Ні, це не так. Функції з <ctype.h>призначені для роботи з ints, які представляють собою unsigned charабо особливе значення EOF. Дивіться stackoverflow.com/q/7131026/225757 .
Roland Illig

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
Так, нарешті хтось, хто знає про тип невизначеної поведінки.
Roland Illig

2
@chux Я думаю, що це повинно бути len = (size_t) (ps) +1; інакше останній лист перекривається.
theriver

4

Ось таке рішення, подібне до @ adam-rosenfields, замість звичайного режиму модифікації, але без необхідності вдаватися до strlen (). Як і @jkramer, рядок регулюється ліворуч у буфері, щоб ви могли звільнити той же покажчик. Не є оптимальним для великих струн, оскільки в ньому не використовується пам'ятка. Включає операторів ++ / -, які згадує @ 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] = ""; без підпису 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, дякую за повідомлення про помилку. Я оновив логіку, щоб уникнути відступу лівої частини буфера, коли рядок містить лише пробіл. Чи відповідає ця нова версія вашим проблемам?
Rhys Ulerich

Мовні юристи, напевно, кричать на вас за просту думку про спекуляції щодо створення вказівника на таблицю, що передує точці "a" (що і буде робити ваш "--p"). У реальному світі ти, мабуть, добре. Але ви також можете просто змінити '> =' на '>' і перемістити декремент p в 'isspace (* - p)'.
Віллемос

Я думаю, що адвокатам було б добре, оскільки це просто порівняння адреси, не торкаючись її, але мені подобається і ваша пропозиція щодо декрету. Я відповідно оновив його. Дякую.
Ріс Улеріч

1
doukremt, Ви стурбовані тим, що весь буфер після foobar не заповнений нулями? Якщо так, то було б набагато корисніше, якби ви сказали це прямо, а не кидали розпливчасті скелі.
Rhys Ulerich

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; але його буде працювати лише з одним словом, яке може бути не тим, чого хотів ОП (тобто обрізка "abc", ймовірно, повинна призвести до "ab c", тоді як ваш одиночний scanf просто призводить до "a"). Тож нам потрібна петля та лічильник для пропущених символів із %nспецифікатором перетворення, і врешті-решт, я боюсь зробити це просто простіше.
Пітер - Відновіть Моніку

Дуже корисно, коли потрібно, щоб перше слово рядка не враховувало будь-яких початкових пробілів.
J ... S

3

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

  1. Повернув інший вказівник всередині початкового рядка вказівника (вид болю жонглювати двома різними покажчиками на одне і те ж).
  2. Безкоштовно використовує такі речі, як strlen (), які попередньо повторюють весь рядок.
  3. Використовуються не портативні функції, пов’язані з ОС.
  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
Ви повинні кинути аргумент для , isspaceщоб unsigned char, в іншому випадку ви закликаєте до невизначеному поведінки.
Roland Illig

Оскільки ця відповідь стосується "Витрачених циклів", зауважте, що код зайво копіює всю жало, коли немає місця. Провідний while (isspace((unsigned char) *szWrite)) szWrite++;заважав би цьому. Код також копіює весь пробіл.
chux

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

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

@chux, для ситуації "копії, коли немає місця", лише виконання, *szWrite = *szReadколи покажчики не рівні, пропустить запис у такому випадку, але тоді ми додали ще одне порівняння / гілку. З сучасним процесором / MMU / BP я не маю уявлення, чи буде ця перевірка втратою чи виграшем. З більш простими процесорами та архітектурою пам'яті дешевше просто зробити копію і пропустити порівняння.
Джейсон Стюарт

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 - Так, кожен персонаж проходить тестування двічі. Я хотів би, щоб цей код був фактично перевіреним, особливо це стосується рядків з великою кількістю пробілів, порівняно з іншими алгоритмами тут.
David R

trim()ГАРАЗД. Кутовий випадок: trim2(char *d, const char *s)має проблеми при d,sперекритті та s < d.
chux

@chux - У такому кутовому випадку, як слід trim()поводитися? Ви просите обрізати та скопіювати рядок у пам'ять, зайняту самим рядком. На відміну від memmove()цього, потрібно визначити довжину вихідного рядка перед тим, як зробити обрізку, що вимагає додаткового часу сканування всієї рядка. Краще написати іншу rtrim2()функцію, яка знає скопіювати джерело до місця призначення назад і, ймовірно, бере додатковий аргумент довжини рядка.
David R

1

Я не впевнений, що ви вважаєте "безболісним".

Струни досить болючі. Ми можемо знайти першу позицію символів, що не є пробілом, тривіально:

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

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

while (* q) q ++;
робити {q--; } while (isspace (* q));

(Я позбавив вас від того, що ви одночасно користуєтесь операторами *та ++операторами.)

Питання зараз - що ти з цим робиш? Даний тип даних насправді не є великим надійним рефератом, Stringпро який легко подумати, але натомість ледве чи не більшим, ніж масив байтів для зберігання. Не маючи надійного типу даних, неможливо написати функцію, яка буде виконувати те саме, що і chompфункція PHperytonby . Що б повернула така функція в C?


Це добре працює, якщо рядок не складається з усіх пробілів. Потрібно перевірити один раз, перш ніж do { q--; } ...знати *q != 0.
chux

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()повертає a, 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

@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

Найпростіший спосіб пропустити провідні пробіли в рядку - це,

#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 ".
David R Tribble

1

Добре, це мій погляд на питання. Я вважаю, що це найбільш стисле рішення, яке змінює рядок на місці ( freeбуде працювати) і уникає будь-якого UB. Для невеликих струн це, мабуть, швидше, ніж рішення, що стосується пам'яті.

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()не є частиною стандарту С, а лише 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
Не використовуйте цей код. Це створює переповнення буфера.
Roland Illig

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

Для обрізки моїх струн з обох сторін я використовую олджі, але гуді;) Це може обрізати що завгодно з 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

У цьому випадку це нормально, оскільки адреса призначення завжди має менший індекс, ніж джерело, але так, пам’ятка дійсно буде кращою.
Деян Добромиров

ні, це не нормально. не має значення, як масиви джерела та призначення перекриваються, він викликає невизначене поведінку, оскільки ви не можете безпечно робити припущення щодо реалізації функцій бібліотеки за межами їх стандартної специфікації. Сучасні компілятори схильні використовувати несправедливі переваги ситуацій з потенційною невизначеною поведінкою, грати в безпеку і триматися подалі від УБ і не дозволяти новачкам робити небезпечні припущення.
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.