WordPress 公司提供百年域名注册选项WordPress 公司称，希望保存故事、照片、声音和视频等数字资产的家庭，或希望保护和记录公司历史的创始人可以使用该产品。该服务的费用为 3.8 万美元，也就是平均每年 380 美元。此方案还包括跨越多个地理区域的内容备份，带宽不受限制，以及 24/7 小时的技术支持。消息来源：Silicon Republic老王点评：这钱真白交，我不觉得 100 年后还会有现在的互联网。IBM 开发出新型模拟 AI 芯片，大幅降低能耗IBM 的研究团队近期发布了一款新型的模拟 AI 芯片设计，这种基于大脑模式的芯片设计，可以模拟人脑神经网络的工作方式，实现复杂的计算任务，同时保持高效的能源使用。其性能已经达到了 GPU 的级别，却只有同等级 GPU 1/14 的能耗。这种芯片芯片使用相变存储器（PCM），PCM 不是记录数字系统中的 0 或 1，而是非晶态和晶态之间的连续值，它可将神经网络的权重直接编码到物理芯片上。IBM 的原型芯片能编码 3500 万个 PCM 装置，单个芯片最多能支持 1700 万参数的模型。消息来源：IBM老王点评：模拟 AI 芯片以其更低的能耗和类似人脑的处理方式，未来有望在 AI 应用中扮演更重要的角色。Meta 发布了编程的大语言模型Meta 发布了 Code Llama，这是一种基于 Llama 2 的新型大型语言模型，旨在通过生成和调试代码来帮助程序员。它与 ChatGPT 和 GitHub Copilot 类似，你可以用对话要求它编写代码。Code Llama 可以用 Python、Java、C++、PHP、TypeScript、C#、Bash 脚本等多种编程语言进行编程。该模型有三个规模：7B、13B 和 43B，其中 7B 和 13B 模型速度更快，更适合需要低延迟的任务，如实时代码完成，并且可以在单个消费级 GPU 上运行。消息来源：Ars Technica老王点评：这样说，可以在程序员自己的笔记本上跑了？另外，我才知道 Llama 这个缩写原来也是一个英文单词，意思是美洲羊驼，你知道这种动物的。
硬核观察 #1022 这是我们所知的计算机编程的终结
这是我们所知的计算机编程的终结纽约时报发表文章《这是我们所知的计算机编程的终结》谈到，虽然人工智能可能不会意味着编程的结束（世界仍然需要具有高级编码技能的人），但它可能标志着一种新的编程的开始 ——“一种不需要我们学习代码，而是将人类语言指令转变为软件的编程”。计算机编程从一种极客爱好变成了一种严肃职业，曾经很多人都教诲孩子们去学习编程，但编程一直在走向越来越简单的道路上。曾经焦虑孩子们不喜欢编程，与未来脱节，但现在，“等到他们找工作的时候，编码可能就像我的第一台电脑一样过时了。”消息来源：纽约时报老王点评：作为一个计算机专家，我也曾经希望我的孩子能喜欢编程，精通编程，但是似乎他除了喜欢游戏，并不喜欢编程。我也为此焦虑过，现在看起来大可不必，就像我并没有学会（或者说继承）我父亲精通的机电和电路一样，每个人都有自己的方向和需要掌握的技能。世界上最受欢迎的在线编程课向人工智能寻求帮助哈佛大学的 CS50 是一门计算机科学的入门课程，校内有数百名学生参加，网上有超过 4 万名学生参加。但即使有 100 多名现实生活中的教学助理，也很难与越来越多从不同时区登录的、具有不同知识和经验水平的学生充分接触。CS50 计划使用人工智能来评分作业、教授编码和个性化的学习提示。他们现在正在微调一个人工智能系统来标记学生的工作，并测试一个虚拟教学助理来评估和提供对学生编程的反馈。消息来源：MSN老王点评：结合上一条，既然人工智能可以干的更好，学生们何必再去学习普通的编程技能呢？红帽放弃维护 LibreOffice RPM 包红帽公司负责大部分桌面工作的显示系统团队多年来一直在 Fedora 中维护 LibreOffice 的 RPM 包。但现在他们将把更多精力投放到 Wayland、HDR 支持及工作站用户需要的一系列改进上，因此决定从未来的 RHEL 版本开始，停止把 LibreOffice 作为 RHEL 的一部分。但他们仍然会在目前支持的 RHEL 7/8/9 中维护 LibreOffice 相关的补丁。他们希望用户去使用 Flatpak 版的 LibreOffice。当然，他们也不反对任何社区用户接管这些 RPM 包的维护，并指出这是一个相当大的软件包和依赖关系块，需要大量的工作来维持。消息来源：LWN老王点评：作为红帽来说似乎这样的决策无可厚非，但是我怎么觉得这是红帽将用户推向 Flatpak 的一种潜意识行为呢？
这种有趣的开源语言是学习编码真正的完美选择。在这篇文章中，我将介绍一个最好的编码语言，学习如何制作任何东西！它叫做 Emojicode，是由 Theo Belaire 在 2014 年创建的，它是一种开源的编程语言，使用 emoji 字符来表示其语法。当用 Emojicode 开发时，表情符被用来创建变量、函数和控制结构。因为它是一种静态类型的语言，变量类型必须在使用前声明，但它也支持类和继承等面向对象的概念。这种语言可以在每个操作系统上运行，它是一种超级有趣的编码方式，特别是当你是一个非英语母语的人时。这很有帮助，因为象形表示可以把我们大家聚集在一起，让我们以类似数学的方式说同样的语言。先决条件在本教程中，我使用的是基于 Debian 的操作系统。我的工具可能与你的操作系统的要求不同。以下是我所使用的工具：Geany，一个 Linux 下的开源 IDE。IBus，它允许你挑选表情符并把它们放在你的编辑器中。我使用的界面被称为 emoji picker。基于 Debian 的 Linux。一个 C++ 编译器。我使用的是 g++ 编译器。Emojicode我使用的是 Linux 操作系统，但你可以 阅读文档 了解在其他操作系统上使用它时可能需要采取的任何特殊步骤。在 Linux 上安装 Emojicode有几种方法可以在你的电脑上安装 Emojicode，但它们有一个很酷的 神奇的安装页面，可以告诉你具体该怎么做。下面是我的做法：$ wget https://github.com/emojicode/emojicode/releases/download/v1.0-beta.2/Emojicode-1.0-beta.2-Linux-x86_64.tar.gz -O emojicode.tar.gz \ && tar -xzf emojicode.tar.gz && rm emojicode.tar.gz \ && cd Emojicode-1.0-beta.2-Linux-x86_64 && ./install.sh \ && cd .. && rm -r Emojicode-1.0-beta.2-Linux-x86_64 Emojicode 的安装过程提供了有用的反馈。现在，你已经安装好了，是时候开始编写代码了！它是怎么运作的？首先，所有 Emojicode 文件的扩展名都以文件名 .? 结尾，但因为你不能在一般的文件名中这样做，所以它被翻译成 filename.emojic。这里是最重要的语法元素：把 ? 放在一行的开头，表示要执行哪些代码块用 ? 开始一个代码块用 ? 来结束一个代码块想打印什么吗？就用 ? ? <string> ? ❗还有很多其他的，所以这里有一些实际的例子。打印一首俳句首先，试着打印一首漂亮的俳句来玩玩吧! 在这个例子中，我将添加一行注释。看看你是否能识别它。?? ? This is a single line comment for fun ? ?Emojicode is great,? ❗ ? ?Fun and expressive code,? ❗ ? ?no sadness, just joy.? ❗ ? 现在我们需要保存我们的代码，并通过我们的编译器运行它，使之成为一个漂亮的可执行文件：$ emojicodec haiku.emojic $ ls haiku haiku.emojic haiku.o 正如你所看到的，代码已经被编译并生成了两个文件，其中一个是可执行的。运行 haiku 文件：$ ./haiku Emojicode is great, Fun and expressive code, no sadness, just joy. 数学和变量操作接下来，你要同时做几件事：一点点数学和变量的改变。首先，将一个变量赋值为 0：0 ➡️ ??x 你刚刚使用蜡笔（?）表情符、新建（?）表情符和变量名称创建了一个新变量，同时还将该变量赋值为 0。接下来，用磁铁（?）表情符打印一行包括该变量的内容：? ?The value is ?x? ? ❗ 接下来，使用加号（➕）和箭头（⬅️）表情符改变变量：x ⬅️➕ 1 然后打印另一行的值。如此这般，然后打印出最终的数值。如下：? ? ?Updating a variable using math 0 ➡️ ??x ? ?The value is ?x? ? ❗ x ⬅️➕ 1 ? ?The value is ?x? ? ❗ x ⬅️➕ 15 ? ?The value is ?x? ? ❗ x ⬅️➖ 9 ? ?The value is ?x? ? ❗ x ⬅️➗ 2 ? ?The final value is ?x? ? ❗ ? 接下来，用 emojicodec 编译代码，然后用你的可执行代码来看看结果：$ emojicodec math.emojic $ ./math The value is 0 The value is 1 The value is 16 The value is 7 The final value is 3 如你所见，作为变量打印出来的所有内容都已使用新数学进行了更新。你可以用许多数学表情符来进一步操作。下面是一些更多的运算符：? is your modulo ◀ Less than ▶ Greater than ◀? less than or equal to ▶? greater than or equal to Emojicode 的怪癖当我在文档中来回查看时，我发现了一些有趣的怪癖。其中之一是，当从用户那里抓取输入时，由于一些已知的编译器问题，耳朵的肤色很重要。获取用户输入的动作是这样的：??▶️??❗️ 获取和分配用户输入的操作是这样的：??▶️??❗️ ➡️ inputText 我试图让它工作，我的编译器出现了一些问题，我发现了这个问题。你也可能会在这里和那里遇到一些小问题。如果你遇到了，请创建一个 议题，这样它就有可能被修复。没有技巧，只有很棒的代码虽然我可以介绍更多内容，但我可以向你保证，这段令人惊叹的代码背后的文档非常丰富。尽管我写这篇文章只是为了赶上愚人节的乐趣，但我不得不承认这是有史以来最好的语言之一，因为它教会了你很多非常真实的编程概念。我恳切地建议把它作为一种有趣的方式来教你的一些朋友、孩子，或者是对编码感兴趣的同学。希望你度过了一个充满乐趣的愚人节！via: https://opensource.com/article/23/4/emojicode作者：Jessica Cherry 选题：lkxed 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
我写了一个名为 “Toy CPU” 的教育性复古计算机程序，以便我的学生能够学习机器语言。我兼职教授大学课程，包括一个对所有专业开放的一般计算机主题的课程。这是一门入门课程，向学生讲授技术是如何运作的，以消除围绕计算的神秘感。虽然不是计算机科学课程，但这门课的一个部分涉及计算机编程。我通常用非常抽象的术语谈论编程，所以不会让听众听不懂。但是今年，我想让我的学生以 “老派” 的方式做一些需要 “动手” 的编程。同时，我又想保持简单，以便让每个人都能跟上。我喜欢将我的课程结构化，以显示你是如何从 “那里” 到 “这里” 的。理想情况下，我会让我的学生学习如何编写一个简单的程序。然后，我将从这里开始，展示现代编程是如何让开发人员创建更复杂的程序的。我决定尝试一种非常规的方法 —— 教学生学习终极的低级别编程语言：机器语言。机器语言编程早期的个人电脑如 Apple II（1977 年）、TRS-80（1977 年）和 IBM PC（1981 年）让用户用键盘输入程序，并在屏幕上显示结果。但计算机并不总是带有屏幕和键盘。Altair 8800 和 IMSAI 8080（均为 1975 年制造）要求用户使用面板上的 “开关和灯” 输入程序。你可以用机器语言输入指令，使用一组开关，机器会点亮 LED 灯以代表每个二进制指令的 1 和 0。对这些早期机器进行编程，需要了解被称为 “ 操作码 opcode ” （操作代码的简称）的机器语言指令，以执行基本操作，如将两个数字相加或将一个值存储到计算机的存储器中。我想向我的学生展示程序员是如何通过开关和灯，手工输入一系列指令和内存地址的。然而，在这门课上，使用实际的 Altair 8800 就有点太复杂了。我需要一些简单的、任何初级水平的学生都能掌握的东西。理想情况下，我希望能找到一个简单的 “业余” 复古计算机，其工作原理与 Altair 8800 相似，但我无法找到一个价格低于 100 美元的合适的 “类似 Altair” 的设备。我找到了几个 “Altair” 软件模拟器，但它们忠实地再现了 Altair 8800 的操作码，这对我的需求来说太过沉重。我决定编写我自己的 “教育” 复古计算机。我称它为 “Toy CPU”。你可以在我的 GitHub 代码库 上找到它，包括几个可以运行的版本。第一版是一个实验性的原型，运行在 FreeDOS 上。第二版是一个更新的原型，在 Linux 上用 ncurses 运行。版本 3 是一个 FreeDOS 程序，在图形模式下运行。Toy CPU 的编程Toy CPU 是一个非常简单的复古计算机。它只有 256 字节的内存和一个最小化的指令集，其目的是在复制 “开关和灯” 编程模式的同时保持简单化。它的界面模仿 Altair 8800，有一系列 8 个 LED 灯，分别代表计数器（程序的 “行号”）、指令、累积器（用于临时数据的内部存储器）和状态。当你启动 Toy CPU 时，它通过清除内存来模拟 “启动”。当它启动时，它也会在屏幕右下方的状态灯中显示 “INI”（初始化）。“PWR”（电源）灯亮表示 Toy CPU 已被打开。当 Toy CPU 准备好让你进入一个程序时，它通过状态灯指示 “INP”（“输入”模式），并让你从程序的计数器 0 开始。Toy CPU 的程序总是从计数器 0 开始。在 “输入” 模式下，用上下方向键显示不同的程序计数器，按回车键编辑当前计数器上的指令。当你进入 “编辑” 模式时，Toy CPU 的状态灯上会显示 “EDT”（“编辑” 模式）。Toy CPU 有一张速查表，被 “贴” 在显示屏的前面。它列出了 Toy CPU 可以处理的不同操作码。00000000（STOP）：停止程序执行。00000001（RIGHT）：将累加器中的位向右移动一个位置。值 00000010 变成 00000001，00000001 变成 00000000。00000010（LEFT）：将累加器中的位向左移动一个位置。值 01000000 变成 10000000，10000000 变成 00000000。00001111（NOT）：对累加器进行二进制非操作。例如，值 10001000 变成 01110111。00010001（AND）：对累加器用存储在某一地址的值进行二进制与操作。该地址被存储在下一个计数器中。00010010（OR）：对累积器用存储在某一地址的值进行二进制或运算。00010011（XOR）：对累加器用存储在某一地址的值进行二进制异或运算。00010100（LOAD）：将一个地址的值加载（复制）到累加器中。00010101（STORE）： 存储（复制）累加器中的值到一个地址。00010110（ADD）：将存储在某一地址的数值加入到累加器中。00010111（SUB）：从累积器中减去储存在某一地址的数值。00011000（GOTO）：转到（跳到）一个计数器地址。00011001（IFZERO）：如果累加器为零，转到（跳到）一个计数器地址。10000000（NOP）：空操作，可以安全地忽略。当处于 “编辑” 模式时，使用左右方向键选择操作码中的一个位，然后按空格键在关闭（0）和开启（1）之间翻转数值。当你完成编辑后，按回车键回到 “输入” 模式。一个示例程序我想通过输入一个简短的程序来探索 Toy CPU，将两个数值相加，并将结果存储在 Toy CPU 的内存中。实际上，这执行的是算术运算 A+B=C。要创建这个程序，你只需要几个操作码：00010100（LOAD）00010110（ADD）00010101（STORE）00000000（STOP）LOAD、ADD 和 STORE 指令需要一个内存地址，这个地址总是在下一个计数器的位置。例如，程序的前两条指令是:计数器 0：00010100 计数器 1：某个内存地址，第一个值 A 存放在那里 计数器 0 中的指令是 LOAD 操作，计数器 1 中的值是你存储某个值的内存地址。这两条指令一起将内存中的数值复制到 Toy CPU 的累加器中，在那里你可以对该数值进行操作。将一个数字 A 装入累加器后，你需要将数值 B 加到它上面。你可以用这两条指令来做：计数器 2：00010110 计数器 3：存储第二个值 B 的内存地址 假设你把值 1（A）装入累加器，然后把值 3（B）加到它上面。现在累加器的值是 4。现在你需要用这两条指令把数值 4 复制到另一个内存地址（C）：计数器 4：00010101 计数器 5：一个内存地址（C），我们可以在那里保存新的值 把这两个值加在一起后，现在可以用这条指令结束程序：计数器 6: 00000000 计数器 6 之后的任何指令都可以供程序作为存储内存使用。这意味着你可以用计数器 7 的内存来储存值 A，计数器 8 的内存来储存值 B ，计数器 9 的内存来储存值 C。你需要将这些分别输入到 Toy CPU 中：计数器 7：00000001（1） 计数器 8：00000011（3） 计数器 9：00000000（0，以后会被覆盖） 在弄清了所有指令和 A、B 和 C 的内存位置后，现在可以将完整的程序输入到 Toy CPU 中。这个程序将数值 1 和 3 相加，得到 4：计数器 0：00010100 计数器 1：00000111（7） 计数器 2：00010110 计数器 3：00001000（8） 计数器 4：00010101 计数器 5：00001001（9） 计数器 6：00000000 计数器 7：00000001（1） 计数器 8：00000011（3） 计数器 9：00000000（0，以后会被覆盖） 要运行程序，在 “输入” 模式下按下 R 键。Toy CPU 将在状态灯中显示 “RUN”（“运行” 模式），并从计数器 0 开始执行你的程序。Toy CPU 有一个明显的延迟，所以你可以看到它执行程序中的每一步。随着程序的进行，你应该看到计数器从 00000000（0）移动到 00000110（6）。在计数器 1 之后，程序从内存位置 7 加载数值 1，累积器更新为 00000001（1）。在计数器 3 之后，程序将加数值 3，并更新累加器显示 00000100（4）。累加器将保持这种状态，直到程序在计数器 5 之后将数值存入内存位置 9，然后在计数器 6 结束。探索机器语言编程你可以使用 Toy CPU 来创建其他程序，并进一步探索机器语言编程。通过用机器语言编写这些程序来测试你的创造力。一个在累积器上闪灯的程序你能点亮累加器上的右四位，然后是左四位，然后是所有的位吗？你可以用两种方法之一来写这个程序。一种直接的方法是，从不同的内存地址加载三个数值，像这样：计数器 0：LOAD 计数器 1：“右边” 计数器 2：LOAD 计数器 3：“左边” 计数器 4：LOAD 计数器 5：“所有” 计数器 6：STOP 计数器 7：00001111（“右边”） 计数器 8：11110000（“左边”） 计数器 9：11111111（“全部”） 写这个程序的另一种方法是尝试使用 NOT 和 OR 二进制操作。这样可以得到一个更小的程序：计数器 0：LOAD 计数器 1：“右边” 计数器 2：NOT 计数器 3：OR 计数器 4：“右边” 计数器 5：STOP 计数器 6：00001111（“右边”） 从一个数字开始倒数你可以把 Toy CPU 作为一个倒数计时器。这个程序行使 IFZERO 测试，只有当累加器为零时，程序才会跳转到一个新的计数器：计数器 0：LOAD 计数器 1：“初始值” 计数器 2：IFZERO（这也是倒计时的“开始”） 计数器 3：“结束” 计数器 4：SUB 计数器 5：“1” 计数器 6：GOTO 计数器 7：“开始” 计数器 8：STOP 计数器 9：00000111（“初始值”） 计数器 10：00000001（“1”） Toy CPU 是学习机器语言的一个好方法。我在入门课程中使用了 Toy CPU，学生们说他们发现写第一个程序很困难，但写下一个程序就容易多了。学生们还表示，用这种方式编写程序其实很有趣，他们学到了很多关于计算机实际工作的知识。Toy CPU 既具有教育性，也很有趣味性！via: https://opensource.com/article/23/1/learn-machine-language-retro-computer作者：Jim Hall 选题：lkxed 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #845 AlphaCode 在编程比赛中胜过了近一半的程序员
AlphaCode 在编程比赛中胜过了近一半的程序员DeepMind 在《科学》杂志发布文章说，在至少有 5000 人参加的在线编码比赛中，AlphaCode 的表现超过了 45.7% 的程序员。令人惊讶的是，研究人员将其产生的程序与训练数据库中的程序进行了比较，发现它并没有重复大段的代码或逻辑，而产生了一些新的代码。这让专家们注意到了递归式自我改进的软件的长期风险，这可能导致超级智能的人工智能接管世界。专家们希望尽早为人工智能编码领域建立护栏。消息来源：《科学》杂志老王点评：在科幻小说中，阿西莫夫创造性的提出了《机器人三定律》，而就目前的发展来看，可能要制定更具体的限制规则，让人工智能的递归式发展具有自限性。费米实验室和 CERN 选择 AlmaLinux在 2021 年底，CentOS 项目停止了 CentOS Linux 的更新，并以滚动版的 CentOS Stream 取代，重建了红帽的 Linux 开发和维护流程。很多使用 CentOS Linux 的企业转而使用其他的替代版本，如 AlmaLinux 和 Rocky Linux，以及国内的 AnolisOS 和 openEuler。著名的科学研究机构费米实验室和欧洲粒子物理研究所（CERN）发表联合声明，宣布将在其研究设施中采用 AlmaLinux。声明称，AlmaLinux 的优点包括：每个大版本支持寿命长，支持扩展架构，快速的发布周期，上游社区贡献，支持安全建议元数据，完美兼容红帽的企业版 Linux（RHEL）。消息来源：The Register老王点评：CentOS 当年的决定，带来的影响非常深远。美国 FBI 对苹果扩大端到端加密技术表示密切关注苹果公司昨天宣布，将在更多敏感类型的 iCloud 数据上采用端到端加密，这包括设备备份、信息、照片等，以满足用户和隐私团体的长期需求。这意味着只有受信任的设备才能解密和读取用户存储在 iCloud 中的数据。FBI 表示它“深切关注端到端加密和只允许用户访问的加密所带来的威胁”。它表示端到端加密使该机构更难开展工作，要求“通过设计进行合法访问”。消息来源：Mac Rumors老王点评：隐私和公众安全的边界确实存在冲突，至少值得讨论和博弈。
不同编程语言是如何完成同一件事
通过一个简单的小游戏比较 13 种编程语言。当我开始学习一种新的编程语言时，会把重点放在定义变量、书写声明以及计算表达式，一旦对这些概念有一个大致的了解，通常就能够自己弄清剩下的部分。大多数编程语言都具有相似性，所以如果你掌握了一种编程语言，学习下一种语言的重点就是弄清楚独有的概念以及区分不同。我喜欢写一些测试程序来帮助练习新的编程语言。其中我经常写的是一个叫做“猜数字”的小游戏，计算机选出 1 到 100 里的任一数字，然后我来猜。程序循环进行，直到猜出正确数字。通过伪代码可以看出，这是个非常简单的程序：计算机在 1 到 100 之间选出一个随机数字循环进行直到猜出该随机数字计算机读取我的猜测告诉我我的猜测过高还是过低我们发表了一些文章，用不同的语言写这个程序。这是一个比较不同语言做同样事情的有趣机会。大多数编程语言具有相似性，所以当你在学习下一种新的编程语言时，主要是学习它的独特之处。C 语言由 Dennis Ritchie 于 1972 年在贝尔实验室创建，是一种早期的通用编程语言。C 语言非常受欢迎，并迅速成为 Unix 系统上的标准编程语言。正是因为它的流行，许多其他编程语言也采用了类似的编程语法。这就是为什么如果你已经知道如何使用 C 语言编程，学习 C++、Rust、Java、Groovy、JavaScript、awk 或 Lua 会更容易。接下来我们看看这些不同的编程语言是如何实现 “猜数字” 游戏的主要步骤。我将把重点放在基本元素的相似或不同，跳过一些外围代码，如分配临时变量。计算机在 1 到 100 之间选出一个随机数字你可以看到这里有许多相似之处。大多数编程语言使用类似 rand() 的函数，你可以设定一个范围来生成随机数。而其他一些语言使用一个特殊的函数来设定范围生成随机数。C：// Using the Linux `getrandom` system call getrandom(&randval, sizeof(int), GRND_NONBLOCK); number = randval % maxval + 1; // Using the standard C library number = rand() % 100 + 1; C++：int number = rand() % 100+1; Rust：let random = rng.gen_range(1..101); Java：private static final int NUMBER = r.nextInt(100) + 1; Groovy：int randomNumber = (new Random()).nextInt(100) + 1 JavaScript：const randomNumber = Math.floor(Math.random() * 100) + 1 awk：randomNumber = int(rand() * 100) + 1 Lua：number = math.random(1,100) 循环进行直到我猜出该随机数字循环通常是用控制流程来实现的，如 while 或 do-while。JavaScript 中的实现没有使用循环，而是 “实时 ”更新 HTML 页面，直到用户猜出正确的数字。Awk 虽然支持循环，但是通过循环读取输入信息是没有意义的，因为 Awk 是基于数据管道的，所以它从文件而不是直接从用户读取输入信息。C：do { … } while (guess != number); C++：do { … } while ( number != guess ); Rust：for line in std::io::stdin().lock().lines() { … break; } Java：while ( guess != NUMBER ) { … } Groovy：while ( … ) { … break; } Lua：while ( player.guess ~= number ) do … end 计算机读取我的猜测不同编程语言对输入的处理方式不同。例如，JavaScript 直接从 HTML 表单中读取数值，而 Awk 则从数据管道中读取数据。C：scanf("%d", &guess); C++：cin >> guess; Rust：let parsed = line.ok().as_deref().map(str::parse::<i64>); if let Some(Ok(guess)) = parsed { … } Java：guess = player.nextInt(); Groovy：response = reader.readLine() int guess = response as Integer JavaScript：let myGuess = guess.value Awk：guess = int($0) Lua：player.answer = io.read() player.guess = tonumber(player.answer) 告诉我猜测过高还是过低在这些类 C 语言中，通常是通过 if 语句进行比较的。每种编程语言打印输出的方式有一些变化，但打印语句在每个样本中都是可识别的。C：if (guess < number) { puts("Too low"); } else if (guess > number) { puts("Too high"); } … puts("That's right!"); C++：if ( guess > number) { cout << "Too high.\n" << endl; } else if ( guess < number ) { cout << "Too low.\n" << endl; } else { cout << "That's right!\n" << endl; exit(0); } Rust：_ if guess < random => println!("Too low"), _ if guess > random => println!("Too high"), _ => { println!("That's right"); break; } Java：if ( guess > NUMBER ) { System.out.println("Too high"); } else if ( guess < NUMBER ) { System.out.println("Too low"); } else { System.out.println("That's right!"); System.exit(0); } Groovy：if (guess < randomNumber) print 'too low, try again: ' else if (guess > randomNumber) print 'too high, try again: ' else { println "that's right" break } JavaScript：if (myGuess === randomNumber) { feedback.textContent = "You got it right!" } else if (myGuess > randomNumber) { feedback.textContent = "Your guess was " + myGuess + ". That's too high. Try Again!" } else if (myGuess < randomNumber) { feedback.textContent = "Your guess was " + myGuess + ". That's too low. Try Again!" } Awk：if (guess < randomNumber) { printf "too low, try again:" } else if (guess > randomNumber) { printf "too high, try again:" } else { printf "that's right\n" exit } Lua：if ( player.guess > number ) then print("Too high") elseif ( player.guess < number) then print("Too low") else print("That's right!") os.exit() end 非类 C 编程语言会怎么样呢？非类 C 编程语言会有很大的不同，需要学习特定的语法来完成每一步。Racket 源于 Lisp 和 Scheme，所以它使用 Lisp 的前缀符和大量括号。Python 使用空格而不是括号来表示循环之类的块。Elixir 是一种函数式编程语言，有自己的语法。Bash 是基于 Unix 系统中的 Bourne shell，它本身借鉴了 Algol68，并支持额外的速记符，如 && 作为 and 的变体。Fortran 是在使用打孔卡片输入代码的时期创建的，所以它依赖于一些重要列的 80 列布局。我将通过比较 if 语句，举例表现这些编程语言的不同。if 判断一个值是否小于或大于另一个值，并向用户打印适当信息。Racket：(cond [(> number guess) (displayln "Too low") (inquire-user number)] [(< number guess) (displayln "Too high") (inquire-user number)] [else (displayln "Correct!")])) Python：if guess < random: print("Too low") elif guess > random: print("Too high") else: print("That's right!") Elixir：cond do guess < num -> IO.puts "Too low!" guess_loop(num) guess > num -> IO.puts "Too high!" guess_loop(num) true -> IO.puts "That's right!" end Bash：[ "0$guess" -lt $number ] && echo "Too low" [ "0$guess" -gt $number ] && echo "Too high" Fortran：IF (GUESS.LT.NUMBER) THEN PRINT *, 'TOO LOW' ELSE IF (GUESS.GT.NUMBER) THEN PRINT *, 'TOO HIGH' ENDIF 更多当你在学习一种新的编程语言时 “猜数字” 游戏是一个很友好的入门程序，通过一种简单的方式练习了几个常见的编程概念。通过不同编程语言实现这个简单游戏，你可以理解一些核心概念和每种语言的细节。学习如何用 C 和类 C 语言编写 “猜数字” 游戏：C， Jim HallC++， Seth KenlonRust， Moshe ZadkaJava， Seth KenlonGroovy， Chris HermansenJavaScript， Mandy Kendallawk， Chris HermansenLua， Seth Kenlon其他语言：Racket， Cristiano L. FontanaPython， Moshe ZadkaElixir， Moshe ZadkaBash， Jim HallFortran， Jim Hallvia: https://opensource.com/article/21/4/compare-programming-languages作者：Jim Hall 选题：lujun9972 译者：VeryZZJ 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #536 AI 参加编程竞赛，成绩超过了一半的程序员
AI 参加编程竞赛，成绩超过了一半的程序员DeepMind 刚刚宣布了其最新打造的 AlphaCode 编程 AI。它参加了 Codeforces 的编程挑战，这是一个极具挑战性的编程平台，通过类似于国际象棋的 Elo 评级系统来分享每周问题和相关排名。最终 AlphaCode 针对 Codeforces 网站上 5000 名用户解决的十个挑战项目开展了测试，且拿下了平均名次位于前 54.3% 的成绩。DeepMind 估计该系统的 Codeforces Elo 为 1238，位居过去 6 个月在该网站上发起挑战的用户榜单的前 28% 。老王点评：看来至少有一半的程序员可以考虑改行了。树莓派操作系统发布 64 位版本虽然 2016 年发布的树莓派 3 及后来的产品已经带有 64 位 CPU，甚至树莓派 Zero 2 也具有 64 位功能，但官方提供的树莓派操作系统一直是 32 位的。不过，64 位的操作系统可以支持更多内存，较新的指令集也有性能优势，此外，一些基于 Arm 的商业/二进制分发软件只支持 AArch64，因此树莓派基金会决定提供 64 位的树莓派操作系统。老王点评：终于升级到 64 位了，虽然之前可以使用其他的 64 位操作系统。Oracle Linux 出现在 Windows 商店Oracle Linux 8.5 是 CentOS 8.5 的衍生发行版，以 WSL 的形式出现在 Windows 商店，这是红帽家族的发行版 首次 出现在 Windows 商店，尽管 Oracle Linux 不是红帽的。在 Windows 商店中，如果你搜索 Linux，你会发现 Ubuntu、SUSE/openSUSE、Debian、Kali、Alpine 等等，这都是为 WSL 开发的。但你不会看到 Fedora、CentOS、RHEL 之类的红帽系的 Linux，虽然有人为 WSL 中制作了 Fedora remix。老王点评：我就说 Oracle Linux 是叛徒，对吧。:D
借助开源的力量，任何人都可以编程。找到一个你想做的项目，并让它成为你进入编程的第一个项目。任何人都可以开始学习编程。我们都是从某个起点开始的，而且你不需要有计算机科学背景就可以学习编程。这就是 Linux 和开源的力量：任何人都可以学习一点编程。如果你想学习一种新的编程语言，我们有几篇可以让你起步的很棒的文章。下面是我们最受欢迎的几篇文章，它们可以帮助到你。比较编程语言大多数编程语言都有某些相似之处。当你知道如何用一种编程语言做一件事，学习下一种编程语言主要是弄清其语法和结构。不同的编程语言如何做同样的事情学习一种新的编程语言的一个好方法是写一个简单的测试程序，如一个游戏，以探索该编程语言的工作原理。我经常写的一个示例程序是一个简单的“猜数字”游戏，即计算机在 1 到 100 之间挑选一个数字，让我猜出来。今年早些时候，我们发表了 一系列文章，探讨如何用几种编程语言编写猜数字游戏。了解这些不同的编程语言如何实现“猜数字”游戏的主要步骤。不同的编程语言如何读写数据Alan 的文章比较了不同的编程语言如何在相同的思想下 读写数据。无论这些数据是来自于配置文件还是用户创建的文件，在存储设备上处理数据对于编码者来说是很常见的。Alan 的比较文章提供了对几种流行的编程语言，如 C、Java、Groovy 和其他语言所采取的不同方法的深入了解。学习一种新的编程语言无论你是想学习一种新的编程语言，还是想探索一种现有的编程语言，请看看下面这些关于学习编程的好文章。如何用 WebAssembly 编写 “Hello World”WebAssembly 是一种字节码格式，几乎所有的浏览器都可以将其编译为主机系统的机器代码。与 JavaScript 和 WebGL 一起，WebAssembly 满足了将应用移植到网络浏览器中独立使用的需求。Stephan 解释了如何用 WASM-text 创建经典的 Hello World 程序。用 Golang 轻松实现交叉编译Gaurav 写了关于通过将脚本转换为 Go 程序来学习 Go 的交叉编译支持。你可以一次写好你的程序，并通过交叉编译为另一个环境进行编译。为什么我使用 D 编程语言来编写脚本D 编程语言由于其静态类型和元编程能力，常常被认为是一种系统编程语言。然而，它也是一种非常高效的脚本语言。Lawrence 写了关于如何利用 D 编程语言进行 普通的脚本编写。借助于开源的力量，编程可以被任何人所接受。找到一个你想做的项目，并让它成为你进入编程的第一个项目。via: https://opensource.com/article/22/1/learn-programming作者：Jim Hall 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #507 新的插件可以防止从网上复制代码
新的插件可以防止从网上复制代码JetBrains 发布了一个 AntiCopyPaster 开源插件，可以防止复制和粘贴代码。研究人员观察到，虽然“复制和粘贴构成了编写代码的一个重要部分”，但这样做会导致代码维护、安全问题和许可证问题。由于开发者倾向于抄袭，有很多重复的代码在四处飘荡。截至 2017 年，GitHub 上约有 70% 的代码 来自复制的文件。AntiCopyPaster 会监控 IDE 中的粘贴代码，并扫描目标文件中的 Java 方法以找出重复的内容。该插件在粘贴操作后会等待一段时间，以允许对复制的代码进行编辑。只有当克隆的代码没有被改变时，该插件才会继续检查粘贴的片段是否原样复制的代码。老王点评：看来以后 ICP 程序员难混咯。阿里巴巴为 Linux 内核调度器提出组平衡器概念越来越多的组织为了发展云计算等业务而配置他们的服务器在应用程序之间共享 CPU 核心/资源，而不是将某个 CPU 核心专门分配给单个应用程序/任务。阿里巴巴为 Linux 内核调度器提出一个新的“组平衡器”概念，以提升系统资源利用。这个平衡器的重点是在各组 CPU 核心之间平衡各组任务。这是一种缓解共享模式下冲突的方法，使组尽可能地排他，以获得性能和资源效率。在一台 128 核 CPU 服务器上的基准测试发现，与标准共享模式相比，Redis 在组平衡器模式下可以提升 2~10% 的性能。老王点评：现在越来越多的看到了中国开发者在内核前沿做出更深入的探索和研究。夸大其词的 CleanCache 补丁将在十年后从 Linux 内核中删除CleanCache 是由 Oracle 开发的，在推出时他们宣称它是“非常酷的东西，有巨大的潜力超级优化运行的虚拟机的性能，是相当多的研究和实验的结果。”它有可能在许多环境中以可忽略的成本大幅提高许多工作负载的页面缓存效率。十年后的今天，CleanCache 已经没有任何内核代码在使用它们了，因此，它很可能在 Linux 5.17 中 被主线移除。老王点评：这种新陈代谢挺好，要不 Linux 内核越来越臃肿了。
了解有关内存安全和效率的更多信息。C 是一种高级语言，同时具有“ 接近金属 close-to-the-metal ”（LCTT 译注：即“接近人类思维方式”的反义词）的特性，这使得它有时看起来更像是一种可移植的汇编语言，而不像 Java 或 Python 这样的兄弟语言。内存管理作为上述特性之一，涵盖了正在执行的程序对内存的安全和高效使用。本文通过 C 语言代码示例，以及现代 C 语言编译器生成的汇编语言代码段，详细介绍了内存安全性和效率。尽管代码示例是用 C 语言编写的，但安全高效的内存管理指南对于 C++ 是同样适用的。这两种语言在很多细节上有所不同（例如，C++ 具有 C 所缺乏的面向对象特性和泛型），但在内存管理方面面临的挑战是一样的。执行中程序的内存概述对于正在执行的程序（又名 进程 process ），内存被划分为三个区域： 栈 stack 、 堆 heap 和 静态区 static area 。下文会给出每个区域的概述，以及完整的代码示例。作为通用 CPU 寄存器的替补，栈 为代码块（例如函数或循环体）中的局部变量提供暂存器存储。传递给函数的参数在此上下文中也视作局部变量。看一下下面这个简短的示例：void some_func(int a, int b) { int n; ... } 通过 a 和 b 传递的参数以及局部变量 n 的存储会在栈中，除非编译器可以找到通用寄存器。编译器倾向于优先将通用寄存器用作暂存器，因为 CPU 对这些寄存器的访问速度很快（一个时钟周期）。然而，这些寄存器在台式机、笔记本电脑和手持机器的标准架构上很少（大约 16 个）。在只有汇编语言程序员才能看到的实施层面，栈被组织为具有 push（插入）和 pop（删除）操作的 LIFO（后进先出）列表。 top 指针可以作为偏移的基地址；这样，除了 top 之外的栈位置也变得可访问了。例如，表达式 top+16 指向堆栈的 top 指针上方 16 个字节的位置，表达式 top-16 指向 top 指针下方 16 个字节的位置。因此，可以通过 top 指针访问实现了暂存器存储的栈的位置。在标准的 ARM 或 Intel 架构中，栈从高内存地址增长到低内存地址；因此，减小某进程的 top 就是增大其栈规模。使用栈结构就意味着轻松高效地使用内存。编译器（而非程序员）会编写管理栈的代码，管理过程通过分配和释放所需的暂存器存储来实现；程序员声明函数参数和局部变量，将实现过程交给编译器。此外，完全相同的栈存储可以在连续的函数调用和代码块（如循环）中重复使用。精心设计的模块化代码会将栈存储作为暂存器的首选内存选项，同时优化编译器要尽可能使用通用寄存器而不是栈。堆 提供的存储是通过程序员代码显式分配的，堆分配的语法因语言而异。在 C 中，成功调用库函数 malloc（或其变体 calloc 等）会分配指定数量的字节（在 C++ 和 Java 等语言中，new 运算符具有相同的用途）。编程语言在如何释放堆分配的存储方面有着巨大的差异：在 Java、Go、Lisp 和 Python 等语言中，程序员不会显式释放动态分配的堆存储。例如，下面这个 Java 语句为一个字符串分配了堆存储，并将这个堆存储的地址存储在变量 greeting 中：String greeting = new String("Hello, world!"); Java 有一个垃圾回收器，它是一个运行时实用程序，如果进程无法再访问自己分配的堆存储，回收器可以使其自动释放。因此，Java 堆释放是通过垃圾收集器自动进行的。在上面的示例中，垃圾收集器将在变量 greeting 超出作用域后，释放字符串的堆存储。Rust 编译器会编写堆释放代码。这是 Rust 在不依赖垃圾回收器的情况下，使堆释放实现自动化的开创性努力，但这也会带来运行时复杂性和开销。向 Rust 的努力致敬！在 C（和 C++）中，堆释放是程序员的任务。程序员调用 malloc 分配堆存储，然后负责相应地调用库函数 free 来释放该存储空间（在 C++ 中，new 运算符分配堆存储，而 delete 和 delete[] 运算符释放此类存储）。下面是一个 C 语言代码示例：char* greeting = malloc(14); /* 14 heap bytes */ strcpy(greeting, "Hello, world!"); /* copy greeting into bytes */ puts(greeting); /* print greeting */ free(greeting); /* free malloced bytes */ C 语言避免了垃圾回收器的成本和复杂性，但也不过是让程序员承担了堆释放的任务。内存的 静态区 为可执行代码（例如 C 语言函数）、字符串文字（例如“Hello, world!”）和全局变量提供存储空间：int n; /* global variable */ int main() { /* function */ char* msg = "No comment"; /* string literal */ ... } 该区域是静态的，因为它的大小从进程执行开始到结束都固定不变。由于静态区相当于进程固定大小的内存占用，因此经验法则是通过避免使用全局数组等方法来使该区域尽可能小。下文会结合代码示例对本节概述展开进一步讲解。栈存储想象一个有各种连续执行的任务的程序，任务包括了处理每隔几分钟通过网络下载并存储在本地文件中的数字数据。下面的 stack 程序简化了处理流程（仅是将奇数整数值转换为偶数），而将重点放在栈存储的好处上。#include <stdio.h> #include <stdlib.h> #define Infile "incoming.dat" #define Outfile "outgoing.dat" #define IntCount 128000 /* 128,000 */ void other_task1() { /*...*/ } void other_task2() { /*...*/ } void process_data(const char* infile, const char* outfile, const unsigned n) { int nums[n]; FILE* input = fopen(infile, "r"); if (NULL == infile) return; FILE* output = fopen(outfile, "w"); if (NULL == output) { fclose(input); return; } fread(nums, n, sizeof(int), input); /* read input data */ unsigned i; for (i = 0; i < n; i++) { if (1 == (nums[i] & 0x1)) /* odd parity? */ nums[i]--; /* make even */ } fclose(input); /* close input file */ fwrite(nums, n, sizeof(int), output); fclose(output); } int main() { process_data(Infile, Outfile, IntCount); /** now perform other tasks **/ other_task1(); /* automatically released stack storage available */ other_task2(); /* ditto */ return 0; } 底部的 main 函数首先调用 process_data 函数，该函数会创建一个基于栈的数组，其大小由参数 n 给定（当前示例中为 128,000）。因此，该数组占用 128000 * sizeof(int) 个字节，在标准设备上达到了 512,000 字节（int 在这些设备上是四个字节）。然后数据会被读入数组（使用库函数 fread），循环处理，并保存到本地文件 outgoing.dat（使用库函数 fwrite）。当 process_data 函数返回到其调用者 main 函数时，process_data 函数的大约 500MB 栈暂存器可供 stack 程序中的其他函数用作暂存器。在此示例中，main 函数接下来调用存根函数 other_task1 和 other_task2。这三个函数在 main 中依次调用，这意味着所有三个函数都可以使用相同的堆栈存储作为暂存器。因为编写栈管理代码的是编译器而不是程序员，所以这种方法对程序员来说既高效又容易。在 C 语言中，在块（例如函数或循环体）内定义的任何变量默认都有一个 auto 存储类，这意味着该变量是基于栈的。存储类 register 现在已经过时了，因为 C 编译器会主动尝试尽可能使用 CPU 寄存器。只有在块内定义的变量可能是 register，如果没有可用的 CPU 寄存器，编译器会将其更改为 auto。基于栈的编程可能是不错的首选方式，但这种风格确实有一些挑战性。下面的 badStack 程序说明了这点。#include <stdio.h>; const int* get_array(const unsigned n) { int arr[n]; /* stack-based array */ unsigned i; for (i = 0; i < n; i++) arr[i] = 1 + 1; return arr; /** ERROR **/ } int main() { const unsigned n = 16; const int* ptr = get_array(n); unsigned i; for (i = 0; i < n; i++) printf("%i ", ptr[i]); puts("\n"); return 0; } badStack 程序中的控制流程很简单。main 函数使用 16（LCTT 译注：原文为 128，应为作者笔误）作为参数调用函数 get_array，然后被调用函数会使用传入参数来创建对应大小的本地数组。get_array 函数会初始化数组并返回给 main 中的数组标识符 arr。 arr 是一个指针常量，保存数组的第一个 int 元素的地址。当然，本地数组 arr 可以在 get_array 函数中访问，但是一旦 get_array 返回，就不能合法访问该数组。尽管如此，main 函数会尝试使用函数 get_array 返回的堆栈地址 arr 来打印基于栈的数组。现代编译器会警告错误。例如，下面是来自 GNU 编译器的警告：badStack.c: In function 'get_array': badStack.c:9:10: warning: function returns address of local variable [-Wreturn-local-addr] return arr; /** ERROR **/ 一般规则是，如果使用栈存储实现局部变量，应该仅在该变量所在的代码块内，访问这块基于栈的存储（在本例中，数组指针 arr 和循环计数器 i 均为这样的局部变量）。因此，函数永远不应该返回指向基于栈存储的指针。堆存储接下来使用若干代码示例凸显在 C 语言中使用堆存储的优点。在第一个示例中，使用了最优方案分配、使用和释放堆存储。第二个示例（在下一节中）将堆存储嵌套在了其他堆存储中，这会使其释放操作变得复杂。#include <stdio.h> #include <stdlib.h> int* get_heap_array(unsigned n) { int* heap_nums = malloc(sizeof(int) * n); unsigned i; for (i = 0; i < n; i++) heap_nums[i] = i + 1; /* initialize the array */ /* stack storage for variables heap_nums and i released automatically when get_num_array returns */ return heap_nums; /* return (copy of) the pointer */ } int main() { unsigned n = 100, i; int* heap_nums = get_heap_array(n); /* save returned address */ if (NULL == heap_nums) /* malloc failed */ fprintf(stderr, "%s\n", "malloc(...) failed..."); else { for (i = 0; i < n; i++) printf("%i\n", heap_nums[i]); free(heap_nums); /* free the heap storage */ } return 0; } 上面的 heap 程序有两个函数： main 函数使用参数（示例中为 100）调用 get_heap_array 函数，参数用来指定数组应该有多少个 int 元素。因为堆分配可能会失败，main 函数会检查 get_heap_array 是否返回了 NULL；如果是，则表示失败。如果分配成功，main 将打印数组中的 int 值，然后立即调用库函数 free 来对堆存储解除分配。这就是最优的方案。get_heap_array 函数以下列语句开头，该语句值得仔细研究一下：int* heap_nums = malloc(sizeof(int) * n); /* heap allocation */ malloc 库函数及其变体函数针对字节进行操作；因此，malloc 的参数是 n 个 int 类型元素所需的字节数（sizeof(int) 在标准现代设备上是四个字节）。malloc 函数返回所分配字节段的首地址，如果失败则返回 NULL .如果成功调用 malloc，在现代台式机上其返回的地址大小为 64 位。在手持设备和早些时候的台式机上，该地址的大小可能是 32 位，或者甚至更小，具体取决于其年代。堆分配数组中的元素是 int 类型，这是一个四字节的有符号整数。这些堆分配的 int 的地址存储在基于栈的局部变量 heap_nums 中。可以参考下图： heap-based stack-based / \ +----+----+ +----+ heap-nums--->|int1|int2|...|intN| +----+----+ +----+ 一旦 get_heap_array 函数返回，指针变量 heap_nums 的栈存储将自动回收——但动态 int 数组的堆存储仍然存在，这就是 get_heap_array 函数返回这个地址（的副本）给 main 函数的原因：它现在负责在打印数组的整数后，通过调用库函数 free 显式释放堆存储：free(heap_nums); /* free the heap storage */ malloc 函数不会初始化堆分配的存储空间，因此里面是随机值。相比之下，其变体函数 calloc 会将分配的存储初始化为零。这两个函数都返回 NULL 来表示分配失败。在 heap 示例中，main 函数在调用 free 后会立即返回，正在执行的程序会终止，这会让系统回收所有已分配的堆存储。尽管如此，程序员应该养成在不再需要时立即显式释放堆存储的习惯。嵌套堆分配下一个代码示例会更棘手一些。C 语言有很多返回指向堆存储的指针的库函数。下面是一个常见的使用情景：1、C 程序调用一个库函数，该函数返回一个指向基于堆的存储的指针，而指向的存储通常是一个聚合体，如数组或结构体：SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */ 2、 然后程序使用所分配的存储。3、 对于清理而言，问题是对 free 的简单调用是否会清理库函数分配的所有堆分配存储。例如，SomeStructure 实例可能有指向堆分配存储的字段。一个特别麻烦的情况是动态分配的结构体数组，每个结构体有一个指向又一层动态分配的存储的字段。下面的代码示例说明了这个问题，并重点关注了如何设计一个可以安全地为客户端提供堆分配存储的库。#include <stdio.h> #include <stdlib.h> typedef struct { unsigned id; unsigned len; float* heap_nums; } HeapStruct; unsigned structId = 1; HeapStruct* get_heap_struct(unsigned n) { /* Try to allocate a HeapStruct. */ HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ /* Try to allocate floating-point aggregate within HeapStruct. */ heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* then return NULL */ } /* Success: set fields */ heap_struct->id = structId++; heap_struct->len = n; return heap_struct; /* return pointer to allocated HeapStruct */ } void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } int main() { const unsigned n = 100; HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */ /* Do some (meaningless) work for demo. */ unsigned i; for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i; for (i = 0; i < n; i += 10) printf("%12f\n", hs->heap_nums[i]); free_all(hs); /* free dynamically allocated storage */ return 0; } 上面的 nestedHeap 程序示例以结构体 HeapStruct 为中心，结构体中又有名为 heap_nums 的指针字段：typedef struct { unsigned id; unsigned len; float* heap_nums; /** pointer **/ } HeapStruct; 函数 get_heap_struct 尝试为 HeapStruct 实例分配堆存储，这需要为字段 heap_nums 指向的若干个 float 变量分配堆存储。如果成功调用 get_heap_struct 函数，并将指向堆分配结构体的指针以 hs 命名，其结果可以描述如下：hs-->HeapStruct instance id len heap_nums-->N contiguous float elements 在 get_heap_struct 函数中，第一个堆分配过程很简单：HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ sizeof(HeapStruct) 包括了 heap_nums 字段的字节数（32 位机器上为 4，64 位机器上为 8），heap_nums 字段则是指向动态分配数组中的 float 元素的指针。那么，问题关键在于 malloc 为这个结构体传送了字节空间还是表示失败的 NULL；如果是 NULL，get_heap_struct 函数就也返回 NULL 以通知调用者堆分配失败。第二步尝试堆分配的过程更复杂，因为在这一步，HeapStruct 的堆存储已经分配好了：heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* and then return NULL */ } 传递给 get_heap_struct 函数的参数 n 指明动态分配的 heap_nums 数组中应该有多少个 float 元素。如果可以分配所需的若干个 float 元素，则该函数在返回 HeapStruct 的堆地址之前会设置结构的 id 和 len 字段。 但是，如果尝试分配失败，则需要两个步骤来实现最优方案：1、 必须释放 HeapStruct 的存储以避免内存泄漏。对于调用 get_heap_struct 的客户端函数而言，没有动态 heap_nums 数组的 HeapStruct 可能就是没用的；因此，HeapStruct 实例的字节空间应该显式释放，以便系统可以回收这些空间用于未来的堆分配。2、 返回 NULL 以标识失败。如果成功调用 get_heap_struct 函数，那么释放堆存储也很棘手，因为它涉及要以正确顺序进行的两次 free 操作。因此，该程序设计了一个 free_all 函数，而不是要求程序员再去手动实现两步释放操作。回顾一下，free_all 函数是这样的：void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } 检查完参数 heap_struct 不是 NULL 值后，函数首先释放 heap_nums 数组，这步要求 heap_struct 指针此时仍然是有效的。先释放 heap_struct 的做法是错误的。一旦 heap_nums 被释放，heap_struct 就可以释放了。如果 heap_struct 被释放，但 heap_nums 没有被释放，那么数组中的 float 元素就会泄漏：仍然分配了字节空间，但无法被访问到——因此一定要记得释放 heap_nums。存储泄漏将一直持续，直到 nestedHeap 程序退出，系统回收泄漏的字节时为止。关于 free 库函数的注意事项就是要有顺序。回想一下上面的调用示例：free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ 这些调用释放了分配的存储空间——但它们并 不是 将它们的操作参数设置为 NULL（free 函数会获取地址的副本作为参数；因此，将副本更改为 NULL 并不会改变原地址上的参数值）。例如，在成功调用 free 之后，指针 heap_struct 仍然持有一些堆分配字节的堆地址，但是现在使用这个地址将会产生错误，因为对 free 的调用使得系统有权回收然后重用这些分配过的字节。使用 NULL 参数调用 free 没有意义，但也没有什么坏处。而在非 NULL 的地址上重复调用 free 会导致不确定结果的错误：free(heap_struct); /* 1st call: ok */ free(heap_struct); /* 2nd call: ERROR */ 内存泄漏和堆碎片化“内存泄漏”是指动态分配的堆存储变得不再可访问。看一下相关的代码段：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ nums = malloc(sizeof(float) * 25); /* 25 new floats */ 假如第一个 malloc 成功，第二个 malloc 会再将 nums 指针重置为 NULL（分配失败情况下）或是新分配的 25 个 float 中第一个的地址。最初分配的 10 个 float 元素的堆存储仍然处于被分配状态，但此时已无法再对其访问，因为 nums 指针要么指向别处，要么是 NULL。结果就是造成了 40 个字节（sizeof(float) * 10）的泄漏。在第二次调用 malloc 之前，应该释放最初分配的存储空间：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ free(nums); /** good **/ nums = malloc(sizeof(float) * 25); /* no leakage */ 即使没有泄漏，堆也会随着时间的推移而碎片化，需要对系统进行碎片整理。例如，假设两个最大的堆块当前的大小分别为 200MB 和 100MB。然而，这两个堆块并不连续，进程 P 此时又需要分配 250MB 的连续堆存储。在进行分配之前，系统可能要对堆进行 碎片整理 以给 P 提供 250MB 连续存储空间。碎片整理很复杂，因此也很耗时。内存泄漏会创建处于已分配状态但不可访问的堆块，从而会加速碎片化。因此，释放不再需要的堆存储是程序员帮助减少碎片整理需求的一种方式。诊断内存泄漏的工具有很多工具可用于分析内存效率和安全性，其中我最喜欢的是 valgrind。为了说明该工具如何处理内存泄漏，这里给出 leaky 示例程序：#include <stdio.h> #include <stdlib.h> int* get_ints(unsigned n) { int* ptr = malloc(n * sizeof(int)); if (ptr != NULL) { unsigned i; for (i = 0; i < n; i++) ptr[i] = i + 1; } return ptr; } void print_ints(int* ptr, unsigned n) { unsigned i; for (i = 0; i < n; i++) printf("%3i\n", ptr[i]); } int main() { const unsigned n = 32; int* arr = get_ints(n); if (arr != NULL) print_ints(arr, n); /** heap storage not yet freed... **/ return 0; } main 函数调用了 get_ints 函数，后者会试着从堆中 malloc 32 个 4 字节的 int，然后初始化动态数组（如果 malloc 成功）。初始化成功后，main 函数会调用 print_ints函数。程序中并没有调用 free 来对应 malloc 操作；因此，内存泄漏了。如果安装了 valgrind 工具箱，下面的命令会检查 leaky 程序是否存在内存泄漏（% 是命令行提示符）：% valgrind --leak-check=full ./leaky 绝大部分输出都在下面给出了。左边的数字 207683 是正在执行的 leaky 程序的进程标识符。这份报告给出了泄漏发生位置的详细信息，本例中位置是在 main 函数所调用的 get_ints 函数中对 malloc 的调用处。==207683== HEAP SUMMARY: ==207683== in use at exit: 128 bytes in 1 blocks ==207683== total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated ==207683== ==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==207683== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==207683== by 0x109186: get_ints (in /home/marty/gc/leaky) ==207683== by 0x109236: main (in /home/marty/gc/leaky) ==207683== ==207683== LEAK SUMMARY: ==207683== definitely lost: 128 bytes in 1 blocks ==207683== indirectly lost: 0 bytes in 0 blocks ==207683== possibly lost: 0 bytes in 0 blocks ==207683== still reachable: 0 bytes in 0 blocks ==207683== suppressed: 0 bytes in 0 blocks 如果把 main 函数改成在对 print_ints 的调用之后，再加上一个对 free 的调用，valgrind 就会对 leaky 程序给出一个干净的内存健康清单：==218462== All heap blocks were freed -- no leaks are possible 静态区存储在正统的 C 语言中，函数必须在所有块之外定义。这是一些 C 编译器支持的特性，杜绝了在另一个函数体内定义一个函数的可能。我举的例子都是在所有块之外定义的函数。这样的函数要么是 static ，即静态的，要么是 extern，即外部的，其中 extern 是默认值。C 语言中，以 static 或 extern 修饰的函数和变量驻留在内存中所谓的 静态区 中，因为在程序执行期间该区域大小是固定不变的。这两个存储类型的语法非常复杂，我们应该回顾一下。在回顾之后，会有一个完整的代码示例来生动展示语法细节。在所有块之外定义的函数或变量默认为 extern；因此，函数和变量要想存储类型为 static ，必须显式指定：/** file1.c: outside all blocks, five definitions **/ int foo(int n) { return n * 2; } /* extern by default */ static int bar(int n) { return n; } /* static */ extern int baz(int n) { return -n; } /* explicitly extern */ int num1; /* extern */ static int num2; /* static */ extern 和 static 的区别在于作用域：extern 修饰的函数或变量可以实现跨文件可见（需要声明）。相比之下，static 修饰的函数仅在 定义 该函数的文件中可见，而 static 修饰的变量仅在 定义 该变量的文件（或文件中的块）中可见：static int n1; /* scope is the file */ void func() { static int n2; /* scope is func's body */ ... } 如果在所有块之外定义了 static 变量，例如上面的 n1，该变量的作用域就是定义变量的文件。无论在何处定义 static 变量，变量的存储都在内存的静态区中。extern 函数或变量在给定文件中的所有块之外定义，但这样定义的函数或变量也可以在其他文件中声明。典型的做法是在头文件中 声明 这样的函数或变量，只要需要就可以包含进来。下面这些简短的例子阐述了这些棘手的问题。假设 extern 函数 foo 在 file1.c 中 定义，有无关键字 extern 效果都一样：/** file1.c **/ int foo(int n) { return n * 2; } /* definition has a body {...} */ 必须在其他文件（或其中的块）中使用显式的 extern 声明 此函数才能使其可见。以下是使 extern 函数 foo 在文件 file2.c 中可见的声明语句：/** file2.c: make function foo visible here **/ extern int foo(int); /* declaration (no body) */ 回想一下，函数声明没有用大括号括起来的主体，而函数定义会有这样的主体。为了便于查看，函数和变量声明通常会放在头文件中。准备好需要声明的源代码文件，然后就可以 #include 相关的头文件。下一节中的 staticProg 程序演示了这种方法。至于 extern 的变量，规则就变得更棘手了（很抱歉增加了难度！）。任何 extern 的对象——无论函数或变量——必须 定义 在所有块之外。此外，在所有块之外定义的变量默认为 extern：/** outside all blocks **/ int n; /* defaults to extern */ 但是，只有在变量的 定义 中显式初始化变量时，extern 才能在变量的 定义 中显式修饰（LCTT 译注：换言之，如果下列代码中的 int n1; 行前加上 extern，该行就由 定义 变成了 声明）：/** file1.c: outside all blocks **/ int n1; /* defaults to extern, initialized by compiler to zero */ extern int n2 = -1; /* ok, initialized explicitly */ int n3 = 9876; /* ok, extern by default and initialized explicitly */ 要使在 file1.c 中定义为 extern 的变量在另一个文件（例如 file2.c）中可见，该变量必须在 file2.c 中显式 声明 为 extern 并且不能初始化（初始化会将声明转换为定义）：/** file2.c **/ extern int n1; /* declaration of n1 defined in file1.c */ 为了避免与 extern 变量混淆，经验是在 声明 中显式使用 extern（必须），但不要在 定义 中使用（非必须且棘手）。对于函数，extern 在定义中是可选使用的，但在声明中是必须使用的。下一节中的 staticProg 示例会把这些点整合到一个完整的程序中。staticProg 示例staticProg 程序由三个文件组成：两个 C 语言源文件（static1.c 和 static2.c）以及一个头文件（static.h），头文件中包含两个声明：/** header file static.h **/ #define NumCount 100 /* macro */ extern int global_nums[NumCount]; /* array declaration */ extern void fill_array(); /* function declaration */ 两个声明中的 extern，一个用于数组，另一个用于函数，强调对象在别处（“外部”）定义：数组 global_nums 在文件 static1.c 中定义（没有显式的 extern），函数 fill_array 在文件 static2.c 中定义（也没有显式的 extern）。每个源文件都包含了头文件 static.h。static1.c 文件定义了两个驻留在内存静态区域中的数组（global_nums 和 more_nums）。第二个数组有 static 修饰，这将其作用域限制为定义数组的文件 (static1.c)。如前所述， extern 修饰的 global_nums 则可以实现在多个文件中可见。/** static1.c **/ #include <stdio.h> #include <stdlib.h> #include "static.h" /* declarations */ int global_nums[NumCount]; /* definition: extern (global) aggregate */ static int more_nums[NumCount]; /* definition: scope limited to this file */ int main() { fill_array(); /** defined in file static2.c **/ unsigned i; for (i = 0; i < NumCount; i++) more_nums[i] = i * -1; /* confirm initialization worked */ for (i = 0; i < NumCount; i += 10) printf("%4i\t%4i\n", global_nums[i], more_nums[i]); return 0; } 下面的 static2.c 文件中定义了 fill_array 函数，该函数由 main（在 static1.c 文件中）调用；fill_array 函数会给名为 global_nums 的 extern 数组中的元素赋值，该数组在文件 static1.c 中定义。使用两个文件的唯一目的是凸显 extern 变量或函数能够跨文件可见。/** static2.c **/ #include "static.h" /** declarations **/ void fill_array() { /** definition **/ unsigned i; for (i = 0; i < NumCount; i++) global_nums[i] = i + 2; } staticProg 程序可以用如下编译：% gcc -o staticProg static1.c static2.c 从汇编语言看更多细节现代 C 编译器能够处理 C 和汇编语言的任意组合。编译 C 源文件时，编译器首先将 C 代码翻译成汇编语言。这是对从上文 static1.c 文件生成的汇编语言进行保存的命令：% gcc -S static1.c 生成的文件就是 static1.s。这是文件顶部的一段代码，额外添加了行号以提高可读性： .file "static1.c" ## line 1 .text ## line 2 .comm global_nums,400,32 ## line 3 .local more_nums ## line 4 .comm more_nums,400,32 ## line 5 .section .rodata ## line 6 .LC0: ## line 7 .string "%4i\t%4i\n" ## line 8 .text ## line 9 .globl main ## line 10 .type main, @function ## line 11 main: ## line 12 ... 诸如 .file（第 1 行）之类的汇编语言指令以句点开头。顾名思义，指令会指导汇编程序将汇编语言翻译成机器代码。.rodata 指令（第 6 行）表示后面是只读对象，包括字符串常量 "%4i\t%4i\n"（第 8 行），main 函数（第 12 行）会使用此字符串常量来实现格式化输出。作为标签引入（通过末尾的冒号实现）的 main 函数（第 12 行），同样也是只读的。在汇编语言中，标签就是地址。标签 main:（第 12 行）标记了 main 函数代码开始的地址，标签 .LC0:（第 7 行）标记了格式化字符串开头所在的地址。global_nums（第 3 行）和 more_nums（第 4 行）数组的定义包含了两个数字：400 是每个数组中的总字节数，32 是每个数组（含 100 个 int 元素）中每个元素的比特数。（第 5 行中的 .comm 指令表示 common name，可以忽略。）两个数组定义的不同之处在于 more_nums 被标记为 .local（第 4 行），这意味着其作用域仅限于其所在文件 static1.s。相比之下，global_nums 数组就能在多个文件中实现可见，包括由 static1.c 和 static2.c 文件翻译成的汇编文件。最后，.text 指令在汇编代码段中出现了两次（第 2 行和第 9 行）。术语“text”表示“只读”，但也会涵盖一些读/写变量，例如两个数组中的元素。尽管本文展示的汇编语言是针对 Intel 架构的，但 Arm6 汇编也非常相似。对于这两种架构，.text 区域中的变量（本例中为两个数组中的元素）会自动初始化为零。总结C 语言中的内存高效和内存安全编程准则很容易说明，但可能会很难遵循，尤其是在调用设计不佳的库的时候。准则如下：尽可能使用栈存储，进而鼓励编译器将通用寄存器用作暂存器，实现优化。栈存储代表了高效的内存使用并促进了代码的整洁和模块化。永远不要返回指向基于栈的存储的指针。小心使用堆存储。C（和 C++）中的重难点是确保动态分配的存储尽快解除分配。良好的编程习惯和工具（如 valgrind）有助于攻关这些重难点。优先选用自身提供释放函数的库，例如 nestedHeap 代码示例中的 free_all 释放函数。谨慎使用静态存储，因为这种存储会自始至终地影响进程的内存占用。特别是尽量避免使用 extern 和 static 数组。本文 C 语言代码示例可在我的网站（https://condor.depaul.edu/mkalin）上找到。via: https://opensource.com/article/21/8/memory-programming-c作者：Marty Kalin 选题：lujun9972 译者：unigeorge 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出