物理 [栖岸计划]智英带你学C语言(第二章)
返回目录:[栖岸计划]智英带你学C语言(新版回归)
第二章 第一个C语言程序
刚才的图片中展示了这样一个完整的C语言程序:
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
运行结果是在控制台打印了"Hello, world!"。与Python只用一句
print('Hello, world!')
即可实现相同功能不同,由于C语言写的程序有不少必要的组成部分,所以长度更长。下面我们来拆解这段代码。
2.1 预处理指令
我们看到第一行:
#include <stdio.h>
类似这种以#号开头的指令会在编译之前执行,作为编译的准备工作,因此被称为预处理指令。预处理指令主要分为以下几种:
2.1.1 头文件包含
即#include。后面跟着由<>或""包裹的文件名,表示导入在这些头文件中声明的函数、结构体,以及头文件中的宏定义等信息。在入门阶段,我们重点关注的是如何调用头文件中声明的函数。换句话说,如果我在某个头文件A.h中声称“你可以调用这个函数”,那么我在程序开头添加#include "A.h"这一语句,就可以调用上面这里提到的函数。
习惯上,在包含头文件时,标准库头文件用<>,自己写的头文件用"",这与头文件的搜索路径相关。前者有时也可以混用"",但后者绝大多数时候不能使用<>,后文会详细讲解。
(关于函数的声明、定义与调用,后文中会详细解释,这里先按下不表)
具体到上面这段代码,我们需要调用格式化输出函数printf。C语言没有内置的输入和输出,需要先包含头文件stdio.h(或者自己实现),才能从中调用scanf,printf等用于输入输出的函数。因此,上面程序的头文件包含指令是必要的。我们可以尝试,删除这一指令后,会发生什么:

报错信息显示,printf函数存在不兼容的隐式声明,建议包含stdio.h或者自行提供printf函数的声明。这也正是没有包含必要的头文件会带来的后果。由于输入和输出极为必要,绝大多数程序都会直接或间接包含stdio.h。
关于头文件与#include指令的更详细讲解,以及如何自己写头文件,远超本章的入门需求,将在后文中提到。
2.1.2 宏定义
即#define。原版代码中没有呈现,但我们稍作修改:

上面的#define STR "Hello, world!\n"指令告诉预编译器,需要将后面所有的STR替换为"Hello, world!\n"。于是后面的STR被替换为"Hello, world!\n",此时代码已与原版没有任何区别。从而最终仍然打印"Hello, world!"。
看起来,既然宏定义仅仅是对后面代码的直接替换,完全可以在手写时就在后面的代码使用替换的结果。实际上,#define对于编写和阅读代码都有很重要的作用,后文会详细提到。
需要注意的是,这里的STR看起来像一个单独变量,实际不是。由于宏定义仅仅是单纯的文本替换,我们完全可以写一个这样的程序:

是的,它通过了编译,且完全能正常输出。当然,没有任何一个正常人会这么写代码,这一极端的例子,不过是更具体地体现了“文本替换”的含义。
2.1.3 条件编译和其他指令
条件编译语句有很多种,包括#if, #else, #elif, #endif, #ifndef, #ifdef等。除此之外,预处理指令还有#error等,有不同的用途。这些对于初学者而言,难度较大,此处略去,在后文中都会出现。
2.2 main函数
在预处理指令之后,我们看到整个程序由这些部分构成:
int main() {
//此处有一系列的代码
return 0;
}
这里的int main()(或写作int main(void))被称为主函数(main函数),其作用是给运行程序提供入口。执行程序时,系统会调用main函数,完成main函数内部内容的运行。
一般在main函数的定义内部,最后需要以return 0;结尾。这里return后面跟的数表示函数的返回值,换句话说,就是函数运行的结果。main函数返回0表示程序正常运行,返回其他值表示程序异常。例如,我们对上面程序里main函数的返回值改为1:

看起来它还是正常输出了"Hello, world!",实际上控制台的红叉告诉我们,程序异常退出了。这就是main函数返回值不为0的结果。有的时候,我们会刻意把某些情况下main函数的返回值设为非零数,其目的往往是为了在程序出现异常时,能通过不同的返回值设置,找到出问题的地方。例如下面的程序:

这个程序比较复杂,所以这里语言解释其含义:从键盘输入一系列内容,如果输入的是整数,那么把这个整数打印出来;如果输入的不是整数,那么就报错。下面控制台显示的内容:当输入1,114514,-11等整数时,程序把这个整数打印了一遍;但输入的是abc时,程序报错。此时,我们就可以通过查看返回值,找到程序出错的地方。如果我们把这个return 1;删掉,让它正常返回,那么结果会变成这样:

输入的是整数的时候,看起来一切正常;但输入的是字符串时,程序正常返回,并输出了一个奇怪的整数。此时,我们再要去排查问题,难度就会更大。更何况实际进行编程时,往往涉及到许多文件,每个文件都可能有上百行,如果不进行这些设置,debug会成为一件极度令人痛苦的事。(当然,即便有这些设置,debug也很令人痛苦,但至少比这种都不知道错在哪要好得多)
(上面为什么会输出一个奇怪的整数?因为声明n时,其会被初始化为一个“垃圾值”。详细内容后面会再提到。)
main函数还有一些不标准的写法。在讲解之前先强调一遍:尽量不要用以下两种方法写main函数!
一般情况下,如果main函数的返回值为0,那么return 0;语句可以省略。例如,下面的程序也是正确的:

或者,有些时候main函数的返回值类型会被设为void(返回一个无类型变量,简单理解,就是没有返回值),而非int:

不过,有些编译器不支持第一种操作;第二种操作严格来说属于未定义行为,更多的编译器不允许这种行为,风险比第一种操作还要高。所以绝大多数情况下,我们还是会完整书写。
这里顺带提一下编程习惯的问题。有的时候,有些代码是能通过某些编译器编译,能正常运行的,但对于另外一些编译器,这些代码就会出错,存在风险。所以,在编程过程中,要尽量避免这样的行为!