这个C语言for循环如何打印文本艺术金字塔?

25

这是我第一次在这里发布,希望我做得对。

基本上我需要帮助来尝试理解我用C语言编写的代码。程序的目的是要求用户输入0到23之间的一个数字。然后,根据用户输入的数字,将打印出一个半金字塔(就像旧款马里奥游戏中的那些金字塔)。我是一个初学者,在编程方面是侥幸得到了我的代码的答案,但现在我真的无法分辨出我的for循环是如何提供金字塔图形的。

#include <stdio.h>

int main ( void )
{
    int user_i;
    printf ( "Hello there and welcome to the pyramid creator program\n" );
    printf ( "Please enter a non negative INTEGER from 0 to 23\n" );
    scanf ( "%d", &user_i );

    while ( user_i < 0 || user_i > 23 )
    {
        scanf ( "%d", &user_i );
    }

    for ( int tall = 0; tall < user_i; tall++ )
    {
        // this are the two for loops that happened by pure magic, I am still
        // trying to figure out why are they working they way they are
        for ( int space = 0; space <= user_i - tall; space++ )
        {
            printf ( " " );
        }
        for ( int hash = 0; hash <= tall; hash++ )
        {
            printf ( "#" );
        }
        // We need to specify the printf("\n"); statement here
        printf ( "\n" );
    }

    return 0;
}

作为一名编程新手,我按照自己所知道的伪代码进行编程,但是我似乎无法理解for循环部分的工作原理。我完全理解while循环(虽然欢迎更正和最佳实践),但是for循环的逻辑仍然让我困惑,并且我希望在继续之前完全理解它。任何帮助都将不胜感激。


7
你尝试过使用调试器逐步执行代码以查看其行为吗? - buc
4
对于刚开始学习编程的人来说,正确缩进代码可以使代码更易于阅读和调试。以下是一个有用的链接,介绍了一些好的实践方法:http://net.tutsplus.com/tutorials/html-css-techniques/top-15-best-practices-for-writing-super-readable-code/。我想分享这个提示,因为我不确定你发布问题时是否已经进行了代码缩进。 - Anil
8
欢迎 - 至少这位新手不是在请求别人为他做作业,而是实际写了一些代码。这里有几个提示:你需要检查 scanf 的返回值;你不需要 void,也就是说,int main() 就可以了;还要修复缩进,这样代码更容易阅读。顺便说一句,我给你点赞了。 - Ed Heal
2
@user2485710:感谢您为这个问题格式化代码,但是您在C代码中使用的空格比我多得多。作为一个通常的经验法则,在回答新手问题时,我认为在代码示例中“入乡随俗”通常是一个好主意——对于C语言来说,可能意味着K&R风格。(当然,您可以持不同意见。) - Daniel Pryden
3
抱歉,我不理解:你写了代码,但不知道它是如何工作的? - bolov
显示剩余13条评论
10个回答

58

我将解释我如何理解这段代码,以至于我可以自如地使用它。假设我没有阅读您的描述,所以我从头开始。这个过程分为几个阶段,我会在进行时进行编号。我的目标是提供一些通用技巧,使程序更易于阅读。

第一阶段:理解粗略结构

第一步将是了解程序的总体功能,而不陷入细节中。让我们开始阅读主函数的主体部分。

int main(void) {
    int user_i;
    printf("Hello there and welcome to the pyramid creator program\n");
    printf("Please enter a non negative INTEGER from 0 to 23\n");
    scanf("%d", &user_i);

到目前为止,我们已经声明了一个整数,并告诉用户输入一个数字,然后使用scanf函数将整数设置为用户输入的内容。不幸的是,从提示或代码中无法清楚地知道这个整数的目的是什么。让我们继续阅读。

    while (user_i < 0 || user_i > 23) {
        scanf("%d", &user_i);
    }

这里可能要求用户输入其他整数。根据提示,很可能这个语句的目的是确保我们的整数在适当的范围内,并且可以通过检查代码来轻松验证。让我们看看下一行。

     for (int tall = 0; tall < user_i; tall++) {

这是外部循环。我们神秘的整数user_i再次出现,并且我们有另一个整数tall,它在0user_i之间变化。让我们看看更多代码。

        for (int space = 0; space <= user_i - tall; space++) {
            printf(" ");
        }

这是第一个内部for循环。我们不必深入了解这个新整数space发生了什么,或者为什么会出现user_i - tall,但是我们只需要注意,for循环的主体只打印一个空格。因此,这个for循环只是打印了一堆空格。让我们看看下一个内部for循环。

        for (int hash = 0; hash <= tall; hash++) {
            printf("#");
        }

这个看起来很相似。它只是打印一堆哈希标记。接下来我们有

        printf("\n");

这个会打印一个新的行。接下来是

    }

    return 0;
}

