| 关于   英文版   «  4.1. 第 3 周:算法   ::   目录   ::   6.1. 第 5 周:数据结构  »

5.1. 第 4 周:内存

5.1.1. 欢迎!

  • 今天,我们将卸下许多你在本课程入门阶段所使用的辅助轮。

  • 在前几周,我们讨论了图像由称为像素的更小构建块组成。

  • 本周,我们将更深入地探讨构成这些图像的零和一。特别是,我们将深入探究构成文件(包括图像)的基本构建块。

  • 此外,我们将讨论如何访问存储在计算机内存中的底层数据。

  • 在我们开始今天的内容时,请注意,本讲座涵盖的概念可能需要一些时间才能完全 理解 。

5.1.2. 像素艺术

  • 像素是排列在上下、左右网格上的彩色方块,即单个点。

  • 你可以将图像想象为位图,其中 0 代表黑色,1 代表白色。

    Zeros and ones being converted to a black and white smiley

5.1.3. 十六进制

  • RGB ,即 红、绿、蓝 ,是表示每种颜色含量的数值。在 Adobe Photoshop 中,你可以看到如下设置:

    A photoshop panel with RGB values and hexadecimal input

    注意红色、蓝色和绿色的量如何改变所选颜色。

  • 从上图可以看出,颜色并非仅由三个值表示。在窗口底部,有一个由数字和字符组成的特殊值。 255 被表示为 FF 。这是为什么呢?

  • 十六进制 是一种拥有 16 个计数值的计数系统。它们如下所示:

    0 1 2 3 4 5 6 7 8 9 A B C D E F
    

    注意 F 表示 15 。

  • 十六进制也被称为 base-16 。

  • 在十六进制计数中,每一列都是 16 的幂。

  • 数字 0 表示为 00 。

  • 数字 1 表示为 01 。

  • 数字 9 由 09 表示。

  • 数字 10 表示为 0A 。

  • 数字 15 表示为 0F 。

  • 数字 16 表示为 10 。

  • 数字 255 表示为 FF ,因为 16 x 15(或 F )是 240. 再加 15 得到 255。这是使用两位十六进制系统可以计数的最大数字。

  • 十六进制很有用,因为它可以用更少的数字来表示。十六进制允许我们更简洁地表示信息。

5.1.4. 内存

  • 在过去的几周里,你可能还记得我们绘制的并发内存块示意图。为每个内存块应用十六进制编号后,你可以将其可视化如下:

    Blocks of memory numbered in hex

  • 你可以想象,对于上述 10 块究竟表示内存中的一个位置还是值 10 ,可能会产生混淆。因此,按照惯例,所有十六进制数通常都带有 0x 前缀,如下所示:

    blocks of memory numbered in hex with 0x

  • 在终端窗口中,输入 code addresses.c 并按如下方式编写代码:

    // Prints an integer
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        printf("%i\n", n);
    }
    

    注意 n 是如何以值 50 存储在内存中的。

  • 你可以按如下方式可视化该程序如何存储此值:

    the value 50 stored in a memory location with hex

5.1.5. 指针

  • C 语言有两个与内存相关的强大运算符:

    & Provides the address of something stored in memory.
    * Instructs the compiler to go to a location in memory.
    
  • addresses0.c

    我们可以利用这一知识,按如下方式修改代码:

    // Prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        printf("%p\n", &n);
    }
    

    注意 %p ,它允许我们查看内存中某个位置的地址。 &n 可以直译为" n 的地址”。执行此代码将返回一个以 0x 开头的内存地址。你可以在此处 here 下载此代码。

  • 指针 是存储某物地址的变量。最简洁地说,指针是你计算机内存中的一个地址。

  • 考虑以下代码:

    int n = 50;
    int *p = &n;
    

    注意 p 是一个指针,它包含整数 n 的地址。

  • addresses1.c

    按以下方式修改你的代码:

    // Stores and prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        int *p = &n;
        printf("%p\n", p);
    }
    

    请注意,这段代码的效果与我们之前的代码相同。我们只是利用了关于 & 和 * 运算符的新知识。你可以在此处 here 下载此代码。

  • addresses2.c

    为了说明 * 运算符的用法,考虑以下示例:

    // Stores and prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        int *p = &n;
        printf("%p\n", p);
    }
    

    请注意, printf 行打印了该整数的地址。 int *p 创建了一个指针,其作用是存储一个整数的内存地址。你可以在此处 here 下载此代码。

  • 你可以将我们的代码可视化如下:

    Same value of 50 in a memory location with a pointer value stored elsewhere

    请注意,指针看起来相当大。事实上,指针通常存储为 8 字节的值。 p 正在存储 50 的地址。

  • 你可以更准确地将指针可视化为一个指向另一个地址的地址:

    A pointer as an arrow, pointing from one location of memory to another

