第 2 讲 · C 语言编程基础框架

一个最小的程序可以拆成 IPO:Input(输入)→ Process(处理)→ Output(输出)。第二讲做的事情,就是把这三个盒子里最基本的零件配齐。

1. 先读懂一个完整程序

#include <stdio.h>

int main(void)
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%d\n", a + b);
    return 0;
}
  • #include <stdio.h> 引入标准输入输出函数的声明;
  • main 是程序入口;
  • int a, b 定义两个整型变量;
  • scanf 从标准输入读取数据;
  • printf 把结果写到标准输出;
  • return 0 表示程序正常结束。

大括号形成语句块,分号结束一条普通语句。缩进不改变 C 的语义,却直接影响代码是否容易看懂。

2. 常量:写在程序里的固定值

2.1 整数与进制

int a = 19;     /* 十进制 */
int b = 023;    /* 八进制,十进制的 19 */
int c = 0x13;   /* 十六进制,十进制的 19 */

前导 0 在 C 整数常量中不是装饰。010 是八进制的 10,也就是十进制 8。实际代码中若没有特殊目的,整数优先写十进制。

2.2 浮点常量

double x = 0.28;
double y = 2.8e-1;
float z = 0.28f;

没有后缀的浮点常量默认是 double;后缀 f 或 F 表示 float。1.6e-2 表示 1.6×10−21.6\times 10^{-2}。

2.3 字符、ASCII 与转义

字符常量用单引号:

char digit = '5';
char lower = 'a';
printf("%d\n", digit);       /* 常见 ASCII 环境下输出 53 */
printf("%c\n", lower + 2);   /* 输出 c */

字符在机器里保存为整数编码。ASCII 中数字字符、大小写字母各自连续,因此:

int value = digit - '0';      /* '5' 变成整数 5 */
char upper = lower - 'a' + 'A';

几个常用转义字符:

写法含义
\n换行
\t水平制表
\0编码为 0 的空字符
\\反斜线
\'、\"单引号、双引号
\x61、\141十六进制、八进制编码的字符,均为 a

要用 printf 输出百分号,应写 %%。

2.4 字符串不是一个字符

字符串常量用双引号,并以 \0 结束:

char s[] = "x";

这个数组实际包含 'x' 和 '\0' 两个元素。'x' 是一个字符值,"x" 是一段字符序列,二者不可互换。

3. 变量:带类型、名字和存储空间的数据

int count = 0;
double radius = 1.5;
char grade = 'A';

变量必须先定义再使用。局部变量若没有初始化,其初值不确定:

int sum;
printf("%d\n", sum);   /* 错:读取未初始化的局部变量 */

3.1 标识符规则

变量名、函数名等标识符:

  • 由字母、数字和下划线组成;
  • 不能以数字开头;
  • 区分大小写;
  • 不能使用 int、return、if 等关键字;
  • a[2]、sum*count、sin(25) 是表达式,不是名字。

命名风格可以用 studentScore、student_score 等,关键是含义明确并在同一项目中保持一致。课件还介绍了匈牙利、骆驼、下划线和 Pascal 命名法;这些是风格,不是语法规则。

3.2 类型与范围

C 标准只规定了各种整数类型的相对大小和最小范围,具体字节数应以当前平台的 sizeof 为准:

printf("%zu\n", sizeof(int));

常见 64 位桌面环境里,char 为 1 字节、int 和 float 为 4 字节、double 和 long long 为 8 字节,但不要把“常见”误当成语言的绝对保证。

signed 表示有符号整数,unsigned 只表示非负数。数据范围不够时,计算会溢出:

int a = 1000000000;
int b = 2000000000;
long long sum = (long long)a + b;

强制转换写在运算前,才能让加法以更宽的类型完成。

4. 格式化输入输出

4.1 常用格式符

数据scanfprintf
int%d%d
unsigned int%u%u
long long%lld%lld
float%f%f(会提升为 double)
double%lf%f
char%c%c
字符串%s%s

scanf 需要变量地址:

int age;
double height;
if (scanf("%d%lf", &age, &height) == 2) {
    printf("age=%d, height=%.2f\n", age, height);
}

格式符和实参类型不匹配会导致未定义行为,而不只是“显示不好看”。

4.2 空白与字符输入陷阱

数值格式符会跳过前导空白,%c 不会。刚读完数字后,缓冲区里的换行可能被 %c 读走:

char op;
scanf(" %c", &op);   /* 前面的空格表示跳过任意空白 */

