8

我正在写 K&R 的书。我比我做的练习读得更远,主要是因为没有时间。我正在赶上进度,并且已经完成了第 1 章(即教程)中的几乎所有练习。

我的问题是练习 1-18。练习是为了:

编写一个程序从输入行中删除尾随空格和制表符,并删除完全空白行

我的代码(如下)可以做到这一点,并且可以工作。我的问题是我实现的修剪方法。感觉……错了……不知何故。就像我在代码审查中看到 C# 中的类似代码一样,我可能会发疯。(C# 是我的专长之一。)

任何人都可以提供一些关于清理这个问题的建议 - 说建议只能使用 K&R 第 1 章中的知识。(我知道有无数种方法可以使用完整的 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 的 n00b,特别是还没有得到指针。(记住关于 K&R 的 Ch.1 的一点——Ch.1 不做指针。)我“有点”得到了一些解决方案,但对于我所处的位置,它们仍然有点先进......

我正在寻找的大部分内容是修剪方法本身——特别是我循环了3次的事实(感觉很脏)。我觉得如果我稍微聪明一点(即使没有 C 的高级知识),这本可以更干净。

4

9 回答 9

9

如果您坚持使用第 1 章,那对我来说看起来不错。从代码审查的角度来看,这是我的建议:

在 C 中检查相等性时,始终将常量放在首位

if (1 == myvar)

这样你就不会意外地做这样的事情:

if (myvar = 1)

在 C# 中你无法摆脱它,但它在 C 中编译得很好,并且可以成为真正的调试恶魔。

于 2008-10-02T11:56:01.550 回答
5

没有理由有两个缓冲区,您可以将输入行修剪到位

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);

编辑:假设 ASCII 编码,您可以使 while 循环更加简单。

while (len > 0 && line[len-1] <= ' ')
    line[--len] = 0;
于 2008-10-02T12:06:31.930 回答
1

个人对于 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;
        }
}
于 2008-10-02T12:19:00.340 回答
1

trim() 太大了。

我认为您需要的是一个类似 strlen 的函数(继续写 int stringlength(const char *s))。

然后你需要一个名为 int scanback(const char *s, const char *matches, int start) 的函数,它从 start 开始,只要在匹配中包含的 s id 处扫描的字符就会下降到 z,返回最后一个索引,其中找到匹配项。

然后你需要一个名为 int scanfront(const char *s, const char *matches) 的函数,它从 0 开始,只要在 s 处扫描的字符包含在匹配项中,就会向前扫描,返回找到匹配项的最后一个索引。

然后你需要一个名为 int charinstring(char c, const char *s) 的函数,如果 c 包含在 s 中,则返回非零,否则返回 0。

您应该能够根据这些来编写修剪。

于 2008-10-02T12:39:36.483 回答
0

首先:

诠释主要(无效)

您知道 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() 专门将它用于lineout变量;trim(),它只对它们起作用,不需要使用常量。您应该像在 getline() 中那样将大小作为参数传递。

于 2008-10-02T12:26:23.380 回答
0

这是我在不知道第 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;
}
于 2008-10-02T13:21:41.220 回答
0

我个人会这样写代码:

ret[i] != '\0' && ret[i] != '\r' && ret[i] != '\n'

进入一个单独的函数(甚至是一个定义宏)

于 2008-10-02T13:29:30.123 回答
0
  1. trim 确实应该只使用 1 个缓冲区(正如@Ferruccio 所说)。
  2. 正如@plinth 所说,修剪需要分解
  3. trim 不需要返回任何值(如果你想检查空字符串,测试 line[0] == 0)
  4. 对于额外的 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;
  }
}
于 2008-10-02T13:42:40.623 回答
0

做同样事情的另一个例子。通过使用 C99 特定的东西做了一些轻微的违规行为。在 K&R 中找不到。还使用了作为starndard 库一部分的assert() 函数,但可能未在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;

}
于 2008-10-02T19:03:06.343 回答