Программа C для удаления пустых строк

Я написал следующую программу на C для удаления всех пустых строк из текстового файла «LINES.txt».

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

int main()
{
    FILE *fp, *ft;
    fp = fopen("LINES.txt", "r");
    ft = fopen("TEMP.txt", "w");
    if (fp == NULL || ft == NULL)
    {
        printf("Cannot open file\n");
        fclose(fp);
        fclose(ft);
        exit(1);
    }

    int e_flag = 0;
    char ch, c[100];
    int i = 0;
    while((ch = fgetc(fp)) != EOF)
    {
        if (ch != '\n' || ch != '\r')
        {
            c[i] = ch;
            i++;
            if (c[i] != ' ' && c[i] != '\t')
                e_flag = 1;
        }
        else
        {
            c[i] = '\0';
            if (e_flag == 1)
            {
                fputs(c, fp);
                e_flag = 0;
            }
            i = 0;
        }
    }

    fclose(fp);
    fclose(ft);
    remove("LINES.txt");
    rename("TEMP.txt", "LINES.txt");
    printf("\nFile editing complete\n");
    return 0;
}

Кажется, код компилируется хорошо, без ошибок и предупреждений. Однако, когда я пытаюсь запустить его на кодовых блоках с помощью GCC, я получаю сообщение «Программа перестала работать». Я не могу понять, что здесь происходит не так.

Того же самого можно легко добиться, используя fgets() вместо fgetc(). Однако проблема состоит в том, чтобы использовать fgetc(). Заранее спасибо.

Насколько я знаю, небезопасно вызывать fclose для NULL-дескрипторов файлов, поэтому используйте брандмауэр для обработки «Невозможно открыть файл», например if (fp != NULL) { fclose(fp); fp = NULL; }.

jarmod 05.04.2024 20:11

Первая ошибка, которую я вижу, это if (ch!='\n'||ch!='\r'). Это условие всегда выполняется. Дальше я не пытался понять логику.

n. m. could be an AI 05.04.2024 20:12

Не вызывайте fclose указателем NULL. Это не ваша проблема, если файлы открываются успешно, но определенная проблема, если один или оба не открываются. ch должно быть int, и ничто не мешает i превысить 99.

Retired Ninja 05.04.2024 20:12

Если бы вы запустили это в отладчике, вы бы легко увидели, в чем проблема и что происходит.

Retired Ninja 05.04.2024 20:14

Вы ничего не пишете в ft.

Allan Wind 05.04.2024 21:56

Еще стоит почитать: stackoverflow.com/questions/26337003/…

Barmar 05.04.2024 21:56

Вместо чтения файла посимвольно прочитайте целую строку с помощью fgets(). Затем вы можете проверить, пуста ли строка. Если нет, запишите это в выходной файл.

Barmar 05.04.2024 21:57

Даже если вы исправите логическую ошибку и используете правильный дескриптор для записи в TEMP.txt, ваша проблема приведет к удалению всех «\r» и «\n», а не только пустых строк.

Allan Wind 05.04.2024 21:59

Вы не защищаете от переполнения c[i]

Allan Wind 05.04.2024 22:00
fgetc() возвращает int, а не char.
Allan Wind 05.04.2024 22:02

Вам нужно либо знать максимальную длину строки (в настоящее время жестко закодировано 100 и не проверено), динамически изменять размер c (как для вас подойдет getline()), чтобы прочитать всю строку, или использовать fseek(), чтобы запомнить начало строки, а затем посмотреть вперед, чтобы найти конец строки; если были только пробелы, пропустите строку, в противном случае вернитесь к началу строки с помощью fseek() и теперь распечатайте ее. Вы также можете mmap файл для просмотра вперед.

Allan Wind 05.04.2024 23:06

На самом деле, вам также следует указать, какие терминаторы строк вы хотите поддерживать. Вы работаете с eol («\r\n»), unix («\n»), устаревшим Mac («\r»). Некоторые или все? Если вам нужны DOS и Unix, но не Mac, как вы хотите обрабатывать «\r», за которым не следует «\n»?

Allan Wind 06.04.2024 06:59

Вы должны определить, что вы имеете в виду с помощью пустой строки. Пустая строка (с пробелом и табуляцией) пуста?

Allan Wind 06.04.2024 07:05

@Бармар говорит мудро. Просто объявите достаточный буфер, прочитайте с помощью fgets() и проверьте, является ли первый символ в буфере '\n'. Если да — строка пуста. Это кажется гораздо проще, чем чтение, ориентированное на персонажей. Допустим, ваша строка не может содержать более 1023 символов. Все, что вам нужно, это char buf[1024]; while (fgets (buf, sizeof buf, stdin)) { if (*buf == '\n') { puts ("empty line"); } } Это можно всю main() выплевывать "empty line" каждый раз, когда вы сталкиваетесь с ним.

David C. Rankin 06.04.2024 09:42

Ниже вы получили пару хороших ответов. Убедитесь, что вы принимаете лучший вариант, нажав на галочку рядом с ним.

Allan Wind 09.04.2024 21:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
16
185
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

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

