Week 1 C

欢迎!

  • 在上一课中,我们学习了 Scratch,一种可视化编程语言。

  • 事实上,当你学习任何编程语言时,都会用到 Scratch 中呈现的所有核心编程概念。Scratch 中的函数、条件、循环和变量,都是你在任何编程语言中都会遇到的基本构建块。

  • 回想一下,机器只能理解二进制。人类编写的是源代码,也就是人类可读的计算机指令列表;而机器只能理解我们现在所说的机器代码。机器代码由 1 和 0 的模式构成,用来产生期望的效果。

  • 事实证明,我们可以使用一种非常特殊的软件(称为“编译器”)将“源代码”转换为机器代码。今天,我们将向您介绍一个编译器,它允许您将编程语言 C 中的源代码转换为机器代码。

  • 今天,除了学习如何编程之外,你还会学习如何编写好的代码。

CS50 使用的 Visual Studio Code

  • 本课程使用的文本编辑器是 Visual Studio Code,又名 VS Code,俗称 cs50.dev,可以通过相同的 URL 访问。

  • 我们使用 VS Code 最重要的原因之一是它已经预装了课程所需的所有软件。本课程和此处的说明是根据 VS Code 设计的。

  • 在自己的计算机上手动安装课程所需的软件很繁琐,也容易出问题。本课程的作业最好始终使用 VS Code 完成。

  • 你可以通过 cs50.dev 打开 VS Code。

这个编程环境可以划分为几个区域:

请注意,左边有一个“文件资源管理器”,您可以在其中找到您的文件。另外,请注意中间有一个称为“文本编辑器”的区域,您可以在其中编辑程序。最后,还有一个 command line interface,称为 CLI命令行终端窗口,我们可以在其中向云端计算机发送命令。

  • 在终端窗口中,我们可以使用的一些常见命令行参数包括:

cd,用于更改当前目录(文件夹)

cp,用于复制文件和目录

ls,用于列出目录中的文件

mkdir,用于创建目录

mv,用于移动(重命名)文件和目录

rm,用于移除(删除)文件

rmdir,用于移除(删除)目录

  • 最常用的是 ls,它会列出当前目录中的所有文件。现在在终端窗口中输入 ls 并按下 enter。你会看到当前文件夹中的所有文件。

  • 由于此 IDE 预配置了所有必要的软件,因此您应该使用它来完成本课程的所有作业。

你好世界

我们将使用三个命令来编写、编译并运行我们的第一个程序:

code hello.c

make hello

./hello

第一个命令 code hello.c 会创建一个文件,并允许我们为这个程序输入指令。第二个命令 make hello 会根据我们用 C 写下的指令编译该文件,并创建一个名为 hello 的可执行文件。最后一个命令 ./hello 会运行名为 hello 的程序。

我们可以通过在终端窗口中输入 code hello.c 来在 C 中构建您的第一个程序。请注意,我们有意将整个文件名小写并包含 .c 扩展名。然后,在出现的文本编辑器中,编写代码如下:

// A program that says hello to the world

#include

int main(void)
{
    printf("hello, world\n");
}

请注意,上面的每个字符都有作用。如果输入错误,程序将无法运行。printf 是一个可以输出一行文本的函数。请注意引号和分号的位置。另外,\n 会在 hello, world 之后创建一个新行。

  • 回到终端窗口,你可以通过执行 make hello 来编译代码。请注意,我们省略了 .cmake 会寻找我们的 hello.c 文件,并将其转换为名为 hello 的程序。如果执行此命令没有错误,就可以继续。如果有错误,请再次检查代码,确保它与上面的示例一致。

  • 现在,输入 ./hello,你的程序就会运行并输出 hello, world

  • 现在,打开左侧的文件资源管理器。你会注意到现在既有一个名为 hello.c 的文件,也有另一个名为 hello 的文件。编译器能够读取 hello.c:你的代码就存储在那里。hello 是一个可执行文件,你可以运行它,但编译器不会把它当作源代码读取。

从 Scratch 到 C

  • 在 Scratch 中,我们利用 say 块在屏幕上显示任何文本。事实上,在 C 中,我们有一个名为 printf 的函数就是执行此操作。