5.1.6. 字符串

  • 现在我们已经建立了指针的心智模型,可以揭开本课程早期提供的一层简化。

  • addresses4.c

    按以下方式修改你的代码:

    // Prints a string
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%s\n", s);
    }
    

    注意,会打印一个字符串 s 。你可以下载此代码 here 。

  • 回想一下,字符串只是一个字符数组。例如, string s = "HI!" 可以表示如下:

    The string HI with an exclamation point stored in memory

  • 然而, s 究竟是什么? s 存储在内存中的何处?正如你所想象的那样, s 需要被存储在某个地方。你可以将 s 与该字符串的关系可视化如下:

    Same string HI with a pointer pointing to it

    注意,一个名为 s 的指针告诉编译器字符串的第一个字节在内存中的位置。

  • addresses5.c

    按以下方式修改你的代码:

    // Prints a string's address as well the addresses of its chars
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%p\n", s);
        printf("%p\n", &s[0]);
        printf("%p\n", &s[1]);
        printf("%p\n", &s[2]);
        printf("%p\n", &s[3]);
    }
    

    注意上述代码打印了字符串中每个字符的内存地址 s 。 & 符号用于显示字符串中每个元素的地址。运行此代码时,请注意元素 0 、 1 、 2 和 3 在内存中彼此相邻。你可以在此处下载此代码 here 。

  • addresses6.c

    同样地,你可以按如下方式修改代码:

    // Declares a string with CS50 Library
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%s\n", s);
    }
    

    注意,此代码使用 cs50.h 库创建了一个字符串。你可以在此处 here 下载此代码。

  • addresses7.c

    拆掉辅助轮后,你可以再次修改代码:

    // Declares a string without CS50 Library
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%s\n", s);
    }
    

    请注意 cs50.h 已被移除。字符串被实现为 char * 。此代码将呈现从 s 位置开始的字符串。此代码有效地移除了由 cs50.h 提供的 string 数据类型的辅助轮。这是原始 C 代码,没有 cs50 库的支撑结构。你可以在此处 here 下载此代码。

  • 你可以想象字符串作为一种数据类型是如何创建的。

  • 上周,我们学习了如何使用结构体创建自己的数据类型。

  • cs50 库包含如下类型定义: typedef char *string

  • 使用 cs50 库时,该类型定义是一种简化表示,允许使用名为 string 的自定义数据类型。

5.1.7. 指针算术

  • 指针算术是指能够对内存地址进行数学运算的能力。

  • addresses8.c

    你可以修改代码,按如下方式打印字符串中的每个内存位置:

    // Prints a string's chars
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%c\n", s[0]);
        printf("%c\n", s[1]);
        printf("%c\n", s[2]);
    }
    

    请注意,我们是在 s 的位置打印每个字符。你可以下载此代码 here 。

  • addresses9.c

    此外,你可以按如下方式修改代码:

    // Prints a string's chars via pointer arithmetic
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%c\n", *s);
        printf("%c\n", *(s + 1));
        printf("%c\n", *(s + 2));
    }
    

    请注意,位于 s 处的第一个字符会被打印。然后,位于 s + 1 处的字符会被打印,依此类推。你可以下载此代码 here 。

  • addresses10.c

    同样地,考虑以下情况:

    // Prints substrings via pointer arithmetic
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%s\n", s);
        printf("%s\n", s + 1);
        printf("%s\n", s + 2);
    }
    

    请注意,此代码会打印从 s 开始的各种内存位置中存储的值。你可以在此处 here 下载此代码。

