Удалите все комментарии из программы на C

Это мое решение для упражнения 1-23 из книги «Язык программирования Си».

Упражнение 1-23. Напишите программу, удаляющую все комментарии из программы на языке C.

Ключевая идея:

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

Я постарался сделать код читабельным. Я хотел бы получить ваши отзывы по всем аспектам решения. Будьте резкими, но конструктивными.

#include <stdio.h>
#include <stdbool.h>

#define DOUBLE_QUOTE_CHAR '"'
#define BASK_SLASH '\'
#define NEW_LINE_CHAR 'n'
#define FORWARD_SLASH "https://codereview.stackexchange.com/"
#define ASTERISK '*'
#define SINGLE_QUOTE_CHAR '''

void print_quoted_string();     /* called when a quote char that indicate begining of string is encountered. print the quoated string then stop */ 
void skip_single_line_comment(); /* called when we see // that indicate start of a single-line comment . skip that comment */ 
void skip_multiple_line_comment();  

int main(void)
{
    char cur_char; 
    bool prev_forward_slash = false;    // to handle //, /*
    bool prev_single_quote = false;     // to handle '"' 
    bool prev_back_slash = false;       // to handle '"' 

    while ( (cur_char = getchar()) != EOF) {
        if (cur_char == DOUBLE_QUOTE_CHAR && !prev_single_quote && !prev_back_slash) // to exclude '"' and '"'. Are there any other casse ? 
            print_quoted_string(); 
        else if (prev_forward_slash && cur_char == FORWARD_SLASH) { // this // can not be inside a string. because the first if statement guarentee it.
            skip_single_line_comment();
            prev_forward_slash = false; 
        } 
        else if (prev_forward_slash && cur_char == ASTERISK ) {
            skip_multiple_line_comment(); 
            prev_forward_slash = false; 
        }
        else {
            if (prev_forward_slash) 
                putchar(FORWARD_SLASH);  // since we don't print a forward slash until we read the character after it. 

            if (cur_char == FORWARD_SLASH)    // we should not print this forward slash now since it may be followed by * 
                prev_forward_slash = true;    
            else if (cur_char == SINGLE_QUOTE_CHAR) {
                prev_single_quote = true; 
                putchar(cur_char);
            }
            else if (cur_char == BASK_SLASH) {
                prev_back_slash = true;
                putchar(cur_char);
            } 
            else {
                prev_single_quote = prev_forward_slash =  prev_back_slash = false;
                putchar(cur_char); 
            }
        }
    }

    return 0; 
}


void print_quoted_string()
{
    putchar(DOUBLE_QUOTE_CHAR);   /* print the start quote of the string */ 

    bool prev_char_is_escape_char = false;  /* used to handle the escape sequences issues. for example:  "\" , """, "\\"" */ 
    char cur_char;
    while (1) {
        cur_char = getchar(); 
        
        if (cur_char == DOUBLE_QUOTE_CHAR && !prev_char_is_escape_char)    /* this quote char is the end quote of the string */ 
            break; 
        
        putchar(cur_char);

        /* current char is an escape char iff it's a back slash and not proceeded by escape char */ 
        prev_char_is_escape_char = (cur_char == BASK_SLASH && !prev_char_is_escape_char); 
    }

    putchar(DOUBLE_QUOTE_CHAR);    /* print the end quote of the string */
}

void skip_single_line_comment()
{
    while (getchar() != NEW_LINE_CHAR); 
    putchar(NEW_LINE_CHAR);
}

void skip_multiple_line_comment()
{
    char prev_char="https://codereview.stackexchange.com/"; 
    char cur_char="*"; 

    while (1) {
        char cur_char = getchar(); 

        if (prev_char == '*' && cur_char == "https://codereview.stackexchange.com/") {
            putchar(NEW_LINE_CHAR); 
            return; 
        }

        prev_char = cur_char; 
    }
}

2 ответа
2

