(This picture comes from: VTuberized Logos

Info

该笔记的语言部分基本参考程序设计入门——C 语言。课程老师是浙江大学的翁恺老师。B 站上也有视频。

但是 MOOC 平台更推荐 2014 年的 C 语言程序设计以获取更优体验。

本笔记编排较为混乱,但能力有限,积重难返,请见谅。

你的笔记怎么少了很多算法内容?

翁恺老师的课程以语言为主题,与谭书以及依托该书建立的本校课程语言 + 算法混杂的编排有所不同。如果你发现本笔记的算法部分有缺漏,请查找专门的算法教程,比如 Hello 算法

算法不是编程的全部。代码高天在一次直播中表示,只要刷会了 LeetCode 中的经典 150 题,就可以解决工作中 95% 的算法问题。

Lesson 1 什么是程序设计?

1.1 认识程序设计

开门见山地讲,学习本门课程的目的其实是:了解计算机的工作方式,明白计算机处理问题的优劣势。作为计算机学生的第一门专业课,我们并不会期待你写出一个完整的、具有工程结构的软件,更多是就一个抽象(或者简化)的场景下写一个较短的算法程序。

程序设计,其实是设计如何让计算机解决某些问题的步骤,即描述 “How to do”。编程人员使用特殊的编程语言来编写对计算机的指令,告诉计算机如何解决问题。

Info

编程语言按照抽象程度,从低级到高级分为 “机器语言”、“汇编语言(低级语言)” 和 “高级语言”。机器语言使用纯二进制(为了方便会转换成十六进制)编程,汇编语言使用英文助记符编程,高级语言使用接近自然语言的指令编程。C 语言就是一个高级语言 [1]。除非特别说明,否则后文所称 “编程语言” 均指 “高级语言”。

我们用计算机做计算,计算的步骤就是算法 (Algorithm),算法需要编程语言写出来。

下图展示了算法的基本模式:

graph LR
  A[(Input)] --> B[Algorithm]
  B --> C[(Output)]

1.2 计算机如何解决一个方程?

给定一个方程,要求计算机给出方程的解,要如何命令计算机做这件事?

先从算法角度思考一下,计算机可以用什么方法解决方程?

  • 直接开解:计算机表示没有 CAS(计算机代数系统)?!我不会!
  • 枚举:从起始值开始,一个值一个值验证过去。对计算机而言直接粗暴,但对于大规模数据的处理会偏慢
  • 逐步缩小未知数范围(二分法)(撕电话簿)先假设求解结果在某一区间内,然后不断二分。比枚举更快
  • And more…

再从程序角度思考一下,我们怎么将规划好的步骤以机器友好的形式告诉计算机?

  • 解释:用另一个程序去理解用户程序,理解结果由该解释程序直接执行。解释过程不产生任何东西,而只是做动作。下次执行用户程序时,需要再走一遍解释过程。
  • 编译:用另一个程序去翻译用户程序(翻译成更加机器友好的形式),翻译结果由新产生的可执行文件执行。由于编译过程会产生新的程序文件,下次执行用户程序时,不用再走编译过程。

由此编程语言分为两派:解释型语言和编译型语言。一般来讲,C/C++ 属于编译型语言,而 Python 属于解释型语言。随着技术的进步,解释型语言也开始引入编译技术,提前编译部分重要代码以提高性能

Tip

解释语言有特殊的计算能力,而编译语言有确定的运算性能。由于解释语言必须经过中间层的解释程序,因此执行速度较慢。

可以参考 1.2 程序被其他程序翻译成不同的格式


Lesson 2 C 语言简介和历史

C 语言是一种通用的、面向过程 (procedure-oriented) 的编程语言。

面向过程(Procedure Oriented,简称 POP)是以过程为中心的编程范型。该思想强调将问题拆解为具体步骤,通过函数逐步实现并依次调用,数据和操作分离。

一个可能不是很恰当的说明:面向过程关注 “拌” 这个动作。至于什么拌什么、什么能拌什么,如果没有约定好,也可能接受 “意大利面拌 42 号水泥”。想要严格约束,要么函数内部维护条件判断,要么严格化数据类型(“拌” 只能 “拌” 食物,哪些属于食物),在复杂关系下维护成本可能越来越高。

C 诞生于 1969 至 1973 年,与 Unix 操作系统的早期开发同时进行,两者相互成就。1973 年后,为了推广 Unix,开发小组开始聚焦于可移植性和类型安全。可移植性实验的成功使得 Unix 逐渐受到关注。到 20 世纪 80 年代,C 语言广泛传播,其编译器几乎可用于每一种机器架构和操作系统。

C 语言的最初目标是改善编程体验,获取高级语言带来的易于编写和理解等优势,同时性能与汇编媲美。“尽管有些方面对初学者甚至偶尔对熟练者来说神秘,C 仍然是一种简单而小巧的语言,可以用简单而小巧的编译器进行翻译。”(Dennis M. Ritchie, 1993)

历史参考:


Lesson 3 第一个 C 语言程序

3.1 认识你的工具

在开发之前,我们先来了解开发工具吧。

我真的需要知道这些吗?

有的人会说:“我是来敲代码的,你们要干什么?” 认为开发环境设置是一个高级主题,初学者学习到一定程度 “自然而然地” 会接触到,不需要在一开始就介绍。对于他们而言,重要的是 “先让代码跑起来”。其中的一些激进者甚至表示:“初学者不需要本地安装任何开发软件,只需要使用类似于 OnlineGDB 的在线开发工具即可。” 而我也确实找到了一个仅使用 OnlineGDB 的 Python 小课程
你当然可以忽略下面的介绍。Dev-C++ 已经提供了开箱即用的 C/C++ 开发环境,一些教师也鼓励 “不求甚解”,允许学生 “暂时性屏蔽知识”。
但你要确保在之后能够想起来、返回来学习如何配置并适应开发环境。在 B 站学生 UP 主降星驰的视频中,即使在哈尔滨工业大学,大部分大二学生仍然没有完全适应开发环境:课程推荐的 Code::Blocks 的 debug 功能配置难度较高,而他们中没有人知道如何(使用这个 IDE)进行 debug!

很多教程采用的是 Dev-C++ (IDE),由于它自带 GCC 编译器 (虽然过于陈旧……)、开箱即用的特点,常用于教学用途,也是 OIers 的白月光。不过由于其开发时间较早,在功能性上会落后于当前常用的一些开发工具,因此也有开发者开发了 Dev-C++ 的改版,改善了 Dev-C++ 的一些痛点。这里推荐一下一个改版:小熊猫 C++。其自带的 GCC 版本比较新,界面对国内初学者也比较友好。

IDE, Compiler 和 Editor

注意编译器(Compiler)、集成开发环境(Integrated Development Environment, IDE)和编辑器(Editor)不是一个东西。

Well it's an IDE (Integrated Development Environment), it means it integrates different tools and makes them work together. So CB is NOT a compiler (nor MS, nor Borland, nor GCC), it is not a debugger (nor MS, nor GDB), it is not a makefile system !

——How to use a nightly build - codeblock

Windows 平台上的编译器主要有 GCC/G++、LLVM Clang 和 MSVC 。它们是 C 语言开发的核心,是构建程序的主要工具。

集成开发环境主要有 Visual Studio、Clion 以及我们现在用的 Dev-C++。集成开发环境做的是一条龙服务,集成如内置调试等辅助开发的功能。

编辑器主要有 Visual Studio Code、Sublime Text、Vim/NeoVim、GNU Emacs 等。顾名思义,编辑器主业是编写源代码,但一些编辑器经过配置也可以承担 IDE 的工作。


3.2 Hello World

Hello World 来了!下面是示例:

1
2
3
4
5
6
#include <stdio.h>
int main() // "main"不要写成"mian"了。
{
    printf("Hello World!\n"); // 记得语句末的分号
    return 0;
}

这里有一个最基本的程序框架:

1
2
3
4
5
6
#include <stdio.h>
int main()
{
    /* Your code */
    return 0;
}

绝大部分 C 语言程序都有这个框架,因为所有 C 语言程序都是以 main 函数作为入口点的。

关于这个框架

部分教程会将 int main() 写为 void main(),在特定开发环境中这样写可以通过编译,但这不是标准规定的写法,换个开发环境可能就用不了或者发出警告。

C++ 标准说某个操作非法,那它就是非法的。特定的开发环境下可以通过不代表所有的开发环境都可以通过。当你违反了标准时,你不能寄希望于编译器帮你改正。编译器没有义务必须生成合理的程序。

——摘自 C++ FAQ

另外,对于标准中未定义或不强制要求的操作,不同编译器的应对行为不同,因此会出现代码的兼容性问题。


根据标准文档,如果程序正常运行到最后一行命令并退出,则此行为等效于 return 0。因此为了减轻负担,会省略掉 return 0

如果程序运行异常,main 函数会返回其他的数字。不论何种情况,程序都会退出,只是退出时的状态不一样。通过返回值,其他关联程序就可以判断出这个程序的运行状况,并做出相应行为。

之后的语言采用了 “异常处理” 机制,突破了返回值的局限性(过于笼统,识别不易),实现了更加精细化的管理。

可以参考:return 0 vs return 1 in C++ - GeeksforGeeks

[Solved] why do we have to use 'return 0' ? - CodeProject


ISO/IEC 9899:2024 标准的 Program Startup 部分规定了 main 函数的标准定义模式:

程序启动时调用的函数称为 main。它的返回值应该定义为 int 类型,没有参数:

1
int main(void) { /* ... */ }

或者带有两个参数(参数名没有限制,一般为 argcargv,作为局部变量使用):

1
int main(int argc, char *argv[]) { /* ... */ }

或者类似形式,或者其他实现定义的规则。

此处 “类似形式” 的定义是:int 可以替换为其他被定义为等价于 int 类型的类型名,或者 argv 的类型可以写为 char ** argv,又或者返回值类型由 typeof(1) 指定,诸如此类。

关于程序注释

注释插入在程序代码中,用来向读者提供解释信息。编译时编译器会自动处理注释,因此注释对程序的功能不会有任何影响,但能显著提高源代码的易读性。写注释是一件麻烦事,但当你看别人的代码时,又会希望对方写了详尽的注释。

接下来解读一下不属于这个框架的代码:

1
printf("Hello World!\n");

这行代码起到输出作用。printf 是一个函数,顾名思义,“print” 说明程序会输出(打印)一些东西出来,而末尾的 “f” 指的是格式化 (Formatted)。"" 里面的内容叫做字符串\n表示换行操作。

Info

字符是一个宽泛的概念,英文字母、数字、汉字甚至 Emoji 等都属于字符。多个字符连在一起就形成了字符串。字符同时是一个数据类型。

接下来我们讲讲如何处理程序中的错误

  • 程序中出现的语法错误会在编译时被编译器发现。此时 Dev-C++ 会以红底突出错误所在的地方。
  • 编译器会提供具体的错误原因(英文,看不懂就 bing 一下),列在 “编译器” 栏里。但有时编译器给出的错误原因不是很清晰(位置不对),可以往前往后看一看。
请使用英文

注意应使用半角 / 英文标点符号,如果使用全角符号会出现语法错误。

尽量使用全英文编程,即使部分开发工具声称 “支持中文”。这能够有效避免开发中的编码问题。

例如,MSYS2-GCC 不支持中文文件名,导致它编译得到的程序名是乱码,使我为了继续使用一个 QoL 插件不得不转向 Clang。

关于警告

除了可以终止代码编译的 “错误” (Error) 信息,我们还有 “警告” (Warning) 信息。有些警告信息会影响到程序运行,有些则可能是代码风格问题。你可以忽略这些警告,因为它们(在入门阶段)不会影响编译,但如果你想写出合乎标准的代码,那么你就必须处理所有警告
在大型项目中,可能就需要严肃考虑警告带来的低效问题了,例如 项目警告对构建速度的巨大影响 - XAOXUU 中的情形:屎山代码的警告数超过 IDE 的显示上限,本地构建项目时开发人员的 MacBook Pro 甚至出现长时间卡顿。


3.3 编程字体

我们建议为代码编辑器配置一个等宽字体,而不是宋体等比例字体。等宽字体的可读性更好,部分编程特化的字体还可以显示特殊图案。Maple Mono NF CN、Fira Code、Jetbrains Mono 等都是优秀的字体


3.4 Calculator

还记得我们前面说的计算机的主业是计算吗?现在我们要编写一个简单的定值计算程序。这里假设我们已经套用了上面的程序框架。

1
2
3
printf("%d\n", 23 + 43);
//OR
printf("23+43=%d\n", 23 + 43);

%d是一个格式字符串,起到占位作用,告诉程序这里有一个整数要输出。

如你需要查看有哪些格式字符串,可参考:C 库函数 – printf () | 菜鸟教程

下面总结一下 C 语言常用的算术运算符

数学运算符C 语言对应符号意义
+++
--
××*
÷÷/
%取余
()()()括号

3.5 C Compiler

介绍 C 语言编译器的历史,重点放在 GCC (the GNU Compiler Collection) 上

C 语言与 UNIX 系统一同成长,正是 C 语言赋予了 UNIX 以可移植性,因此所有的 UNIX 系统都会自带一个 CC (C Compiler)。早期的 CC 由系统厂商各自开发,之后 GCC 横空出世,一统江湖。

UNIX 系统的历史,参见 观点 | Unix 历史:一个伟大作品的诞生.

DOS 系统上有微软的 MSCC,但没有 GCC。

之后 Linux 出现,同样有 GCC 的身影。

原本 Windows 系统上只有微软自己的编译器可用。后来有人希望将 GCC 移植到 Windows 上,这时开发者就需要进行这些工作:

  1. 修改 GCC 本身,使 GCC 可以接入 Windows 的 API

  2. 修改库 (library),使得 GCC 可以在 Windows 上调用这些库

Info

我们可以把库看作是一套工具,不同的库里有不同的函数,具有不同的用途,就像螺丝刀套件有不同的刀头。做事情前,我们需要知道要用什么工具,因此在程序开头,我们要 include 这些库,让程序知道我需要这些库,执行下面的代码时就懂得调用。

由此,人们分化出了两种解决方案:

  1. 以 Cygwin 为代表的编译器集采用 UNIX 系统上的库,这种编译器编译出来的 C 语言程序本来是给 UNIX 系统运行的,但通过这个库中的某些接口,Windows 上也可以运行这些程序

  2. 以 MinGW 为代表的编译器集采用 Windows 系统上的库,这种编译器编译出来的 C 语言程序是直接运行在 Windows 上的


3.6 配置 GCC

Dev-C++ 提供的 GCC 版本过于古老,我们可以自己配置一个较新的 GCC。

MinGW 有多个维护者,这里介绍两个维护版本:TDM-GCC (Dev-C++ 选用) 和 MSYS2。

TDM-GCC 的配置流程是这样的:下载安装程序:tdm-gcc > 运行安装程序 > 选定安装文件夹 > 自定义安装项目(建议全装) > 检查安装是否成功(在终端中运行 “gcc” 命令,返回gcc.exe: fatal error: no input files说明已经安装成功,否则进行环境变量配置)

Tip

Win11 下配置环境变量的位置在设置 > 系统 > 系统信息 > 高级系统设置 > 环境变量。配置环境变量时,在 TDM-GCC 安装目录下找到 bin 文件夹,将 bin 文件夹的路径拷贝至环境变量中(用户变量 / 系统变量均可)

对于 MSYS2,MSYS2 项目官网有基础安装教程以供参考,此处不再赘述。

Info

其实 TDM-GCC 早在 2021 年后就停止更新了…… 如果要自己配置开发环境,我们更建议使用 MSYS2。VSCode 官方也推荐 MSYS2。


3.7 配置 VSCode

厌倦了 Dev-C++ 老掉牙的界面,希望来点现代化的东西?那就来尝试一下 VSCode 吧!
和自带编译和 Debug 环境的 Dev-C++ 不同,未经配置的 VSCode 无法走通开发流程,因此上手难度会高一点。
但 VSCode 配置难如登天吗?官方提供的教程大约 10 分钟就能阅读完毕,不出半小时就能得到一个基本可用的编译环境。
这不是新手不可逾越的大山。

可供参考的教程

官方配置教程:C++ programming with Visual Studio Code (English, GCC);

(建议优先参考 VSCode 官方的教程,因为我们还不熟悉 json 文件配置。有些作者擅自修改 json 内容,造成兼容性问题。点名某 SDN,有教程作者甚至下场喷人,翻完文档才发现是 TA 自己不按官方的来)


3.8 了解命令行

Note

不需要强行使用命令行。对于刚入门的新手,直观化的 GUI 界面更容易接受并配置。

目前为止,我们使用的开发工具都是图形化的,这使得某些人认知偏差了:编译运行程序就是一个 ➤ 键的事。

但这个按键只是一个表象,它背后代表的是一行行命令。IDE 的 “输出” 窗口已经揭示了这一点。

小熊猫 C++ 的工具输出窗口

在这一长串命令中,核心的编译命令是 g++.exe E:/Cproject/Algorithm_Exercise/test.cpp -o E:/Cproject/Algorithm_Exercise\test.exe。后面的命令称为 “编译器标志 (flag)”,用来指导编译器如何编译代码。

但这同样省略了一些东西。从源码到可执行文件也不是一个 -o 标志即可搞定的。现代编译器一般经过:

graph LR
A[源代码 Source] -->|语法分析| B[抽象语法树 AST]
B -->|语义分析/生成| C[中间表示 IR]
C -->|指令选择/优化| D[汇编语言 Assembly]
D -->|汇编/链接| E[机器指令 Instruction]

Lesson 4 计算

4.1 存储数据——如何定义一个变量

前面我们所写的程序没有输入环节,输出结果都是写死在程序里的,这导致如果我们想要其他的输出结果,就得修改代码,再走一遍编译流程。这显然是很麻烦的,因此我们需要一个输入环节。

怎么设计一个输入环节呢?我们需要:

  • 1. 有地方存储输入的数据
  • 2. 有办法输入数据
  • 3. 输入的数据能参与计算

接下来给出代码示例(部分):

1
2
3
4
5
int price = 0;
printf("请输入金额(元):")
scanf("%d", &price);
int change = 100 - price;
printf("找您%d元。\n", change);

程序的输入操作在终端中进行。输入以行为单位进行,回车代表行结束,程序在这一步的输入操作也结束,程序正式读取用户输入。在行结束前,程序不会读取任何数据。

接下来我们将解读一下这段代码。

首先, int price = 0; 这一行定义了一个变量。变量是整数型变量,名字是 price ,初始值是0.变量解决了我们上述设计输入环节的第 1、3 点:变量是一个保存数据的地方,数据通过变量参与到计算中

我们要如何定义一个变量呢?有以下规则要遵守:

  • 1. 语法为:type_name var_name;。多个同类型变量可以在一行内定义,如:int a, b, c;。在变量定义时,我们可以为变量赋初始值,如我们上面的示例。

    Tip

    注意不同类型变量不能在一行内定义,比如 int a, float b, char c; 就是非法的。

    另外,有些代码规范不建议在一行内定义多个变量:DCL04-C. Do not declare more than one variable per declaration

  • 2. 变量需要一个名字作为 “标识符” 以区别于其他变量。基本的命名规则是:标识符只能由字母、数字和下划线组成,数字不能出现在第一个位置上,C 语言的关键字(保留字)不能作为标识符

    Tip

    关键字不需要去记忆,实践和编译器会告诉你哪些是关键字。例如,在小熊猫 C++ 中定义 int if 时,代码诊断会提示:[错误] expected unqualified-id before 'if'

    关键字对拼写敏感,就算字母的大小写不一致,也认为不是关键字。比如 Int 就是一个合法的变量名而不是关键字。

  • 3. 建议采用能够标示变量用途的名称作为变量的名字。a, b, c在代码规模很小时固然简单好用,但在工程实践以及较大规模的项目中,我们可能得为这些简单的变量名编写注释,否则其他人甚至几天后的自己就无法理清变量用途了。

    Tip

    例如,同样是最大最小值,有人写的是maxmin,有的人写的是ab;表示数列项,我们说 ana_n,有的人偏偏要用 aba_b

C99 及之后的标准允许开发人员在程序的任何位置定义变量,因此在函数中间插入像 int change = 100 - price; 的定义是正确的。

Info

ANSI C 标准只允许用户在代码开头定义变量,也就不支持这种复合写法了。但现在我们学习 C 语言应该不会遇到这种问题。即使是 Dev-C++ 5.11 自带的古老 TDM-GCC 4.9.4 也已经支持 C99 标准。


4.2 变量赋值与初始化

int price = 0; 中,除了变量定义外,我们还进行了变量的赋值。赋值操作由 = 这个赋值运算符进行,表示将 = 右边的值赋给左边的变量。在变量定义时的赋值操作,叫做初始化。对于拥有运算符的式子,我们称为表达式

Warning

计算机的赋值运算符和数学中的等于号代表的意义并不相同。等于号代表一种关系,赋值运算符代表一种动作。关系是静态的,动作是动态的。在数学中 a=ba=bb=ab=a 是等价的;而在计算机中,两者代表的动作完全相反。

在计算机中,我们用== (两个等号) 代表等于号。

初始化不是必要的,但你必须保证将变量的值赋值给其他人时,这个变量存储了有意义的数据,不然计算机会给变量随机赋一个奇怪的值。

Tip

常量必须初始化,变量也只该在极罕见的情况下不初始化。 在准备好合适的值以前,别引入这个名称。

——1 基础知识・C++ 导览 第二版 简体中文版

多个变量不能在一行内同时初始化,因为赋值本身是一个运算,它的结果就是赋值的值。

Warning

1
2
3
// int a = b = 2; // error: ‘b’ undeclared (first use in this function)
int a, b;
a = b = 2;

到这里我们是不是还没谈int这个东西?int是一种数据类型,表示 price 这个变量能且只能存储整数数据,其他数据类型后续会学习。在 C 语言中,所有的变量在使用前必须定义或者声明,而在变量定义时,必须确定变量的数据类型,变量中只能存放对应类型的数据,程序运行时也不能中途改变变量的类型


4.3 读取输入——scanf函数

现在还有一个问题:怎么输入数据呢?我们来看这一行代码:

1
scanf("%d", &price);

scanf也是一个函数,同样的,“scan” 说明程序会读取(扫描)用户输入的内容,“f” 指格式化。

等等,price 前面的 & 是什么?这涉及到 C 语言一个重要的内容——指针,在后面 (Lesson20) 我们会详细解读。现在只需要记住:没有这个符号,scanf没法将数据赋值给变量 price

scanf的格式化输入比较严格,下面看一组示例:

1
2
scanf("price %d %d ", &a, &b);
printf("%d %d", a, b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Case 1:
$ price 1 2 # 注意这里末尾有空格
$ 1 2 # 正确输出

# Case 2:
$ price 1,2
$ 1 0 # scanf没有读取到变量b的输入
# Case 3:
$ price 1 2 # 注意末尾无空格
$   # 程序判定输入未完成,按回车也没用
$ 2 # 此时需要再输入任意一个字符充当空格
$ 1 2 # 输入结束,输出正确
# Case 4:
$ 1 2 # 我好懒我不想输入price
$ 0 0 # 出错啦QAQ
$ price 1 2 # 格式字符串"price %d %d "中的所有东西都需要你输入,包括说明性的price。因此当我们需要输出说明文字时,先用printf,再用scanf
# Case 5:
$ price QWQ QAQ # 精神错乱(
$ 114514 1919810 # scanf读到了两个不符合要求的数据,拒绝传递给变量,导致两个变量没有值,计算机内存给两个变量随机分配了值

我们再来看一个例子:

1
2
scanf("%d", &a);
scanf("%c", &c);

要怎么输入?正确的输入格式应该长这样:22d

注意中间不要加空格与换行。空格和换行本身也视作字符,会被后面读取字符的scanf读取。

希望输入空格或换行?请在输入intscanf明确指出需要用户输入,即scanf("%d\n", &a);

Tip

也有另外两种做法:

  1. 使用stdlib.h库中的getchar()来消耗掉输入结束时的换行和空格
  2. 在读取字符前加个空格,即写成scanf(" %c", &c),这个空格可以让scanf忽略所有空白字符(包括换行)

4.4 常量

int change = 100 - price;中,100被称为常量,它的值不会因为外界输入而变化。这种直接写在程序里的常量,叫做字面量 (literal)。

没有定义、在代码中间的字面量有时被称为 “魔数”,因为除了注释,基本没有地方能说明它的意义。

如果你的程序有很多地方用到了这个常量,一旦日后这个常量有变化了,你会愿意一处一处修改吗?所以定义常量然后引用变量更好。

常量和变量有一定的相通之处。常量需要指定数据类型,需要常量名称,命名遵循变量命名的原则。但也有差别,例如:

1
const int AMOUNT = 100;

const是一个修饰符,加在 int 的前面会给 “变量” 加上一个 constant(不变的)属性,表示这个变量的值在初始化过后就不能更改了。如果在初始化之后给常量赋值,编译器就会报错:error: read-only variable is not assignable。为了突出常量,建议将常量的标识符全大写

接下来我们就可以将 100 更换为 AMOUNT 了。之后我们想更改常量的值时,只需要修改开头 AMOUNT 的值就行了。

Info

你还会看到有人这样 “声明” 常量:

1
#define AMOUNT 100

我们称之为 (Macros)。在 C 语言中,宏是一段在程序中被替换为宏值的代码。每当编译器遇到宏名时,它会用宏的定义替换该名称。

1
2
3
4
#define base 20

//...
int a = n + base;// actually `int a = n + 20`

宏通过 #define 指令定义,必须在源文件开头定义(不能在函数中定义)。定义语句不需要以分号(;)结束。

上面定义的是对象宏 (Object-like Macros)。一个对象宏类似于一个简单的标识符,它通常用于将符号名称替换为表示为常量的数值 / 变量。

还有其他类型的宏。参考 Macros and its types in C - GeeksforGeeks 以及 Macros (GNU C Language Manual)

宏和 const 常量的不同之处可以参考 C++ 宏定义 #define 和常量 const 的区别。总结就是宏定义是字符替换,而 const 常量确实存在于运行时内存中。

宏在工业级项目中的作用不仅是简单的 “声明常量”,还可以引导编译器按开发者的意图顺利编译整个项目。这在一定程度上导致入门者难以阅读真实 C/C++ 项目的源码。当然,我们现在只是进行单文件编程,不需要掌握这种比较复杂的东西。

小彩蛋

Ariel, listen to me
OO languages? is a mess.

Prgramining in C is better then anything they got over there
The syntax might seem much sweeter
Where objects and subtypes play
But frills like inheritance
Will only get in the way!
Admire C's simple landscape
Efficiently dangerous!
No templates or fancy pitfalls...
like Java and C++!

Program in C
Program in C

Pointers, assembly
Manage your memory
With malloc() and free()!
Don't sink your app with runtime bloat
Software in C will stay afloat
Do what you want there
Close to the hardware
Program in C

Resource: 🐢Vedal987 - Program in C

D 指导的中译:

阿瑞尔,听我说
面向对象?一团糟

用 C 编程胜过所有花哨
那里的语法看似甜美
对象与子类嬉闹
但继承之类的装饰
只会碍手碍脚!
仰望 C 简洁的风景
危险却高效!
没有模板或炫技天坑...
像 Java 和 C++!

用 C 编程
用 C 编程

指针,汇编
亲手管理内存
用 malloc () 和 free ()!
别让运行时臃肿沉船
C 语言软件永葆轻便
在这里随心所欲
直抵硅基心跳
用 C 编程


  1. 由于 C 语言允许直接操作计算机硬件和内存,因此它也被认为是 “低级的”。一些观点认为,C 属于 “中级语言”。
    参考:Computer programming language | Types & Examples | Britannica高级编程语言 - MDN Web 文档术语表 ↩︎