5.1.8. 字符串比较

  • 字符串只是由其首个字节位置标识的字符数组。

  • compare0.c

    在课程前期,我们探讨了整数的比较。我们可以通过在终端窗口中输入 code compare.c 来用代码表示这一点,如下所示:

    // Compares two integers
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two integers
        int i = get_int("i: ");
        int j = get_int("j: ");
    
        // Compare integers
        if (i == j)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

    请注意,此代码从用户处获取两个整数并进行比较。你可以在此处 here 下载此代码。

  • 对于字符串而言,不能使用 == 运算符来比较两个字符串。

  • 尝试使用 == 运算符比较字符串时,实际比较的是字符串的内存地址,而非其中的字符。因此,我们推荐使用 strcmp 。

  • compare1.c

    为了说明这一点,请按以下方式修改你的代码:

    // Compares two strings' addresses
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Compare strings' addresses
        if (s == t)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

    注意到为两个字符串都输入 HI! 仍然会输出 Different 。你可以在此 here 下载此代码。

  • 为什么这些字符串看似不同?你可以使用以下内容来直观理解原因:

    two strings stored separately in memory

  • 因此,上述 compare.c 的代码实际上是在尝试判断内存地址是否不同,而非字符串本身。

  • compare2.c

    使用 strcmp ,我们可以修正代码:

    // Compares two strings using strcmp
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Compare strings
        if (strcmp(s, t) == 0)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

    请注意, strcmp 在字符串相同时可以返回 0 。你可以从此处 here 下载此代码。

  • compare3.c

    为了进一步说明这两个字符串如何存在于两个不同的位置,请按如下方式修改你的代码:

    // Prints two strings
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Print strings
        printf("%s\n", s);
        printf("%s\n", t);
    }
    

    请注意,我们现在有两个独立的字符串被存储,很可能位于两个不同的位置。你可以在此处 here 下载此代码。

  • compare4.c

    通过稍作修改,你可以看到这两个存储字符串的位置:

    // Prints two strings' addresses
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Print strings' addresses
        printf("%p\n", s);
        printf("%p\n", t);
    }
    

    注意,打印语句中的 %s 已更改为 %p 。你可以在此处 here 下载此代码。