Немного проблем:

  • Если строка в кавычках не заканчивается, print_quoted_string становится бесконечным циклом. Конечно, такой ввод неверен, но ваша программа корректно завершится ошибкой.

  • Обратите внимание на обратную косую черту-новую строку:

    // This 
      is a comment.
    
    /
    / This is also a comment
    
    /
    * Even this is a comment */
    

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

    Сначала классический баг: char cur_char;(cur_char = getchar()) != EOF. Переменная должна быть int нет char или вы не можете сравнить это с EOF. Да, это действительно глупо getchar получает int, а не char, но так оно и есть.


    Я знаю, что это простая программа, и производительность, ремонтопригодность и т. Д. Не важны. Но если бы это была настоящая программа по качеству продукции, ее лучше было бы написать по-другому. Ради обучения давайте представим, что это:

    Тогда в целом вы могли бы проверять по справочной таблице, а не с помощью сложной серии if-else if. Их довольно трудно читать, вы получаете различное поведение при обнаружении определенных символов комментариев, разбросанных по различным вложенным if-else if. Кроме того, компилятор с меньшей вероятностью переведет if-else if для некоторого поиска в таблице, более вероятно, что это приведет к созданию группы ветвей, что очень плохо для производительности цикла.

    Таблица поиска, за которой следует централизованный код «действие в зависимости от результата», например, switch улучшит скорость выполнения и удобочитаемость / ремонтопригодность.

    Простейшей формой такого поиска в таблице было бы strchr(""\n/*'", input) затем выполните разные действия в зависимости от того, если strchr вернул NULL или нет.

    Поэтому вместо того, чтобы определять все символы комментариев с помощью макросов, вам лучше иметь typedef enum { DOUBLE_QUOTE_CHAR, BACK_SLASH, ... NO_COMMENT } comment_t; и т.д., соответствующий индексу, переданному строковому литералу, используемому strchr. Тогда вы сможете:

      const char  comment_characters[] = ""\n/*'";
      const char* comment_found = strchr(comment_characters, input)
      comment_t   comm;
    
      if(comment_found)
        comm = (comment_t) (comment_found - comment_characters); // pointer diff arithmetic
      else // strchr returned NULL
        comm = NO_COMMENT;
    
      switch(comm)
      {
         case DOUBLE_QUOTE_CHAR:  /* do double quote stuff */  break;
         case BACK_SLASH:         /* do backslash stuff */     break;
    
         default:                 /* NO_COMMENT etc, do nothing */
      }
    

    Вы даже можете поднять удобочитаемость / ремонтопригодность немного дальше, почти до крайности, сделав вместо этого следующее:

      const char comment_characters [N] = // where N is some size "large enough"
      {
        [DOUBLE_QUOTE_CHAR] = '"',
        [BACK_SLASH] = '\',
        [N-1] = '',                     // strictly speaking not necessary but being explicit is nice
      };
    

    Это гарантирует целостность между строкой и индексами перечисления.

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

      — отметка кодировщика

    • 1

      getchar() возвращение int очень не тупо. Если он вернул char, то EOF значение также должно соответствовать char, что означало бы, что он должен иметь значение некоторого действительного фактического символа. Это означало бы, что вам нужно найти другой способ выяснить, EOF возвращаемое значение было фактическим концом файла или символом. Например, с strtol(), тебе нужно проверить errno. Но поскольку он не изменяется, если есть нет ошибка, значит вам нужно установить errno = 0 перед каждый звонок, если вы хотите обнаружить ошибки. Это хуже, чем вернуться int.

      — ilkkachu

    • @ilkkachu Здесь нет смысла. В те дни, когда все это создавалось по прихоти, использовались 7-битные таблицы токенов, поэтому они могли просто выбрать любое значение с установленным MSB. Например, 0xFF. Но нет, это обязательно должно быть 0xFFFF …! Также не дай бог, если они разработали функцию, как это обычно делается в профессиональном C, отделяя данные от ошибок. char getchar (err_t* result);, что бы было логично и читабельно. Но нет, у getchar не должно быть чар! Этот API просто ужасно плохой, всегда был, без оправданий, без объяснений. Как и большинство stdio.h, вероятно, худшая из когда-либо созданных библиотек.

      — Лундин


    • @Lundin, я думал больше как int getchar (char *c); где возвращаемое значение — это код ОК / ошибки, а считанный символ проходит через указатель. Таким образом, он будет соответствовать большинству других функций. Вызывающий в любом случае должен использовать оба, поэтому не похоже, что было бы проще, если бы возвращаемое значение было char. Но да, я не слишком знаком с этой историей, так что считайте это рационализацией постфактум, если хотите. В любом случае, пока он возвращает только одно значение и все значения char являются допустимыми символами, retval должен быть больше, чем char, независимо от их размера.

      — ilkkachu

    Добавить комментарий

    Ваш адрес email не будет опубликован.