Также добавлена ​​проверка ошибок. Можно было бы добавить больше проверок на ошибки, и я выделил в комментариях как минимум одно такое место.

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

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

/* Note: some files have lines longer than 100 chars. Update this value if needed: */
#define BUFF_SIZE 100 

int main()
{
    FILE *fp_in = fopen("LINES.txt", "r");
    FILE *fp_out = fopen("TEMP.txt", "w");
    int i=0, ch, e_flag = 0;
    char c_buff[BUFF_SIZE+1];

    if (fp_in == NULL || fp_out == NULL)
    {
        printf("Cannot open file\n");
        if (fp_in)
            fclose(fp_in);
        if (fp_out)
            fclose(fp_out);
        exit(EXIT_FAILURE);
    }

    while((ch = fgetc(fp_in)) != EOF)
    {
        switch(ch)
        {
            case '\n': /* newline */
                if (e_flag == 1)
                {
                    /* Code comes here if we have a newline and it
                    * is Not the 1st char of the line.  Else, it 
                    * would be a blank line and we don't want 
                    * to write blank lines.*/
                    c_buff[i++] = (char)ch;
                    c_buff[i] = '\0';

                    /* This is one reason I changed the file pointer 
                    * naming  convention...
                    * ... to add clarity and ease trouble-shooting. 
                    * Original  code was  writing to the input file.  
                    * Pretty sure That wasn't desired. */
                    fputs(c_buff, fp_out); /* Write to OUTPUT file */
                    i = e_flag = 0;
                }
            break;

            /* Exclude these from buffer/output *unless* the line 
            * contains other char types too: */
            case '\r': /* carriage return */
            case '\t': /* tab */
            case ' ':  /* space */
                if (e_flag == 1) /* Not the only character type on the line? */ 
                    c_buff[i++] = (char)ch;  /* A: yes. OK, add to buffer. */
            break;

            default:  /* all other chars: */
                e_flag = 1; /* Line contains chars that are Not just a 
                            * newline, tab, space, etc...
                            * Flag this line as a 'go' for OUTPUT */
                c_buff[i++] = (char)ch;  /* ALWAYS add default chars, */
            break;
        }
        if (i > BUFF_SIZE-2)
        {
            printf("Need a larger buffer for this file!!\n");
            printf("Buffer has %d chars.\n", i);
            return EXIT_FAILURE;
        }
    }
    fclose(fp_in);
    fclose(fp_out);
    remove("LINES.txt");
    /* Should check to ensure remove() is successful before the rename().
    * Left as an exercise for OP...*/
    rename("TEMP.txt", "LINES.txt");
    printf("\nFile editing complete\n");

    return EXIT_SUCCESS;
}
Ответ принят как подходящий

Если вы хотите буферизовать каждую строку ввода, используйте getline() вместо fgetc(). Я написал вам альтернативную реализацию без ограничения строки, которая смотрит на 1 или 2 символа вперед. Он определил строку empty() как строку, которая начинается с:

  • {EOF}
  • {'\n'}
  • {'\r', EOF}
  • {'\r', '\n'}

Впереди может быть деталь реализации empty(), если вы используете ungetc() или fseek(). «гарантирован только один возврат» для ungetc() (хотя в моей системе работало два). fseek(input, -2, SEEK_CUR) не работает на трубах.

Я использовал потоки stdin и stdout для удобства тестирования. Включить уже имеющуюся у вас обработку файлов — это тривиально. Просто не забудьте использовать if-защиту для первых двух вызовов fclose(), как демонстрирует ответ @gregspears.

#include <stdint.h>
#include <stdio.h>

int empty(FILE *input, int lookahead[static 2], uint8_t *n) {
    lookahead[0] = fgetc(input);
    *n = 0;
    if (lookahead[0] == EOF)
        return 1;
    *n = 1;
    if (lookahead[0] == '\n')
        return 1;
    if (lookahead[0] != '\r')
        return 0;
    lookahead[1] = fgetc(input);
    if (lookahead[1] == EOF)
        return 1; // ?
    *n = 2;
    return lookahead[1] == '\n';
}

void print(int lookahead[static 2], uint8_t n, FILE *input, FILE *output) {
    for(int i = 0; i < n; i++)
        fputc(lookahead[i], output);
    for(;;) {
        int ch = fgetc(input);
        if (ch == EOF) break;
        fputc(ch, output);
        if (ch == '\n') break;
    }
}

int main() {
    FILE *input = stdin;
    FILE *output = stdout;
    if (!input || !output) {
        printf("Cannot open file\n");
        return 1;
    }
    for(;;) {
        int lookahead[2];
        uint8_t n;
        if (empty(input, lookahead, &n)) {
            if (!n) break;
            continue;
        }
        print(lookahead, n, input, output);
    }
}

и пример запуска (echo используется для обеспечения отображения строки с «7»):

$ cat input.txt && echo && ./a.out < input.txt && echo
# \n
1
2

3
# \r\n
4
5

6
# {\r, EOF}
7
# \n
1
2
3
# \r\n
4
5
6
# {\r, EOF}
7

Другие вопросы по теме