5.1.9. 复制与 malloc

  • 编程中一个常见的需求是将一个字符串复制到另一个字符串。

  • copy0.c

    在终端窗口中,输入 code copy.c 并按如下方式编写代码:

    // Capitalizes a string
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        string s = get_string("s: ");
    
        // Copy string's address
        string t = s;
    
        // Capitalize first letter in string
        t[0] = toupper(t[0]);
    
        // Print string twice
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    注意, string t = s 将 s 的地址复制到了 t 。这并未实现我们的目标。字符串本身未被复制——仅复制了地址。此外,请注意其中包含了 ctype.h 。你可以在此处 here 下载此代码。

  • 你可以将上述代码可视化如下:

    two pointers pointing at the same memory location with a string

    请注意, s 和 t 仍然指向相同的内存块。这并不是字符串的真实副本;相反,这是两个指向同一字符串的指针。

  • copy1.c

    在应对这一挑战之前,重要的是确保我们的代码不会发生 段错误 ,即避免尝试将 string s 复制到 string t ,而 string t 并不存在。我们可以如下使用 strlen 函数来协助解决这一问题:

    // Capitalizes a string, checking length first
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        string s = get_string("s: ");
    
        // Copy string's address
        string t = s;
    
        // Capitalize first letter in string
        if (strlen(t) > 0)
        {
            t[0] = toupper(t[0]);
        }
    
        // Print string twice
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    注意 strlen 用于确保 string t 有内容。如果没有,则不会复制任何内容。你可以在此处 here 下载此代码。

  • 为了能够制作字符串的真实副本,我们需要引入两个新的构建块。首先, malloc 允许你(程序员)分配一块特定大小的内存。其次, free 允许你告诉编译器 释放 之前分配的那块内存。

  • copy2.c

    我们可以修改代码,如下所示创建字符串的真实副本:

    // Capitalizes a copy of a string
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory, including '\0'
        for (int i = 0; i <= strlen(s); i++)
        {
            t[i] = s[i];
        }
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    注意 malloc(strlen(s) + 1) 创建了一块长度为字符串 s 加一的内存。这允许在我们的最终复制字符串中包含 空 \0 字符。然后, for 循环遍历字符串 s 并将每个值赋给字符串 t 上的相同位置。你可以在此处 here 下载此代码。

  • copy3.c

    事实证明,我们的代码效率低下。请按如下方式修改你的代码:

    // Capitalizes a copy of a string, defining n in loop too
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory, including '\0'
        for (int i = 0, n = strlen(s); i <= n; i++)
        {
            t[i] = s[i];
        }
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    注意 n = strlen(s) 现在定义在 for loop 的左侧。最好不要在 for 循环的中间条件中调用不必要的函数,因为它会反复运行。当将 n = strlen(s) 移到左侧时,函数 strlen 仅运行一次。你可以下载此代码 here 。

  • copy4.c

    C 语言有一个名为 strcpy 的内置函数用于复制字符串。它可以按如下方式实现:

    // Capitalizes a copy of a string using strcpy
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory
        strcpy(t, s);
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    注意 strcpy 完成了我们之前 for 循环所做的相同工作。你可以下载此代码 here 。

  • copy5.c

    在出现错误时, get_string 和 malloc 都会返回 NULL ,这是内存中的一个特殊值。你可以编写如下代码来检查此 NULL 条件:

    // Capitalizes a copy of a string without memory errors
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
        if (s == NULL)
        {
            return 1;
        }
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
        if (t == NULL)
        {
            return 1;
        }
    
        // Copy string into memory
        strcpy(t, s);
    
        // Capitalize copy
        if (strlen(t) > 0)
        {
            t[0] = toupper(t[0]);
        }
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    
        // Free memory
        free(t);
        return 0;
    }
    

    请注意,如果获得的字符串长度为 0 或 malloc 失败,则返回 NULL 。此外,请注意 free 会让计算机知道你已完成对通过 malloc 创建的这块内存的使用。你可以在此处 here 下载此代码。

5.1.10. Valgrind

  • Valgrind 是一个工具,可用于检查你的程序中是否存在与 malloc 相关的内存问题。具体来说,它会检查你是否 free 了所有已分配的内存。

  • memory.c

    考虑以下 memory.c 的代码:

    // Demonstrates memory errors via valgrind
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *x = malloc(3 * sizeof(int));
        x[1] = 72;
        x[2] = 73;
        x[3] = 33;
    }
    

    注意,运行此程序不会导致任何错误。虽然 malloc 用于为数组分配足够的内存,但代码未能 free 所分配的内存。你可以在此处下载此代码 here 。

  • 如果你输入 make memory 后跟 valgrind ./memory ,你将收到一份来自 valgrind 的报告,该报告会指出你的程序导致内存丢失的位置。valgrind 揭示的一个错误是,我们试图在 3 的 x 处赋值 33 ,而我们仅分配了大小为 3 ( x[0] 、 x[1] 和 x[2] )的数组。另一个错误是我们从未释放 x 。

  • 你可以修改代码以释放 x 的内存,方法如下:

    // Demonstrates memory errors via valgrind
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *x = malloc(3 * sizeof(int));
        x[0] = 72;
        x[1] = 73;
        x[2] = 33;
        free(x);
    }
    

    请注意,现在再次运行 valgrind 将不再出现内存泄漏或错误

