C语言学习笔记——第一个程序

准备操作那篇里敲过一段代码, 当时说”目前不用知道原理”. 现在该把那段代码拆开看一看了

这篇不打算先讲什么数据类型、什么运算符, 因为脱离代码去记这些名词没什么意义. 我们直接盯着一个能跑的程序, 一行一行搞清楚它在干什么. 等有了”一个C程序大概长什么样”的印象, 后面的东西才好往这个框架里塞

0 综述

1
2
3
4
5
6
7
8
9
第一个程序
├── 1 最小程序逐行拆解
├── 2 头文件与 #include
├── 3 main 函数: 程序的出入口
├── 4 函数的形状: 首部 + 函数体
├── 5 语句、分号与注释
├── 6 输出: printf 初步
├── 7 输入: scanf 初步
└── 8 其他猎奇题目

1 最小程序逐行拆解

把准备操作里的那段拿来:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("type in whatever you want to print");
return 0;
}

四行代码, 去掉空行其实就四件事:

行 代码 在干什么
1 #include <stdio.h> 把”输入输出工具箱”拿进来
2 int main() { 定义主函数, 程序从这里开始
3 printf("..."); 往屏幕上打一行字
4 return 0; 告诉系统”我正常结束了”

不理解 int 是什么意思、return 0 为什么要写 0都没关系

现在只要知道一个 C 程序 = 一堆函数, 其中必有且仅有一个叫 main不是mian

2 头文件与 #include

#include <stdio.h> 里的 stdio 是 standard input output 的缩写, 也就是”标准输入输出”

C 语言本身其实很小, 它连”怎么往屏幕上打印东西”都不管. 打印是别人写好放在库里的, stdio.h 相当于一张清单, 告诉编译器”我接下来要用这些东西, 你都可以找到”

所以如果把 printf 写在 #include <stdio.h> 之前, 编译器会不认识 printf 从而报错

#include 有个细节: 它是预处理指令, 在编译之前就会被处理掉, 属于”编译前的一道工序”. 展开讲要等到编译预处理那篇

<> 和 "" 的区别(系统头文件 vs 自己写的头文件)也留到那时, 在之前只要知道类似#include "a.h"就代表a.h是用户自己写的

3 main 函数

一个 C 程序里可以有很多函数, 但有且仅有一个 main 函数. 程序运行时, 一定是从 main 的第一行开始, 到 main 结束为止.

1
2
3
4
5
程序开始
↓
main 函数 ──→ 调用别的函数 ──→ 返回
↓
程序结束

需要注意的是, main 写在文件最前面还是最后面都不影响, 它永远是第一个执行的. 换句话说, 一个程序里有几十个函数, 系统偏偏只认 main 这一个入口.

另外注意 main 后面那对代表函数的括号不能省. 写成 int main 而不带 () 是错的

main 前面的 int 表示这个函数返回一个整数. 所以最后要写 return 0;, 把 0 交回给系统

现在很多高级IDLE会自动加上return 0, 但是考试的时候没有高级货, 这也是刚入门用DevC++的好处

为什么是 0? 因为操作系统约定: 返回 0 表示程序正常结束, 返回非 0 表示出了错. 在准备操作里看到的那个”状态码”, 就是 return 出去的值

4 函数的形状: 首部 + 函数体

main 就是一个函数, 而任何一个 C 函数都由两部分组成:

1
2
3
int main()   {    printf("hello");    return 0;   }
└────┬───┘ └────────────────┬────────────────┘
首部 函数体
部分 内容 说明
函数首部 返回值类型 + 函数名 + 参数表 描述”这个函数叫什么、要什么、还什么”
函数体 一对 {} 包起来的部分 描述”具体怎么干”

函数体内部还可以再细分:

  • 声明部分: 定义变量(比如 int a;)
  • 执行部分: 真正干活的语句

这个概念属于”书上写得郑重、考试只考选择题”的类型. 需要记住的就是函数 = 函数首部 + 函数体, 以及函数体 = 声明部分 + 执行部分

{ 和 } 是函数体的标志, 缺一个就是语法错误

顺带一提, 函数体可以为空int main() { } 是合法的, 编译器最多给个”没有 return”的警告, 照样能跑, 只不过什么都不干. 但在 main 里通常至少写个 return 0;.

5 语句、分号与注释(★★★)

5.1 分号

C 语言里每条语句都以分号结束. 少了分号是最常见的编译错误之一

报错形式如[Error] expected ';' before 'xxx' token

但是要注意 #include 后面不写分号. 它是预处理指令而不是语句, 严格来说不该有分号

不过就算手滑写了 #include <stdio.h>;, 编译器只会给个警告, 不会报错, 程序照样能跑. 原因是预处理指令以换行为结束标志, 那个多余的分号被当成了”多余的符号”忽略掉了

但不要因此而养成坏习惯

5.2 注释

1
2
3
4
5
6
// 这是单行注释, 从 // 到本行结束, 中途遇到其他的//不会结束注释状态

/*
这是多行注释
中间可以换行
*/

注释是给人看的, 编译器会直接忽略掉.

但要注意多行注释不能嵌套——/* /* */ */ 这样写, 第一个 */ 就把注释提前结束了, 后面的内容会变成代码导致报错

也要注意注释确实不会让计算机看到, 但不代表没人看毕竟就是写给人看的. 因此不要在注释中写暴戾语句, 每次相关的程序设计竞赛结束后会将所有选手代码公示, 而每年都有许多人因写暴戾语句被禁赛甚至通报批评, 常见的包括但不限于F**k CCF等

6 输出: printf 初步

printf 的作用是把内容打到屏幕上. 最简单的用法是直接扔一个字符串进去:

1
printf("love u");

但更多时候我们需要输出一些无法实现预测的内容, 如变量, 这就需要用到格式控制符:

1
2
int age = 18;  // 代表age的值是18
printf("我今年 %d 岁", age);

输出结果是 我今年 18 岁. 这里的 %d 是个占位符, 它在实际运行过程中会被后面 age 的值顶掉

值得注意的是, printf里的空格是不会消失的

目前只需要记住两个最常用的:

格式符 用来输出
%d 整数
%s 字符串

其余的(%f、%c、%lf 等)等认识了对应的数据类型再说, 现在提前背只会记混

注意引号外面那个逗号: printf("...", 变量). 要输出的变量跟在字符串后面, 用逗号隔开. 有多个变量就依次往后排, 和 %d 一一对应, 如printf(“%d%d%d%d%d”, a, b, c, d, e);

7 输入: scanf 初步

scanf 用来从键盘读数据. 用法和 printf 很像, 但有个必须记住的差异

1
2
printf("%d", age);    // 输出: 不用 &
scanf("%d", &age); // 输入: 必须加 &

为什么输入要加 &?

printf 只需要”知道 age 是多少”, 把值拿出来用就行, 不用知道 age 在内存中的位置. 而 scanf 需要”把读到的数字放进 age 这个盒子里”, 所以它必须知道 age 在内存里的位置. & 就是”取地址”的意思——拿到 age 的门牌号, 才能往里面塞东西

现在只要记住输入加 &, 输出不加, 原理等讲指针时自然就通了

scanf("%d", age); 少写&时, 实测下来它能通过编译(编译器只给一句警告, 不是错误), 但运行时程序会直接崩掉——因为程序会把 age 里那个乱七八糟的值当成地址, 往一个不确定的地方写数据. 这种”编译能过、运行才炸”的 bug 最难查(我们称其为RunTimeError, 简称RTE), 尤其考试时只给结果不给报错, 更要注意

数组名做输入时不用加 &(如 scanf("%s", name);), 原因也留到数组篇

学会了scanf, 可以将上文中的程序进行扩充了, 实现真正的IO, 让程序让你输入年龄, 并输出你的年龄

代码
1
2
3
4
5
6
7
8
#include <stdio.h>;
int main() {
int age;
printf("Input your age:");
scanf("%d",&age);
printf("%d",age);
return 0;
}

如果输入语句中age前没有&, 运行时会在输入后卡一下然后变成下图这样, 注意return的值

1

而如果你学过Python, 并妄想像python里input语句那样合并第4,5行成为scanf("Input your age:%d", &age);的话那恐怕要让你失望了, 因为这样像要让程序读入%d的值的话必须这样输入:Input your age:12, 否则格式不对age就不会有值