Ошибка сегмента в результате strdup и strtok

#c #strtok

#c #strtok

Вопрос:

Мне было поручено домашнее задание от моего профессора колледжа, и я, кажется, обнаружил какое-то странное поведение strtok

В принципе, мы должны проанализировать CSV-файл для моего класса, где количество токенов в CSV известно, и последний элемент может содержать дополнительные "," символы.

Пример строки:

 Hello,World,This,Is,A lot, of Text
  

Где токены должны выводиться как

 1. Hello
2. World
3. This
4. Is
5. A lot, of Text
  

Для этого назначения мы ДОЛЖНЫ использовать strtok . Из-за этого я обнаружил в каком-то другом сообщении SOF, что использование strtok с пустой строкой (или передача "n" в качестве второго аргумента) приводит к чтению до конца строки. Это идеально подходит для моего приложения, поскольку дополнительные запятые всегда появляются в последнем элементе.

Я создал этот код, который работает:

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

#define NUM_TOKENS 5

const char *line = "Hello,World,This,Is,Text";

char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);

    // Make an array the correct size to hold num_tokens
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);

    int i = 0;
    for (char *token = strtok(copy, ",n"); i < NUM_TOKENS; token = strtok(NULL, i < NUM_TOKENS - 1 ? ",n" : "n"))
    {
        tokens[i  ] = strdup(token);
    }

    free(copy);

    return tokens;
}

int main()
{
    char **tokens = split_line(line, NUM_TOKENS);

    for (int i = 0; i < NUM_TOKENS; i  )
    {
        printf("%sn", tokens[i]);
        free(tokens[i]);
    }
}
  

Теперь это работает и должно принести мне полную пользу, но я ненавижу эту троичную систему, которая не должна быть нужна:

 token = strtok(NULL, i < NUM_TOKENS - 1 ? ",n" : "n");
  