这意味着外部的for循环结束了,当外部的for循环结束后,程序就结束了。

请注意,我们已经找到了代码的两个主要部分。第一部分是获取user_i的值,第二部分是外部的for循环,必须使用该值来绘制金字塔。接下来让我们试着弄清楚user_i的含义。

Stage 2: 发现user_i的含义

由于每次外部循环迭代时都会打印一个新行,并且神秘的user_i控制着外部循环的迭代次数,因此控制着打印多少新行,似乎user_i控制着创建的金字塔的高度。为了得到确切的关系,让我们假设user_i的值为3,那么tall将取值0、1和2,所以循环会执行三次,金字塔的高度将为三。还要注意,如果user_i增加了一,那么循环将再次执行,金字塔的高度将增加一。这意味着user_i必须是金字塔的高度。在我们忘记之前,让我们将变量重命名为pyramidHeight。现在我们的主要函数看起来是这样的:

int main(void) {
    int pyramidHeight;
    printf("Hello there and welcome to the pyramid creator program\n");
    printf("Please enter a non negative INTEGER from 0 to 23\n");
    scanf("%d", &pyramidHeight);
    while (pyramidHeight < 0 || pyramidHeight > 23) {
        scanf("%d", &pyramidHeight);
    }

    for (int tall = 0; tall < pyramidHeight; tall++) {
        for (int space = 0; space <= pyramidHeight - tall; space++) {
            printf(" ");
        }
        for (int hash = 0; hash <= tall; hash++) {
            printf("#");
        }
        printf("\n");
    }

    return 0;
}

第三阶段:制作一个获取金字塔高度的函数

我们已经理解了代码的前半部分,现在可以将其移入一个函数中并且不再考虑它。这将使得代码更易于查看。由于这部分代码负责获取有效的高度,让我们将该函数命名为getValidHeight。在这样做之后,注意到金字塔的高度在main方法中不会改变,因此我们可以将其声明为const int。我们的代码现在如下所示:

#include <stdio.h>

const int getValidHeight() {
    int pyramidHeight;
    printf("Hello there and welcome to the pyramid creator program\n");
    printf("Please enter a non negative INTEGER from 0 to 23\n");
    scanf("%d", &pyramidHeight);
    while (pyramidHeight < 0 || pyramidHeight > 23) {
        scanf("%d", &pyramidHeight);
    }
    return pyramidHeight;
}

int main(void) {
    const int pyramidHeight = getValidHeight();
    for (int tall = 0; tall < pyramidHeight; tall++) {
        for (int space = 0; space <= pyramidHeight - tall; space++) {
            printf(" ");
        }
        for (int hash = 0; hash <= tall; hash++) {
            printf("#");
        }
        printf("\n");
    }

    return 0;
}

第四阶段:理解内部for循环

我们知道内部for循环重复打印一个字符,但是重复多少次呢?让我们考虑第一个内部for循环。有多少个空格被打印出来了?你可能会认为通过外部for循环的类比,有 pyramidHeight - tall 个空格,但是在这里我们有 space <= pyramidHeight - tall ,真正类比的情况应该是 space < pyramidHeight - tall。由于我们使用的是 <= 而不是 <,所以我们会得到一个额外的迭代,其中 space 等于 pyramidHeight - tall。因此,实际上会打印出 pyramidHeight - tall + 1 个空格。同样地,会打印出 tall + 1 个井号。

第五阶段:将多个字符的打印移入它们自己的函数中

由于多次打印一个字符很容易理解,我们可以将这些代码移入它们自己的函数中。让我们看看现在我们的代码是什么样子。

#include <stdio.h>