请注意,我们的代码已经调用了这个函数:

printf("hello, world\n");

请注意,调用了 printf 函数。传递给 printf 的参数是 hello, world\n。代码语句以 ; 结束。

代码中出现错误很常见。请按如下方式修改你的代码:

// \n is missing

#include

int main(void)
{
    printf("hello, world");
}

请注意,\n 现在已经消失了。

  • 在终端窗口中运行 make hello。然后在终端窗口中输入 ./hello,你的程序发生了什么变化?这个 \ 字符称为转义字符,它告诉编译器 \n 是一条用于创建换行的特殊指令。

您还可以使用其他转义字符:

\n  create a new line
\r  return to the start of a line
\"  print a double quote
\'  print a single quote
\\  print a backslash

将您的程序恢复为以下内容:

// A program that says hello to the world

#include

int main(void)
{
    printf("hello, world\n");
}

请注意,分号和\n已恢复。

头文件和 CS50 手册页

  • 您的代码开头的语句 #include <stdio.h> 是一个非常特殊的命令,它告诉编译器您想要使用名为 stdio.h的功能,即一个头文件。除其他功能外,这还允许使用 printf 功能。

  • 是某人创建的代码的集合。库是其他人过去编写的预先编写的代码和函数的集合,我们可以在代码中使用它们。

  • 你可以在手册页上了解这个库的所有功能。手册页可以帮助你更好地理解各种命令做什么以及如何工作。

事实证明,CS50 有自己的库,名为 cs50.h。其中包含许多函数,会在你刚开始学习 C 时提供一些“辅助轮”:

get_char
get_double
get_float
get_int
get_long
get_string
  • 让我们在您的程序中使用这个库。

你好,你

  • 回想一下,在 Scratch 中,我们能够询问用户“你叫什么名字?”并在后面加上那个名字说“你好”。

在 C 中,我们也可以这样做。请按如下方式修改你的代码:

// get_string and printf with incorrect placeholder

#include

int main(void)
{
    string answer = get_string("What's your name? ");
    printf("hello, answer\n");
}

get_string 函数用于从用户处获取字符串。然后,变量 answer 被传递给 printf 函数。

  • 再次在终端窗口中运行make hello,请注意出现大量错误。

查看这些错误,编译器无法识别 stringget_string。我们必须添加名为 cs50.h 的库,让编译器知道这些功能。另外,我们也会注意到 answer 并没有按我们的意图显示出来。请按如下方式修改你的代码:

// get_string and printf with %s

#include
#include

int main(void)
{
    string answer = get_string("What's your name? ");
    printf("hello, %s\n", answer);
}

get_string 函数用于从用户处获取字符串。然后,变量 answer 被传递给 printf 函数。 %s 告诉 printf 函数准备接收 string

  • 现在,在终端窗口中再次运行 make hello,然后输入 ./hello 来运行程序。程序现在会询问你的姓名,然后按预期带着你的名字打招呼。

answer 是一个特殊的存放位置,我们称之为变量answer 的类型是 string,可以保存任意字符串。还有许多数据类型,例如 intboolchar 等等。

%s 是一个称为“格式代码”的占位符,它告诉 printf 函数准备接收 stringanswer 是传递给 %sstring

类型

printf 允许多种格式代码。以下是您可以在本课程中使用的不完整列表:

%c
%f
%i
%li
%s

%s 用于 string 变量。%i 用于 int 或整型变量。您可以在手册页上找到更多相关信息

这些格式代码对应于C中可用的许多数据类型:

bool
char
float
int
long
string
...

在整个课程中,我们将使用 C 的许多可用数据类型。

条件句

  • 您在 Scratch 中使用的另一个构建块是条件。例如,如果 x 大于 y,您可能会尝试一件事。此外,如果不满足该条件,您可能会尝试其他事情。

  • 我们看一下 Scratch 中的几个例子。

在C中,您可以按如下方式比较两个值:

// Conditionals that are mutually exclusive

if (x  y)
{
    printf("x is less than y\n");
}
else
{
    printf("x is not less than y\n");
}