5.1.11. 垃圾值

  • 当你向编译器请求一块内存时,无法保证这块内存是空的。

  • garbage.c

    你分配的内存很可能之前已被计算机使用过。因此,你可能会看到 垃圾 或 无用值 。这是由于你获取了一块内存却未对其进行初始化所致。例如,考虑以下 garbage.c 的代码:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int scores[1024];
        for (int i = 0; i < 1024; i++)
        {
            printf("%i\n", scores[i]);
        }
    }
    

    请注意,运行此代码将为你的数组在内存中分配 1024 个位置,但 for 循环可能会显示其中并非所有值都是 0 。当你未将内存块初始化为零或其他值时,始终最好注意可能存在垃圾值。你可以在此处 here 下载此代码。

5.1.12. Binky 的指针乐趣

5.1.13. 交换

  • swap0.c

    在现实世界中,编程中一个常见的需求是交换两个值。自然,如果没有一个临时的存储空间,很难交换两个变量。在实践中,你可以输入 code swap.c 并编写如下代码来观察这一过程:

    // Fails to swap two integers
    
    #include <stdio.h>
    
    void swap(int a, int b);
    
    int main(void)
    {
        int x = 1;
        int y = 2;
    
        printf("x is %i, y is %i\n", x, y);
        swap(x, y);
        printf("x is %i, y is %i\n", x, y);
    }
    
    void swap(int a, int b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
    

    请注意,虽然这段代码可以运行,但它无法正常工作。即使这些值被传递给了 swap 函数,它们也没有发生交换。为什么?你可以下载此代码 here 。

  • 当你向函数传递值时,你提供的只是副本。 x 和 y 的 作用域 仅限于 main 函数,因为代码目前是这样编写的。也就是说,在 main 函数的花括号 {} 中创建的 x 和 y 的值仅具有 main 函数的作用域。在我们上面的代码中, x 和 y 是通过 值 传递的。

  • 考虑以下图像:

    a rectangle with machine code at top followed by globals heap and stack

    请注意,全局 变量(本课程尚未使用)存储在内存的某一处。各种函数则存储在另一块内存区域的 stack 中。

  • 现在,考虑以下图像:

    a rectangle with main function at bottom and swap function directly above it

    注意 main 和 swap 拥有两个独立的 帧 或内存区域。因此,我们无法简单地将值从一个函数传递到另一个函数来修改它们。

  • swap1.c

    按以下方式修改你的代码:

    // Swaps two integers using pointers
    
    #include <stdio.h>
    
    void swap(int *a, int *b);
    
    int main(void)
    {
        int x = 1;
        int y = 2;
    
        printf("x is %i, y is %i\n", x, y);
        swap(&x, &y);
        printf("x is %i, y is %i\n", x, y);
    }
    
    void swap(int *a, int *b)
    {
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
    

    请注意,变量不是按 值 传递,而是按 引用 传递。也就是说, x 和 y 的地址被提供给函数。因此, swap 函数可以知道从哪里修改主函数中的实际 x 和 y 。你可以下载此代码 here 。

  • 你可以将其可视化如下:

    x and y stored in main function being passed by reference to the swap function

5.1.14. 溢出

  • 堆溢出 是指堆发生溢出,触及了本不应访问的内存区域。

  • 栈溢出 是指调用了过多函数,导致可用内存量被耗尽。

  • 这两者都被视为 缓冲区溢出 。

5.1.15. scanf

  • 在 CS50 中,我们创建了诸如 get_int 之类的函数,以简化从用户获取输入的操作。

  • scanf 是一个可以获取用户输入的内置函数。

  • get1.c

    我们可以使用 scanf 相当容易地重新实现 get_int ,如下所示:

    // Gets an int from user using scanf
    
    #include <stdio.h>
    
    int main(void)
    {
        int n;
        printf("n: ");
        scanf("%i", &n);
        printf("n: %i\n", n);
    }
    

    请注意,值 n 存储在位置 n 处,位于行 scanf("%i", &n) 。你可以在此处 here 下载此代码。

  • get3.c

    然而,尝试重新实现 get_string 并不容易。考虑以下情况:

    // Dangerously gets a string from user using scanf with array
    
    #include <stdio.h>
    
    int main(void)
    {
        char s[4];
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
    }
    

    注意,由于 C 语言中的数组名充当指针,因此不需要 & 。然而,该程序并非每次运行都能正常工作。程序中没有任何地方为我们的目标字符串分配足够的内存。事实上,我们不知道用户会输入多长的字符串!此外,我们也不知道该内存位置可能存在哪些垃圾值。你可以下载此代码 here 。

  • 此外,你的代码可以修改如下。然而,我们必须为字符串预先分配一定量的内存:

    // Using malloc
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        char *s = malloc(4);
        if (s == NULL)
        {
            return 1;
        }
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
        free(s);
        return 0;
    }
    

    注意,如果提供一个四字节长的字符串,你 可能 会收到错误。

  • 简化我们的代码如下,我们可以进一步理解这个预分配的本质问题:

    #include <stdio.h>
    
    int main(void)
    {
        char s[4];
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
    }
    

    请注意,如果我们预先分配一个大小为 4 的数组,我们可以输入 cat ,程序也能正常运行。然而,比这更长的字符串 可能 会引发错误。

  • 有时,编译器或其运行的系统可能会分配比我们指示的更多的内存。但从根本上说,上述代码是不安全的。我们无法相信用户会输入一个适合我们预分配内存的字符串。

5.1.16. 文件输入/输出

  • phonebook0.c

    你可以读取并操作文件。虽然此主题将在未来的周次中进一步讨论,但请考虑以下 phonebook.c 的代码:

    // Saves names and numbers to a CSV file
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Open CSV file
        FILE *file = fopen("phonebook.csv", "a");
    
        // Get name and number
        char *name = get_string("Name: ");
        char *number = get_string("Number: ");
    
        // Print to file
        fprintf(file, "%s,%s\n", name, number);
    
        // Close file
        fclose(file);
    }
    

    请注意,此代码使用指针来访问文件。你可以在此处 here 下载该代码。

  • 你可以提前创建一个名为 phonebook.csv 的文件,或者下载 phonebook.csv 。运行上述程序并输入姓名和电话号码后,你会发现这些数据已持久化到你的 CSV 文件中。

  • phonebook1.c

    如果我们希望确保在运行程序之前 phonebook.csv 存在,可以按如下方式修改代码:

    // Saves names and numbers to a CSV file, checking for NULL
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Open CSV file
        FILE *file = fopen("phonebook.csv", "a");
        if (file == NULL)
        {
            return 1;
        }
    
        // Get name and number
        char *name = get_string("Name: ");
        char *number = get_string("Number: ");
    
        // Print to file
        fprintf(file, "%s,%s\n", name, number);
    
        // Close file
        fclose(file);
    }
    

    请注意,该程序通过调用 return 1 来防止 NULL 指针。你可以在此处 here 下载此代码。

  • cp.c

    我们可以通过输入 code cp.c 并编写如下代码来实现自己的复制程序:

    // Copies a file
    
    #include <stdio.h>
    
    typedef unsigned char BYTE;
    
    int main(int argc, char *argv[])
    {
        FILE *src = fopen(argv[1], "rb");
        FILE *dst = fopen(argv[2], "wb");
    
        BYTE b;
    
        while (fread(&b, sizeof(b), 1, src) != 0)
        {
            fwrite(&b, sizeof(b), 1, dst);
        }
    
        fclose(dst);
        fclose(src);
    }
    

    请注意,此文件创建了我们自己的数据类型 BYTE,其大小等同于 uint8_t。然后,该文件读取一个 BYTE 并将其写入文件。你可以在此处 here 下载此代码。

  • BMP 文件也是我们可以检查和处理的数据集合。本周,你将在习题集中进行此类操作!

5.1.17. 总结

在本课中,你学习了能够提供访问和操作特定内存位置数据能力的指针。具体来说,我们深入探讨了…

  • 像素艺术

  • 十六进制

  • 内存

  • 指针

  • 字符串

  • 指针算术

  • 字符串比较

  • 复制

  • malloc 与 Valgrind

  • 垃圾值

  • 交换

  • 溢出

  • scanf

  • 文件输入/输出

下次再见!

   «  4.1. 第 3 周:算法   ::   目录   ::   6.1. 第 5 周:数据结构  »

关闭窗口