const int getValidHeight() {
    int pyramidHeight;
    printf("Hello there and welcome to the pyramid creator program\n");
    printf("Please enter a non negative INTEGER from 0 to 23\n");
    scanf("%d", &pyramidHeight);
    while (pyramidHeight < 0 || pyramidHeight > 23) {
        scanf("%d", &pyramidHeight);
    }
    return pyramidHeight;
}

void printSpaces(const int numSpaces) {
    for (int i = 0; i < numSpaces; i++) {
        printf(" ");
    }
}

void printHashes(const int numHashes) {
    for (int i = 0; i < numHashes; i++) {
        printf("#");
    }
}

int main(void) {
    const int pyramidHeight = getValidHeight();
    for (int tall = 0; tall < pyramidHeight; tall++) {
        printSpaces(pyramidHeight - tall + 1);
        printHashes(tall + 1);
        printf("\n");
    }

    return 0;
}

当我查看main函数时,我不需要担心printSpaces如何实际打印空格的细节。我已经忘记它是否使用for循环或while循环了。这使我的大脑可以去想其他事情。

第6阶段:引入变量并为变量选择良好的名称

我们的main函数现在很容易阅读。我们准备开始思考它实际上做了什么。for循环的每次迭代都会打印一定数量的空格,然后是一定数量的井号,最后是一个换行符。由于空格首先被打印,它们都在左边,这正是我们想要得到的图像。

由于新行在终端上打印在旧行下面,因此tall的值为零对应于金字塔的顶部一行。

考虑到这些因素,让我们引入两个新变量:numSpacesnumHashes用于迭代中将要打印的空格和井号的数量。由于这些变量的值在单个迭代中不会更改,因此我们可以将它们作为常量。同时,将tall的名称(这是一个形容词,因此不适合用作整数的变量名)更改为distanceFromTop。我们新的主方法看起来像这样:

int main(void) {
    const int pyramidHeight = getValidHeight();

    for (int distanceFromTop = 0; distanceFromTop < pyramidHeight; distanceFromTop++) {
        const int numSpaces = pyramidHeight - distanceFromTop + 1;
        const int numHashes = distanceFromTop + 1;
        printSpaces(numSpaces);
        printHashes(numHashes);
        printf("\n");
    }

    return 0;
}

第7阶段:为什么numSpacesnumHashes是它们所代表的意思?

现在一切都开始变得清晰起来了。唯一剩下要解决的问题就是找出给出numSpacesnumHashes的公式。

让我们从numHashes开始,因为它更容易理解。当距离顶部的距离为零时,我们希望numHashes为1,并且每当距离顶部的距离增加时,希望numHashes增加1,因此正确的公式是numHashes = distanceFromTop + 1

现在考虑numSpaces。我们知道每当距离顶部增加时,一个空格会变成一个井号,因此少了一个空格。因此,numSpaces的表达式应该有一个-distanceFromTop。但是顶行应该有多少个空格呢?由于顶行已经有一个井号,需要制作pyramidHeight-1个井号,因此必须至少有pyramidHeight-1个空格,以便将其转换为井号。在代码中,我们选择了pyramidHeight+1个空格作为顶行,比pyramidHeight-1多两个空格,因此使整个图像向右移动了两个空格。

结论

你只是询问两个内部循环如何工作,但我给了一个非常长的答案。这是因为我认为真正的问题不是你不理解for循环的工作原理,而是你的代码难以阅读,因此很难确定任何内容的功能。因此,我展示了我会如何编写程序,希望您认为它更易于阅读,因此您能够更清晰地看到发生了什么,并希望您能学会编写更清晰的代码。

我如何更改代码?我更改了变量的名称,以便清楚地了解每个变量的作用;我引入了新变量,并尝试为它们取好名字;并且将一些涉及输入和输出以及打印字符特定次数的逻辑的低级代码移动到它们自己的方法中。这种最后一种更改极大地减少了main函数中的行数,消除了main函数中的for循环嵌套,并使程序的关键逻辑易于查看。