Я бы хотел заменить метод на эту версию:

 char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);

    // Make an array the correct size to hold num_tokens
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);

    int i = 0;
    for (char *token = strtok(copy, ",n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",n"))
    {
        tokens[i  ] = strdup(token);
    }

    tokens[i] = strdup(strtok(NULL, "n"));

    free(copy);

    return tokens;
}
  

Это гораздо приятнее для моего воображения, поскольку гораздо легче увидеть, что есть окончательный вариант. Вы также избавляетесь от странного троичного оператора.

К сожалению, это ошибка segfaults! Я ни за что на свете не могу понять, почему.

Редактировать: Добавить несколько примеров вывода:

 [11:56:06] gravypod:test git:(master*) $ ./test_no_fault 
Hello
World
This
Is
Text
[11:56:10] gravypod:test git:(master*) $ ./test_seg_fault 
[1]    3718 segmentation fault (core dumped)  ./test_seg_fault
[11:56:14] gravypod:test git:(master*) $ 
  

Комментарии:

1. Ах, теперь я вижу путаницу n. Используя strtok с n, вы бы построчно проанализировали весь текстовый файл, загруженный в память. Если вы обрабатываете только одну строку, которая сама по себе не содержит разрывов строк, то это не имеет магического значения.

Ответ №1:

Пожалуйста, проверьте возвращаемое значение из strtok , прежде чем рисковать переходом NULL к другой функции. Ваш цикл вызывается strtok на один раз больше, чем вы думаете.

Более обычно использовать это возвращаемое значение для управления вашим циклом, тогда вы не зависите от своих данных. Что касается разделителей, лучше сохранить это простым и не пробовать ничего необычного.

 char **split_line(const char *line, int num_tokens)
{
    char *copy = strdup(line);
    char **tokens = (char**) malloc(sizeof(char*) * num_tokens);
    int i = 0;
    char *token;
    char delim1[] = ",rn";
    char delim2[] = "rn";
    char *delim = delim1;                   // start with a comma in the delimiter set

    token = strtok(copy, delim);
    while(token != NULL) {                  // strtok result comtrols the loop
        tokens[i  ] = strdup(token);
        if(i == NUM_TOKENS) {
            delim = delim2;                 // change the delimiters
        }
        token = strtok(NULL, delim);    
    }
    free(copy);
    return tokens;
}
  

Обратите внимание, что вы также должны проверить возвращаемые значения из malloc и strdup и правильно освободить свою память

Комментарии:

1. Я исправлю это управление памятью. Если это альтернатива, я просто придерживаюсь своего троичного. Я думаю, что эта проблема требует шаблона цикла for. Спасибо за помощь!

2. Спасибо за голосование. for Цикл для strtok довольно уродлив и не идиоматичен, и, как вы обнаружили, хлопотный.

3. Если вас беспокоит количество строк здесь, вы могли бы вырезать все переменные delim, сделать первую из них «,n» или «, r n», как здесь, и просто поместить свою троичную строку в последнюю строку strtok. Преимущество циклов for заключается в том, что это гарантирует, что вы не переполните массив tokens: недостатком является то, что вы плохо справляетесь с входными данными с меньшим количеством сегментов. Которая разрешима с обоими подходами, но тогда ваш вызывающий код должен принимать возвращаемые значения с меньшим количеством элементов или возвращать выделенные пустые строки и т.д.

4. Я хотел сделать код очевидным. Я мог бы использовать строку с одним разделителем ",rn" и просто увеличить указатель на строку, когда ',' больше не требовалось. Пожалуйста, напишите код ясно и незатейливо. Возможно, у вас есть «фантазия», что вы хотите заставить какой-то метод работать, но первостепенной заботой должна быть читаемость и ремонтопригодность, а также минимальное время, затрачиваемое на отладку результата сложных выражений.

5. Я совсем не это имел в виду. ОП сказал, что не собирается использовать ваш код, потому что он считает его слишком сложным, поэтому он будет использовать свой тернарный, я рассказал ему, как использовать ваш код с его тернарным в меньшем количестве строк.

Ответ №2:

Когда вы дойдете до последнего цикла, вы получите

 for (char *token = strtok(copy, ",n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",n"))
  
  1. тело цикла
  2. шаг увеличения цикла, т.е. token = strtok(NULL, ",n") (с неправильным вторым аргументом)
  3. проверка продолжения цикла i < NUM_TOKENS - 1

т. е. он все еще вызывался strtok , даже несмотря на то, что вы теперь вне диапазона. Здесь у вас также есть разница в индексах вашего массива: вы хотели бы инициализировать i=0 не 1.

Вы могли бы избежать этого, например

  • превращение начального strtok в особый случай вне цикла, например

     int i = 0;
    tokens[i  ] = strdup(strtok(copy, ",n"));
      
  • затем перемещение strtok(NULL, ",n") внутри цикла

Я также удивлен, что вы вообще хотите, чтобы n был там, или даже вам нужно вызвать последний strtok (разве это уже не просто указывает на остальную часть строки? Если вы просто пытаетесь обрезать завершающую новую строку, есть более простые способы) но я годами не использовал strtok.

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

Комментарии:

1. Если вы посмотрите на мой вопрос, вы поймете, почему мне нужен n в качестве окончательного варианта. Взгляните на то, что необходимо распечатать. Посмотрите на «5». пример вывода.

2. Я понимаю, почему нет запятой, но я не вижу никаких новых строк во входной строке.

Ответ №3:

Помните, что это strtok идентифицирует токен, когда он находит любой из символов в строке-разделителе (второй аргумент strtok() ) — он не пытается сопоставить всю строку-разделитель непосредственно.

Таким образом, тернарный оператор никогда не был нужен в первую очередь — строка будет маркирована на основе появления , OR n во входной строке, поэтому работает следующее:

 for (token = strtok(copy, ",n"); i < NUM_TOKENS; token = strtok(NULL, ",n"))
{                                                                                                         
    tokens[i  ] = strdup(token);                                                                          
} 
  

Второй пример segfaults, потому что он уже маркировал входные данные до конца строки к моменту выхода из for цикла. strtok() Повторный вызов устанавливает token значение NULL , и ошибка сегмента генерируется при strdup() вызове по NULL указателю. Удаление дополнительного вызова strtok дает ожидаемые результаты:

 for (token = strtok(copy, ",n"); i < NUM_TOKENS - 1; token = strtok(NULL, ",n"))
{                                                                                 
    tokens[i  ] = strdup(token);                                                  
}                                                                                 
tokens[i] = strdup(token);                                                        
  

Комментарии:

1. Если вы посмотрите на вопрос 5. A lot, of Text , который включает запятую, вы поймете, почему OP хочет изменить разделители.