Упражнение K & R:Мой код работает, Но кажется Вонючим;Совет по уборке?
-
03-07-2019 - |
Вопрос
Я работаю над книгой о K & R.Я читал дальше вперед, чем выполнял упражнения, в основном из-за нехватки времени.Я наверстываю упущенное и выполнил почти все упражнения из главы 1, которая является учебным пособием.
Моей проблемой было упражнение 1-18.Упражнение заключается в том, чтобы:
Напишите программу для удаления завершающих пробелов и табуляций из строки ввода, а также для удаления полностью пустых строк
Мой код (ниже) делает это и работает.Моя проблема с этим заключается в методе trim, который я реализовал.Это чувствуется ...неправильно ...каким-то образом.Например, если бы я увидел подобный код на C # в обзоре кода, я бы, наверное, сошел с ума.(C # - одна из моих специальностей.)
Кто-нибудь может предложить несколько советов по очистке этого - с той оговоркой, что указанный совет должен использовать только знания из главы 1 K & R.(Я знаю, что есть миллион способов очистить это, используя полную библиотеку C;мы просто обсуждаем главу 1 и базовый стандарт stdio.здесь h.) Кроме того, давая совет, можете ли вы объяснить, почему это поможет?(В конце концов, я пытаюсь учиться!А у кого лучше учиться, чем у здешних экспертов?)
#include <stdio.h>
#define MAXLINE 1000
int getline(char line[], int max);
void trim(char line[], char ret[]);
int main()
{
char line[MAXLINE];
char out[MAXLINE];
int length;
while ((length = getline(line, MAXLINE)) > 0)
{
trim(line, out);
printf("%s", out);
}
return 0;
}
int getline(char line[], int max)
{
int c, i;
for (i = 0; i < max - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
line[i] = c;
if (c == '\n')
{
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}
void trim(char line[], char ret[])
{
int i = 0;
while ((ret[i] = line[i]) != '\0')
++i;
if (i == 1)
{
// Special case to remove entirely blank line
ret[0] = '\0';
return;
}
for ( ; i >= 0; --i)
{
if (ret[i] == ' ' || ret[i] == '\t')
ret[i] = '\0';
else if (ret[i] != '\0' && ret[i] != '\r' && ret[i] != '\n')
break;
}
for (i = 0; i < MAXLINE; ++i)
{
if (ret[i] == '\n')
{
break;
}
else if (ret[i] == '\0')
{
ret[i] = '\n';
ret[i + 1] = '\0';
break;
}
}
}
Редактировать:Я ценю все полезные советы, которые я вижу здесь.Я хотел бы напомнить ребятам, что я все еще новичок в C и, в частности, еще не добрался до указателей.(Помните немного о главе 1 K & R - в главе 1 нет указателей.) Я "вроде" понимаю некоторые из этих решений, но они все еще немного продвинуты для того места, где я нахожусь...
И большая часть того, что я ищу, - это сам метод trim, в частности тот факт, что я перебираю 3 времена (которые кажутся такими грязными).Я чувствую, что если бы я был чуть-чуть умнее (даже без продвинутых знаний языка Си), это могло бы быть чище.
Решение
Нет причин иметь два буфера, вы можете обрезать строку ввода на месте
int trim(char line[])
{
int len = 0;
for (len = 0; line[len] != 0; ++len)
;
while (len > 0 &&
line[len-1] == ' ' && line[len-1] == '\t' && line[len-1] == '\n')
line[--len] = 0;
return len;
}
Возвращая длину строки, вы можете исключить пустые строки, проверив наличие строк ненулевой длины
if (trim(line) != 0)
printf("%s\n", line);
Редактировать:Вы можете сделать цикл while еще проще, предполагая кодировку ASCII.
while (len > 0 && line[len-1] <= ' ')
line[--len] = 0;
Другие советы
Если вы придерживаетесь главы 1, то, по-моему, это выглядит довольно неплохо.Вот что я бы порекомендовал с точки зрения проверки кода:
При проверке равенства в C всегда ставьте константу первой
if (1 == myvar)
Таким образом, вы никогда случайно не сделаете что-то подобное:
if (myvar = 1)
Вам это не сойдет с рук в C #, но на C это прекрасно компилируется и может быть настоящим дьяволом в отладке.
функция trim() слишком велика.
Что, я думаю, вам нужно, так это strlen-иш функция (продолжайте и напишите ее int stringlength(const char * s)).
Затем вам нужна функция с именем int scanback(const char * s, const char *matches, int start), которая начинается с start, опускается до z до тех пор, пока сканируемый символ по идентификатору s, содержащемуся в matches, возвращает последний индекс, в котором найдено совпадение.
Затем вам нужна функция с именем int scanfront (const char * s, const char * matches), которая начинается с 0 и выполняет сканирование вперед до тех пор, пока символ, сканируемый в s, содержится в matches, возвращая последний индекс, в котором найдено совпадение.
Затем вам нужна функция с именем int charinstring(char c, const char * s), которая возвращает ненулевое значение, если c содержится в s, 0 в противном случае.
Вы должны быть в состоянии написать trim в терминах этого.
Лично для while конструирует:
Я предпочитаю следующее:
while( (ret[i] = line[i]) )
i++;
Для:
while ((ret[i] = line[i]) != '\0')
++i;
Они оба проверяют на != 0, но первый выглядит немного чище.Если значение char отличается от 0, то тело цикла будет выполнено, иначе оно выйдет из цикла.
Также для операторов 'for', будучи синтаксически корректными, я нахожу, что следующее:
for ( ; i >= 0; --i)
просто выглядит "странно" для меня и действительно является потенциальным кошмарным решением для потенциальных ошибок.Если бы я просматривал этот код, это было бы похоже на светящееся красным предупреждение типа.Обычно вы хотите использовать циклы for для повторения известное количество раз, в противном случае используйте цикл while.(как всегда, из этого правила есть исключения, но я обнаружил, что в целом это справедливо).Приведенное выше утверждение for могло бы стать:
while (i)
{
if (ret[i] == ' ' || ret[i] == '\t')
{
ret[i--] = '\0';
}
else if (ret[i] != '\0' && ret[i] != '\r' && ret[i] != '\n')
{
break;
}
}
Прежде всего:
int main(пустота)
Вы знаете параметры для main().Они ничто.(Или argc & argv, но я не думаю, что это материал для главы 1.)
Что касается стиля, возможно, вы захотите попробовать скобки в стиле K & R.Им гораздо проще работать в вертикальном пространстве:
void trim(char line[], char ret[])
{
int i = 0;
while ((ret[i] = line[i]) != '\0')
++i;
if (i == 1) { // Special case to remove entirely blank line
ret[0] = '\0';
return;
}
for (; i>=0; --i) { //continue backwards from the end of the line
if ((ret[i] == ' ') || (ret[i] == '\t')) //remove trailing whitespace
ret[i] = '\0';
else if ((ret[i] != '\0') && (ret[i] != '\r') && (ret[i] != '\n')) //...until we hit a word character
break;
}
for (i=0; i<MAXLINE-1; ++i) { //-1 because we might need to add a character to the line
if (ret[i] == '\n') //break on newline
break;
if (ret[i] == '\0') { //line doesn't have a \n -- add it
ret[i] = '\n';
ret[i+1] = '\0';
break;
}
}
}
(Также добавлены комментарии и исправлена одна ошибка.)
Большой проблемой является использование константы MAXLINE - main() использует ее исключительно для линия и вон переменные;trim(), которая работает только с ними, не нуждается в использовании константы.Вы должны передать размер (ы) в качестве параметра точно так же, как вы это сделали в getline().
Лично я бы разместил код следующим образом:
ret[i] != '\0' && ret[i] != '\r' && ret[i] != '\n'
в отдельную функцию (или даже в определяющий макрос)
- trim действительно должен использовать только 1 буфер (как говорит @Ferruccio).
- отделку нужно разбить, как говорит @plinth
- trim не должен возвращать никакого значения (если вы хотите проверить наличие пустой строки, проверьте строку [0] == 0)
- для придания дополнительного вкуса C используйте указатели, а не индексы
-перейти к концу строки (заканчивается 0;-если в начале строки нет пробела и текущим символом является пробел, замените его на 0.-отступи от одного символа
char *findEndOfString(char *string) {
while (*string) ++string;
return string; // string is now pointing to the terminating 0
}
void trim(char *line) {
char *end = findEndOfString(line);
// note that we start at the first real character, not at terminating 0
for (end = end-1; end >= line; end--) {
if (isWhitespace(*end)) *end = 0;
else return;
}
}
Еще один пример того, как делать то же самое.Допустил какое-то незначительное нарушение, используя материал, специфичный для C99.этого не будет найдено в K & R.также использовалась функция assert(), которая является частью библиотеки starndard, но, вероятно, не описана в первой главе K & R.
#include <stdbool.h> /* needed when using bool, false and true. C99 specific. */
#include <assert.h> /* needed for calling assert() */
typedef enum {
TAB = '\t',
BLANK = ' '
} WhiteSpace_e;
typedef enum {
ENDOFLINE = '\n',
ENDOFSTRING = '\0'
} EndofLine_e;
bool isWhiteSpace(
char character
) {
if ( (BLANK == character) || (TAB == character ) ) {
return true;
} else {
return false;
}
}
bool isEndOfLine(
char character
) {
if ( (ENDOFLINE == character) || (ENDOFSTRING == character ) ) {
return true;
} else {
return false;
}
}
/* remove blanks and tabs (i.e. whitespace) from line-string */
void removeWhiteSpace(
char string[]
) {
int i;
int indexOutput;
/* copy all non-whitespace character in sequential order from the first to the last.
whitespace characters are not copied */
i = 0;
indexOutput = 0;
while ( false == isEndOfLine( string[i] ) ) {
if ( false == isWhiteSpace( string[i] ) ) {
assert ( indexOutput <= i );
string[ indexOutput ] = string[ i ];
indexOutput++;
}
i++; /* proceed to next character in the input string */
}
assert( isEndOfLine( string[ i ] ) );
string[ indexOutput ] = ENDOFSTRING;
}
Вот мой пример выполнения упражнения, не зная, что содержится в главе 1 или K & R.Я полагаю, указатели?
#include "stdio.h"
size_t StrLen(const char* s)
{
// this will crash if you pass NULL
size_t l = 0;
const char* p = s;
while(*p)
{
l++;
++p;
}
return l;
}
const char* Trim(char* s)
{
size_t l = StrLen(s);
if(l < 1)
return 0;
char* end = s + l -1;
while(s < end && (*end == ' ' || *end == '\t'))
{
*end = 0;
--end;
}
return s;
}
int Getline(char* out, size_t max)
{
size_t l = 0;
char c;
while(c = getchar())
{
++l;
if(c == EOF) return 0;
if(c == '\n') break;
if(l < max-1)
{
out[l-1] = c;
out[l] = 0;
}
}
return l;
}
#define MAXLINE 1024
int main (int argc, char * const argv[])
{
char line[MAXLINE];
while (Getline(line, MAXLINE) > 0)
{
const char* trimmed = Trim(line);
if(trimmed)
printf("|%s|\n", trimmed);
line[0] = 0;
}
return 0;
}