不要随手在格式串末尾写 \n。scanf("%d\n", &x) 会继续等待下一个非空白字符,看起来像程序“卡住”。

4.3 单组、多组与 EOF

单组输入直接读一次;多组输入可利用返回值:

int a, b;
while (scanf("%d%d", &a, &b) == 2) {
    printf("%d\n", a + b);
}

EOF 是输入结束标记。scanf 成功读取几项,就返回几;在读取任何项之前遇到输入结束,才返回 EOF。

4.4 控制输出外观

printf("%8d\n", 42);       /* 字段宽度 8 */
printf("%.3f\n", 3.14159); /* 小数点后 3 位 */
printf("%-8dend\n", 42);   /* 左对齐 */

OJ 会逐字符核对答案,多一个提示语、少一个换行都可能判错。

5. 算术、赋值与类型转换

5.1 运算符

a + b;   a - b;   a * b;   a / b;   a % b;

% 只用于整数。整数除法会向零截断:

printf("%d\n", 7 / 3);       /* 2 */
printf("%.2f\n", 7.0 / 3);  /* 2.33 */

复合赋值把“读旧值、计算、写回”合在一起:

sum += x;   /* sum = sum + x */
value *= 2;

i++ 和 ++i 单独成句时都让 i 加一;放进复杂表达式时语义更难读。初学阶段应把更新拆成独立语句,避免在同一表达式里多次修改同一变量。

5.2 隐式与显式转换

混合运算通常把较窄类型提升到较宽类型:

double avg = (double)sum / count;

若写成 sum / count 后再赋给 double,整数除法已经发生,小数部分无法挽回。

把宽类型赋给窄类型可能丢失信息:

int n = 3.9;       /* 通常得到 3 */
signed char c = 200; /* 结果依实现和表示范围而定 */

6. 关系、逻辑与短路

六个关系运算符为 <、<=、>、>=、==、!=。逻辑运算符为 &&、||、!。C 中 0 为假,非 0 为真,关系和逻辑表达式结果是 0 或 1。

if (0 <= n && n <= 10) {
    /* n 在区间 [0, 10] */
}

不能写成数学式 0 <= n <= 10。C 会先计算 0 <= n 得到 0 或 1,再拿它与 10 比较,结果几乎总是真。

&& 和 || 会短路:

if (denominator != 0 && numerator / denominator > 2) {
    /* 只有分母非零时才执行除法 */
}

短路既能提高效率,也能保护后半个表达式不会访问非法数据。

7. 优先级:不确定就加括号

只需牢牢记住一条主线:

括号 → 一元运算 → 乘除模 → 加减 → 关系 → 相等 → && → || → 赋值

同一优先级还涉及结合方向。与其背完整大表,不如用括号表达意图:

if ((a & 1) == 0) { /* 后续位运算会用到 */ }

赋值 = 和判断相等 == 是高频笔误。编译器即使允许 if (x = 0),含义也是“给 x 赋 0,再把 0 当条件”。

8. 一维数组初步

int scores[6] = {90, 75, 88};

数组有 6 个元素,下标为 0~5;未显式给出的后三个元素初始化为 0。

int sum = 0;
for (int i = 0; i < 6; ++i) {
    sum += scores[i];
}

数组不检查边界。scores[6] 语法能通过编译,但访问了不属于数组的空间。

9. 给常量起名字:const 与宏

const double PI = 3.141592653589793;
#define MAX_STUDENTS 200

const 创建带类型的只读对象;对象式宏在预处理阶段做文本替换。宏表达式必须加足括号:

#define SQUARE(x) ((x) * (x))

即便如此,SQUARE(i++) 仍会修改 i 两次,所以带副作用的实参不要交给会重复展开参数的宏。现代 C 中能用 const 或函数表达的内容,通常比复杂宏更安全。

10. 把知识串成一个例子

读取若干成绩,直到输入结束,输出平均分和及格人数:

#include <stdio.h>

int main(void)
{
    int score;
    int count = 0;
    int passed = 0;
    long long sum = 0;

    while (scanf("%d", &score) == 1) {
        sum += score;
        count++;
        if (score >= 60) {
            passed++;
        }
    }

    if (count > 0) {
        printf("average=%.2f\n", (double)sum / count);
        printf("passed=%d\n", passed);
    }
    return 0;
}

这段代码同时用到了类型、初始化、格式化输入输出、循环、选择、类型转换和逻辑边界。基础语法真正掌握的标志,就是能解释每一行为什么这样写,而不是能背下它。

评论