请注意,如果 x < y,一个结果会如何发生。如果 x 不小于 y,则出现另一种结果。

同样,我们可以规划出可能的结果:

// Conditional that isn't necessary

if (x  y)
{
    printf("x is less than y\n");
}
else if (x > y)
{
    printf("x is greater than y\n");
}
else if (x == y)
{
    printf("x is equal to y\n");
}

请注意,并非所有这些代码行都是必需的。如何才能消除上面不必要的计算呢?

您可能已经猜到我们可以按以下方式改进此代码:

// Compare integers

if (x  y)
{
    printf("x is less than y\n");
}
else if (x > y)
{
    printf("x is greater than y\n");
}
else
{
    printf("x is equal to y\n");
}

请注意最后的语句如何替换为else

运营商

符号是指编译器支持的数学侵害。在C中,这些数学符号包括:

+ 为加法

- 为减法

* 乘法

/ 为除法

% 为余数

我们将在本课程中使用所有这些动作。

指标

在 C 中,您可以为 int 或变量赋值,如下所示:

int counter = 0;

请注意如何为 int 类型的名为 counter 的变量分配值 0

C也可以编程为counter加1,如下图:

counter = counter + 1;

请注意 1 如何与 counter 的值相加。

这也可以表示为:

counter += 1;

这可以简化为:

counter++;

请注意如何使用 ++ 加 1。

您还可以从counter中减一,如下图:

counter--;

请注意如何从 counter 的值中删除 1

比较.c

  • 使用有关如何为变量赋值的新知识,您可以编写第一个条件语句。

在终端窗口中输入code compare.c并编写代码如下:

// Conditional, Boolean expression, relational operator

#include
#include

int main(void)
{
    // Prompt user for integers
    int x = get_int("What's x? ");
    int y = get_int("What's y? ");

    // Compare integers
    if (x  y)
    {
        printf("x is less than y\n");
    }
}

请注意,我们创建了两个变量,一个名为 int 或整数的 x,另一个名为 y。这些值使用 get_int 函数填充。

  • 您可以通过在终端窗口中执行make compare,然后执行./compare来运行代码。如果您收到任何错误消息,请检查您的代码是否有错误。

流程图是一种检查计算机程序如何运行的方法。这样的图表可以用来检查我们代码的效率。

  • 查看上述代码的流程图,我们可以发现许多缺陷。

我们可以通过如下编码来改进您的程序:

// Conditionals

#include
#include

int main(void)
{
    // Prompt user for integers
    int x = get_int("What's x? ");
    int y = get_int("What's y? ");

    // Compare integers
    if (x  y)
    {
        printf("x is less than y\n");
    }
    else if (x > y)
    {
        printf("x is greater than y\n");
    }
    else
    {
        printf("x is equal to y\n");
    }
}

请注意,所有潜在结果现在均已考虑注意事项。

  • 您可以重新制作并重新运行您的程序并进行测试。

  • 在流程图上检查该程序,您可以看到我们代码设计决策的效率。

同意.c

  • 考虑另一种称为 char 的数据类型,我们可以通过在终端窗口中键入 code agree.c 来启动新程序。

  • 其中 string 是一系列字符,而 char 是单个字符。

在文本编辑器中编写的代码如下:

// Comparing against lowercase char

#include
#include

