转到和代码重复 - 在这种情况下它们可以避免吗?

Goto and Code Repetition - Are they avoidable in this case?

我最近遇到了一个编程问题,在我看来,最优化的解决方法是使用 goto,尽管这不是一个好习惯。问题是:告诉用户输入一个正自然数(> 0)并读取输入。如果此数字有效,请告诉用户该数字的平方。在输入正确时执行此操作。我想出了一些解决方案,但它们似乎都有问题。这是其中两个:
解决方案 1 - 问题:使用 goto

#include <stdio.h>

int main()
{
    int num;

_LOOP:
    printf("Enter a positive natural number: ");
    scanf("%i", &num);

    if (num > 0) {
        printf("Square: %i\n", num * num);
        goto _LOOP;
    }

    printf("Invalid number\n");

    return 0;
}

解决方案 2 - 问题:仔细检查 num > 0(代码重复)

#include <stdio.h>

int main()
{
    int num;

    do {
        printf("Enter a positive natural number: ");
        scanf("%i", &num);

        if (num > 0)
            printf("Square: %i\n", num * num);
    } while (num > 0);
    
    printf("Invalid number\n");

    return 0;
}

显然,有更多的方法可以解决这个问题,但我想出的所有其他方法都没有使用 goto 来解决相同的代码重复问题。那么,是否有一种解决方案可以避免 goto 和代码重复?如果没有,我应该去哪一个?

这是答案的一半;尝试填补缺失的内容。请记住,有时最好将循环构造为“做某事直到...”而不是“做某事而...”

    for (;;) {
        printf("Enter a positive natural number: ");
        scanf("%i", &num);
        if (num <= 0)
            break;
        printf("Square: %i\n", num * num);
    }
    printf("Invalid number\n");

[更新为@rdbo 的回答]

跳出循环呢?这基本上是循环结束的 goto 语句,没有显式使用 goto.

#include <stdio.h>

int main()
{
    int num;

    while(1) {
        printf("Enter a positive natural number: ");
        scanf("%i", &num);

        if (num > 0) {
            printf("Square: %i\n", num * num);
        } else {
            printf("Invalid number\n");
            break;
        }
    }

    return 0;
}

对于初学者来说,如果您希望得到一个非负数,那么变量 num 应该是无符号整数类型,例如 unsigned int.

正如您的问题中所写,用户可以输入无效数据或中断输入。你必须处理这样的情况。

乘法 num * num 也会导致溢出。

并且使用 goto 而不是循环确实是一个坏主意。

注意在使用变量的最小范围内声明变量。

为这样的任务使用 for 循环也是一个坏主意。使用 while 循环非常有表现力。

程序可以这样看

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

int main(void) 
{
    while ( true )
    {
        printf( "Enter a positive natural number: " );
        
        unsigned int num;

        if ( scanf( "%u", &num ) != 1 || num == 0 ) break;

        printf( "Square: %llu\n", ( unsigned long long )num * num );
    }

    puts( "Invalid number" );

    return 0;
}

程序输出可能看起来像

Enter a positive natural number: 100000000
Square: 10000000000000000
Enter a positive natural number: 0
Invalid number

或者将最后一个输出语句移到while语句中会更好。例如

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

int main(void) 
{
    while ( true )
    {
        printf( "Enter a positive natural number: " );
        
        unsigned int num;

        if ( scanf( "%u", &num ) != 1 || num == 0 )
        {
            puts( "Invalid number" );
            break;
        }
        
        printf( "Square: %llu\n", ( unsigned long long )num * num );
    }

    return 0;
}

另一个选项:如果满足继续条件,则检查存储的布尔值。它比无限 loop/break 方法(对我来说)更容易阅读,并且没有代码重复。

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

int main()
{
    int num;
    bool bContinue;

    do {
        printf("Enter a positive natural number: ");
        scanf("%i", &num);

        if (num > 0){
            printf("Square: %i\n", num * num);
            bContinue = true;
        }
        else{
            printf("Invalid number\n");
            bContinue = false;
        }
    } while (bContinue);             

    return 0;
}

我有点惊讶还没有人建议功能分解。 与其编写一大堆原始语句,不如将 main 分割成更小的函数。 除了 readability/maintainability 好处外,它还有助于以非常自然的方式消除代码重复。

在 OP 的情况下,从最终用户那里获得输入是一项单独的责任,并且是一个单独功能的不错选择。

static bool user_enters_number(int *ptr_to_num)
{
    printf("Enter a positive natural number: ");
    return scanf("%i", ptr_to_num) == 1;
}

注意 user_enters_number 显式测试 scanf 的 return 值。 这改进了文件结尾处理。

同样,您可以为数字验证赋予它自己的功能。 这看起来有点矫枉过正(只是 num > 0,对吧?),但它让我们有机会将验证与生成的错误消息结合起来。 在 main 末尾打印“无效数字”感觉不对。无效数字不是唯一的退出条件;文件结尾是另一个。 因此,我会让验证函数确定消息。 作为奖励,这使得支持多种错误类型成为可能(例如,负数和零的单独消息)。

static bool is_valid_number(int num)
{
    bool ok = (num > 0);
    if (!ok) printf("Invalid number\n");
    return ok;
}

我们现在有两个布尔类型的函数,它们可以与 && 整齐地链接在一起,并放入循环的条件部分,这是一种惯用的说法:如果这些函数中的任何一个失败(即 returns false),立即退出循环。

剩下的是一个非常干净的 main 函数。

int main(void)
{
    int num;
    while (user_enters_number(&num) && is_valid_number(num))
    {
        printf("Square: %i\n", num * num);
    }
}

要了解可维护性方面的好处,请尝试重写此代码,使其接受两个数字并打印它们的乘积。

int main(void)
{
    int num1, num2;
    while (user_enters_number(&num1) && is_valid_number(num1) &&
           user_enters_number(&num2) && is_valid_number(num2))
    {
        printf("Product: %i\n", num1 * num2);
    }
}

变化微不足道,仅限于单一功能 (尽管您可能会考虑将参数 input_prompt 添加到 user_enters_number)。

这种 'divide-and-conquer' 方法没有性能损失:智能编译器会做任何必要的事情来优化代码,例如内联函数。