6
感谢你为解释所做的努力。 - Grijesh Chauhan
1
非常清晰易懂的回答。有时候等待并获得像您这样出色的答案是更好的选择。为此点赞 [+1]。 - Gabriel L.
1
总体来说很好。请注意,您尚未测试scanf()的响应,因此如果用户在回答输入高度问题时输入q,则程序(即使是在修改后的形式下)可能会运行很长时间,并且在此过程中非常无聊。当然,这取决于函数运行时pyramidHeight中发生的准随机值。代码还没有解释出了什么问题,或者如果用户键入24或-1等,则不再提示输入。 - Jonathan Leffler
1
这里给出的很多答案都非常好,但是你的答案让我能够遵循一种比我原来的想法更实用的思考过程。我从中学到了很多东西,非常感谢你花费的解释功夫,现在我明白了。 - Alex_adl04
1
新回答者的出色回答加1分。欢迎来到StackOverflow! - Billy ONeal
上帝保佑您,先生,在这里如此有效地进行良好的战斗!您有教学天赋。这个美丽的例子一次性清晰地教授了许多优秀的习惯,我想把它装裱起来挂在我的墙上。 - Patrick Fisher

12

首先,让我们把循环的主体部分搞定。第一个循环只打印空格,第二个循环打印井号。

我们希望打印出这样一行,其中_代表一个空格:

______######

魔术问题是,我们需要打印多少个空格和#号?

在每一行中,我们想要比前一行多打印一个#号,并且比前一行少打印一个空格。这就是外层循环中变量"tall"的作用。你可以将其看作为"这一行应该打印的#号数量"。

剩余需要打印在该行上的所有字符都应该是空格。因此,我们可以取用户输入的总行长度,减去该行上#号的数量,就得到了我们需要的空格数。这就是第一个for循环中的条件:

for ( int space = 0; space <= user_i - tall; space++ )
//                            ~~~~~~~~~~~~~ 
// Number of spaces == number_user_entered - current_line

然后我们需要打印出井号的数量,这个数量总是等于当前行数:

for ( int hash = 0; hash <= tall; hash++ )
//                          ~~~~
// Remember, "tall" is the current line

整个代码块都在一个for循环中,每行只重复一次。

重命名一些变量并引入一些新名称可以使整个代码更易于理解:

#include <stdio.h>

int main ( void )
{
    int userProvidedNumber;
    printf ( "Hello there and welcome to the pyramid creator program\n" );
    printf ( "Please enter a non negative INTEGER from 0 to 23\n" );
    scanf ( "%d", &userProvidedNumber );

    while ( userProvidedNumber < 0 || userProvidedNumber > 23 )
    {
        scanf ( "%d", &userProvidedNumber );
    }

    for ( int currentLine = 0; currentLine < userProvidedNumber; currentLine++ )
    {
        int numberOfSpacesToPrint = userProvidedNumber - currentLine;
        int numberOfHashesToPrint = currentLine;

        for ( int space = 0; space <= numberOfSpacesToPrint; space++ )
        {
            printf ( " " );
        }
        for ( int hash = 0; hash <= numberOfHashesToPrint; hash++ )
        {
            printf ( "#" );
        }

        // We need to specify the printf("\n"); statement here
        printf ( "\n" );
    }

    return 0;
}

我之前没有想到可以在for循环的testExpression中评估变量之前,将它们定义在for循环外部。我将开始使用这种技术,以使我的代码更易于理解。非常感谢您的解释和有用的技巧。 - Alex_adl04

8
一些事情:
考虑“台阶检查”这个概念。也就是说,自己跟踪循环并画出空格和哈希标记。可以考虑使用方格纸。我做了15年这个行业,当它们变得困难时,我仍然会时不时地在纸上追踪。
这里的魔法在于“user_i-tall”和“hash<=tall”的值。这些是括号中两个内部for循环的条件。请注意它们正在做什么:
由于tall从最外层循环开始“向上”,通过从user_i中减去它,打印空格的循环正在“向下”。也就是说,随着时间的推移打印越来越少的空格。
由于tall正在“上升”,并且因为哈希循环基本上是直接使用它,所以它也在“上升”。也就是说,随着时间的推移打印更多的哈希标记。
因此,实际上忽略大部分代码。它是通用的:外部循环只是计数,大多数内部循环只是进行基本初始化(即space=0和hash=0)或基本增量(space++和hash++)。
只有内部循环的中心部分是重要的,它们使用从最外层循环开始的tall的移动来使自己分别向下和向上增加,如上所述,从而形成制作半金字塔所需的空格和哈希标记的组合。
希望这可以帮助你!