int main(void)
{
    // Prompt user to agree
    char c = get_char("Do you agree? ");

    // Check whether agreed
    if (c == 'y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'n')
    {
        printf("Not agreed.\n");
    }
}

请注意,单引号用于单个字符。另外,请注意,== 确保某些东西“相同”其他东西,其中单个等号在 C 中将具有非常不同的功能。

  • 您可以通过在终端窗口中输入make agree,然后输入./agree来测试代码。

我们还可以允许输入大写和小写字符:

// Comparing against lowercase and uppercase char

#include
#include

int main(void)
{
    // Prompt user to agree
    char c = get_char("Do you agree? ");

    // Check whether agreed
    if (c == 'y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'Y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'n')
    {
        printf("Not agreed.\n");
    }
    else if (c == 'N')
    {
        printf("Not agreed.\n");
    }
}

请注意,还提供了其他选项。但是,这不是高效的代码。

我们可以改进如下代码:

// Logical operators

#include
#include

int main(void)
{
    // Prompt user to agree
    char c = get_char("Do you agree? ");

    // Check whether agreed
    if (c == 'Y' || c == 'y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'N' || c == 'n')
    {
        printf("Not agreed.\n");
    }
}

请注意,|| 注释表示

循环和meow.c

  • 我们还可以在 C 程序中使用 Scratch 的循环构建块。

在终端窗口中,输入code meow.c并编写代码,如下所示:

// Opportunity for better design

#include

int main(void)
{
    printf("meow\n");
    printf("meow\n");
    printf("meow\n");
}

请注意,这确实符合预期,但有机会进行更好的设计。代码又重复地重复。

我们可以通过修改您的代码来改进我们的程序,如下所示:

// Better design

#include

int main(void)
{
    int i = 3;
    while (i > 0)
    {
        printf("meow\n");
        i--;
    }
}

请注意,我们创建了一个名为 iint 并顺便指定了值 3。然后,我们创建一个 while 循环,其运行时间与 i > 0 一样长。然后,循环运行。每次使用 i-- 语句将 1 复制为 i

同样,我们可以通过修改代码来实现某种统计,如下所示:

// Print values of i

#include

int main(void)
{
    int i = 1;
    while (i  3)
    {
        printf("meow\n");
        i++;
    }
}

请注意我们的节点i是如何在1处启动的。每次循环运行时,节点都会增加1。一旦节点大于3,就会停止循环。

总的来说,在计算机科学中,我们从零开始计数。最好按如下方式修改您的代码:

// Better design

#include

int main(void)
{
    int i = 0;
    while (i  3)
    {
        printf("meow\n");
        i++;
    }
}

请注意,我们现在从零开始计数。

  • 我们工具箱中用于循环的另一个工具是 for 循环。

您可以修改使用 for 循环改进我们的 meow.c 程序的设计。您的代码如下:

// Better design

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        printf("meow\n");
    }
}

请注意,for 循环包含三个参数。第一个参数 int i = 0 从零开始参数。第二个参数 i < 3 是正在检查的条件。最后,参数 i++ 告诉循环每次运行时加一。

我们甚至可以使用以下代码永远循环:

// Infinite loop

#include
#include

int main(void)
{
    while (true)
    {
        printf("meow\n");
    }
}

请注意,true 始终如此。因此,代码将始终运行。运行此代码您将失去对终端窗口的控制。您可以通过敲击键盘上的 control-C 来打破无限循环。

功能

虽然我们稍后将提供更多指导,但您可以在 C 中创建自己的函数,如下所示:

void meow(void)
{
    printf("meow\n");
}

底层的void表示该函数不返回任何值。(void)表示该函数不提供任何值。

该函数可以在主函数中使用,如下所示:

// Abstraction

#include

void meow(void);

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        meow();
    }
}

// Meow once
void meow(void)
{
    printf("meow\n");
}

请注意如何使用 meow() 指令调用 meow 函数。这是可能的,因为 meow 函数是在代码底部定义的,并且该函数的“原型”在代码顶部提供为 void meow(void)

您的meow函数可以进一步修改以接受输入:

// Abstraction with parameterization

#include

void meow(int n);

int main(void)
{
    meow(3);
}

// Meow some number of times
void meow(int n)
{
    for (int i = 0; i  n; i++)
    {
        printf("meow\n");
    }
}

请注意,原型已更改为 void meow(int n),以 meow 接受 int 作为其输入。

另外,我们还可以用户获取输入:

// User input

#include
#include

void meow(int n);

int main(void)
{
    int n;
    do
    {
        n = get_int("Number: ");
    }
    while (n  1);
    meow(n);
}

// Meow some number of times
void meow(int n)
{
    for (int i = 0; i  n; i++)
    {
        printf("meow\n");
    }
}

请注意,get_int 用于从用户处获取号码。n 提交给 meow

我们甚至可以进行测试以确保用户提供的输入是正确的:

// Return value

#include
#include

int get_positive_int(void);
void meow(int n);

int main(void)
{
    int n = get_positive_int();
    meow(n);
}

// Get number of meows
int get_positive_int(void)
{
    int n;
    do
    {
        n = get_int("Number: ");
    }
    while (n  1);
    return n;
}

// Meow some number of times
void meow(int n)
{
    for (int i = 0; i  n; i++)
    {
        printf("meow\n");
    }
}

请注意,名为 get_positive_int 的新函数要求用户输入整数,而 n < 1 则要求用户输入整数。获得正整数后,该函数将 return n 返回到 main 函数。

正确性、设计、风格

  • 可以在三个轴上评估代码。

  • 首先,正确性是指“代码是否按预期运行?”您可以使用check50检查代码的正确性。

  • 其次,设计是指“代码设计得怎么样?”您可以使用design50评估代码的设计。

  • 最后,风格指的是“代码的美观程度和一致性如何?”您可以使用 style50 评估代码的风格。

马里奥

  • 我们今天讨论的所有内容都集中在作为新兴计算机科学家工作的各个组成部分。

  • 以下内容将帮助您确定本课程的一般问题集:如何处理与计算机科学相关的问题?

想象一下,我们想要模拟《超级马里奥兄弟》游戏的视觉效果。考虑到上面的四个问题块,我们如何创建代表这四个水平块的代码?

在条目窗口中,输入 code mario.c 并编码如下:

// Prints a row of 4 question marks with a loop

#include

int main(void)
{
    for (int i = 0; i  4; i++)
    {
        printf("?");
    }
    printf("\n");
}

请注意此处如何使用循环打印四个问号。

同样,我们可以应用相同的逻辑来创建三个垂直块。

要实现此目的,请按如下方式修改您的代码:

// Prints a column of 3 bricks with a loop

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        printf("#\n");
    }
}

请注意如何使用循环打印三个垂直的砖块。

如果我们想结合这些想法来创建一组三乘三的块怎么办?

我们可以遵循上面的逻辑,结合相同的想法。修改您的代码如下:

// Prints a 3-by-3 grid of bricks with nested loops

#include

int main(void)
{
    for (int i = 0; i  3; i++)
    {
        for (int j = 0; j  3; j++)
        {
            printf("#");
        }
        printf("\n");
    }
}

请注意,一个循环位于另一个循环内部。第一个循环定义要打印垂直行。对于每一行,打印三列。每行之后都会打印一个新行。

如果我们想保证块的数量恒定,即不可更改,该怎么办?修改您的代码如下:

// Prints a 3-by-3 grid of bricks with nested loops using a constant

#include

int main(void)
{
    const int n = 3;
    for (int i = 0; i  n; i++)
    {
        for (int j = 0; j  n; j++)
        {
            printf("#");
        }
        printf("\n");
    }
}

请注意 n 现在如何成为常量。它永远无法改变。

正如本讲座前面所说,我们可以将功能“抽象”作为函数。考虑以下代码:

// Helper function

#include

void print_row(int width);

int main(void)
{
    const int n = 3;
    for (int i = 0; i  n; i++)
    {
        print_row(n);
    }
}

void print_row(int width)
{
    for (int i = 0; i  width; i++)
    {
        printf("#");
    }
    printf("\n");
}

请注意如何通过新函数完成打印行。

评论

  • 注释是计算机程序的基本部分,您可以在其中向自己以及可能与您合作处理您的代码的其他人留下解释性注释。

  • 您所涉及的课程创建的所有代码都必须包含可靠的注释。

  • 通常,每个注释都是几个单词或更多,为读者提供了了解特定代码中发生的情况的机会。此外,当您以后需要修改代码时,此类注释可以提醒您。

注释涉及将 // 放入您的代码中,然后是注释。按如下方式修改代码集成注释:

// Helper function

#include

void print_row(int width);

int main(void)
{
    const int n = 3;

    // Print n rows
    for (int i = 0; i  n; i++)
    {
        print_row(n);
    }
}

void print_row(int width)
{
    for (int i = 0; i  width; i++)
    {
        printf("#");
    }
    printf("\n");
}