7

这是您的代码,已经重新格式化并去除了注释:

for(int tall = 0;tall<user_i;tall++)
{
    for(int space=0; space<=user_i-tall; space++){ 
        printf(" "); 
    }
    for(int hash=0; hash<=tall; hash++){ 
        printf("#");
    }
    printf("\n");   
}

同样的代码,加入了一些解释:

// This is a simple loop, it will loop user_i times.
//   tall will be [0,1,...,(user_i - 1)] inclusive.
//   If we peek at the end of the loop, we see that we're printing a newline character.
//   In fact, each iteration of this loop will be a single line.
for(int tall=0; tall<user_i; tall++)
{
    // "For each line, we do the following:"
    //
    // This will loop (user_i - tall + 1) times, each time printing a single space.
    //   As we've seen, tall starts at 0 and increases by 1 per line.
    //   On the first line, tall = 0 and this will loop (user_i + 1) times, printing that many spaces
    //   On the last line, tall = (user_i - 1) and this will loop 0 times, not printing any spaces
    for(int space=0; space<=user_i-tall; space++){ 
        printf(" "); 
    }

    // This will loop (tall + 1)  times, each printing a single hash
    //   On the first line, tall = 0 and this will loop 1 time, printing 1 hash
    //   On the last line, tall = (user_i - 1) and this will loop user_i times, printing that many hashes
    for(int hash=0; hash<=tall; hash++){ 
        printf("#");
    }

    // Finally, we print a newline
    printf("\n");   
}

1
嗯... 这是 C 语言,# 是什么意思? - Daniel Pryden
这段代码不是用来复制、粘贴和编译的……如果它真的让你困惑,我会改变它。 - jedwards

4

这种类型的小程序在我早期很吸引我。我认为它们在构建逻辑和理解哪个循环在何时、何地、以及在哪种情况下是最完美的,起到了至关重要的作用。
了解发生了什么的最好方式是手动调试每个语句。
但为了更好地理解,让我们了解如何构建逻辑,我做了一些小改变,以便更好地理解。

  • n是要在金字塔中打印的行数n =5
  • 用连字符'-'(破折号符号)替换空格' '

现在金字塔看起来像这样:

                            ----#
                            ---##
                            --###
                            -####
                            #####

现在,设计循环的步骤如下:
  • 首先打印n行,即5行,因此第一个循环将运行5次。
    for (int rowNo = 0; rowNo < n; rowNo++ ) 其中行号rowNo类似于您循环中的tall
  • 在每一行中,我们必须打印5个字符,但要使我们得到所需的图形,如果仔细观察一下逻辑,
    rowNo=0(即我们的第一行),我们有4条破折号和1 个井号
    rowNo=1 我们有3条破折号和2 个井号
    rowNo=2 我们有2条破折号和3 个井号
    rowNo=3 我们有1条破折号和4 个井号
    rowNo=4 我们有0条破折号和5 个井号
  • 稍加检查后可以发现,对于每一行表示为rowNo,我们必须打印n - rowNo - 1条破折号和rowNo + 1个井号
    因此,在第一个for循环中,我们必须有两个循环,一个用于打印破折号,一个用于打印井号。
    破折号循环将是for (int dashes= 0; dashes < n - rowNo - 1; dashes ++ ) 这里的dashes类似于您原始程序中的space
    井号循环将是 for (int hash = 0; hash < rowNo + 1; dashes ++ )
  • 每一行之后,我们必须打印一个换行符,以便我们可以移动到下一行。
希望上述解释清楚了如何构建您编写的for循环。
在您的程序中,与我的解释相比,只有一些微小的更改。在我的循环中,我使用了小于<运算符,而您使用了<=运算符,所以会多运行一次。

3
的确有帮助,我之前没有很清楚地想象出空格的正确打印方式和for循环的构造方式。 - Alex_adl04

3
您应该使用缩进来使您的代码更易于阅读,因此更容易理解。
您的代码会打印user_i行,每行由user_i-tall+1个空格和tall+1个#组成。由于迭代索引tall随每次遍历而增加,这意味着会打印一个多余的#。它们是右对齐的,因为还省略了一个空格。因此,您将获得形成金字塔的“成长”#行。
您所做的与以下伪代码相当:
for every i between 0 and user_i do:
    print " " (user_i-i+1) times
    print "#" (i+1) times

2
#include <stdio.h>

// Please note spacing of
// - functions braces
// - for loops braces
// - equations
// - indentation
int main(void)
{
    // Char holds all the values we want
    // Also, declaire all your variables at the top
    unsigned char user_i;
    unsigned char tall, space, hash;

    // One call to printf is more efficient
    printf("Hello there and welcome to the pyramid creator program\n"
           "Please enter a non negative INTEGER from 0 to 23\n");

    // This is suited for a do-while. Exercise to the reader for adding in a
    // print when user input is invalid.
    do scanf("%d", &user_i);
    while (user_i < 0 || user_i > 23);

    // For each level of the pyramid (starting from the top)...
    // Goes from 0 to user_i - 1
    for (tall = 0; tall < user_i; tall++) {

        // We are going to make each line user_i + 2 characters wide

        // At tall = 0,          this will be user_i + 1                characters worth of spaces
        // At tall = 1,          this will be user_i + 1            - 1 characters worth of spaces
        // ...
        // At tall = user_i - 1, this will be user_i + 1 - (user_i - 1) characters worth of spaces
        for (space = 0; space <= user_i - tall; space++)
            printf(" "); // no '\n', so characters print right next to one another

        //                 because of using '<=' inequality
        //                                                \_  
        // At tall = 0,          this will be          0 + 1 characters worth of hashes
        // At tall = 1,          this will be          1 + 1 characters worth of hashes
        // ...
        // At tall = user_i - 1, this will be user_i - 1 + 1 characters worth of spaces
        for (hash = 0; hash <= tall; hash++)
            printf("#");

        // Level complete. Add a newline to start the next level
        printf("\n");   
    }

    return 0;
}

1
将char等数据类型推广而不是int,对于那些不理解for循环如何工作(或为什么要这样做)的人来说,可能会有点困惑。(尽管它们适合这项工作) - Phil
“Spaghetti to the wall” 方法。尽可能灌输多种良好的实践,并希望其中一些能够被采纳。 - user1902824

2
最简单的答案是因为一个循环打印了像这样用 - 表示的空格。
----------
---------
--------
-------
------
-----

等等。

哈希值会像这样打印:

------#
-----##
----###
---####
--#####
-######

由于在第二阶段哈希打印循环需要打印相等数量的哈希以完成金字塔,因此可以通过两种方法解决。一种是将哈希循环复制并粘贴两次,另一种是通过以下修改使循环下一次运行两次。

for ( int hash = 0; hash <= tall*2; hash++ )
        {
            printf ( "#" );
        }

构建这样的循环的逻辑很简单,最外层循环打印一个换行符来分隔循环,内部循环负责每行的内容。 空格循环放置空格,哈希循环在空格末尾添加哈希。 [我的答案可能会有冗余,因为我没有仔细阅读其他答案,它们太长了]
       #
      ###
     #####
    #######
   #########
  ###########

2
分析循环: 第一个循环
for ( int tall = 0; tall < user_i; tall++ ){...}

正在控制该行。第二个循环

for ( int space = 0; space <= user_i - tall; space++ ){...}  

将空格填充到列中。
对于每行,它将使用空格填充所有的user_i - tall列。
现在使用循环将剩余列填充为#

for ( int hash = 0; hash <= tall; hash++ ){...}  

1
for ( int tall = 0; tall < user_i; tall++ ) { ... }

我会将其自然语言化为以下形式:
  1. int tall = 0; // 开始时,tall 的初始值为 0
  2. tall < user_i; // 只要 tall 小于 user_i,在花括号之间的操作就会被执行
  3. tall++; // 每次循环结束后,先将 tall 加 1,再重新检查是否符合第二步中的条件,继续下一次循环
并且在您的代码中添加了良好的缩进,现在更容易看出 for 循环是如何嵌套的了。内部循环将在外部循环的每次迭代中运行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接