请注意每个注释如何以 // 开头。

有关运营商的更多信息

您可以在C中计算器实现。在您的终端中,输入code calculator.c并编写代码,如下所示:

// Addition with int

#include
#include

int main(void)
{
    // Prompt user for x
    int x = get_int("x: ");

    // Prompt user for y
    int y = get_int("y: ");

    // Add numbers
    int z = x + y;

    // Perform addition
    printf("%i\n", z);
}

请注意如何使用 get_int 函数从用户处获取整数倍。一个整数存储在名为 xint 变量中。另一个存储在名为 yint 变量中。总和存储在 z 中。然后,printf 函数打印 z 的值,由 %i符号指定。

我们还可以将数字加倍:

// int

#include
#include

int main(void)
{
    int dollars = 1;
    while (true)
    {
        char c = get_char("Here's $%i. Double it and give to next person? ", dollars);
        if (c == 'y')
        {
            dollars *= 2;
        }
        else
        {
            break;
        }
    }
    printf("Here's $%i.\n", dollars);
}

运行该程序,dollars 中出现一些扫描错误的错误。这是为什么呢?

  • C 的缺点之一是它管理内存的便利性。虽然 C 为您提供了对内存使用方式的巨大控制,但程序员必须非常清楚内存管理的潜在陷阱。

  • 类型是指可以存储在变量中的可能数据。例如,char 旨在容纳单个字符,如 a2

  • 类型非常重要,因为终结类型都有特定的限制。例如,由于内存的限制,int 的顶点可能是 4294967295。如果尝试对 int 进行更高的计数,则会导致溢出整数,因此这个变量中存储不正确的值。

  • 限制了我们可以统计的高低。

  • 这可能会对现实世界产生灾难性的影响。

我们可以使用名为 long 的数据类型来修正此问题。

// long

#include
#include

int main(void)
{
    long dollars = 1;
    while (true)
    {
        char c = get_char("Here's $%li. Double it and give to next person? ", dollars);
        if (c == 'y')
        {
            dollars *= 2;
        }
        else
        {
            break;
        }
    }
    printf("Here's $%li.\n", dollars);
}

请注意运行此代码将如何允许非常高的美元金额。

在本课程中您可能会接触到的类型包括:

bool,true 或 false 的布尔表达式

char,单个字符,如 a 或 2

double,比浮点数多的浮点值

float,浮点值,或带十照明值的实数

int,达到一定大小或附加的整数

long,具有更多位的整数,因此它们可以比 int 计数更高

string,一串字符

哈断

使用数据类型时可能会出现另一个问题,包括中断。

// Division with ints, demonstrating truncation

#include
#include

int main(void)
{
    // Prompt user for x
    int x = get_int("x: ");

    // Prompt user for y
    int y = get_int("y: ");

    // Divide x by y
    printf("%i\n", x / y);
}

整数除以整数将始终得到 C 中的整数因此。,上述代码通常会导致小数点后的任何数字被丢弃。

这可以通过使用 float 来解决:

// Floats

#include
#include

int main(void)
{
    // Prompt user for x
    float x = get_float("x: ");

    // Prompt user for y
    float y = get_float("y: ");

    // Divide x by y
    printf("%.50f\n", x / y);
}

请注意,这解决了我们的一些问题。但是,我们可能会注意到程序提供的答案不准确。

浮点不精确表明计算机计算数字的精确度是有限的。

  • 在编码时,请特别注意所使用的变量类型,居民代码中出现的问题。

  • 我们研究了一些可能导致类型相关的错误并发生灾难性的后果。

总结

在本课程中,您学习了如何将在 Scratch 中学习的构建块应用到 C 编程语言中。您学到了……

  • 如何在 C 中创建您的第一个程序。

  • 如何使用命令行。

  • 关于C自带的预定义函数。

  • 如何使用变量、条件和循环。

  • 如何创建您自己的函数来简化和改进您的代码。

  • 如何从三个轴评估代码:正确的性、设计和风格。

  • 如何将集成注释到代码中。

  • 如何使用类型和符号以及您选择的意义。

下次见!