C学习教程,从入门到精通,C语言输入和输出(4)

C语言输入和输出

本章定位 :程序要能"干活",就必须能接收用户给的数据 ,并把结果展示给用户。输入和输出(简称 I/O,Input/Output)就是程序与外界沟通的"嘴巴和耳朵"。本章学完,你写的程序就不再是"闷头算数",而是能和人对话的程序了。


4.1 初步认识 C 语句

4.1.1 C 语句简介

1. 什么是"语句"

知识点说明 :C 程序的基本组成单位是函数 ,而函数体是由一条条语句(statement)组成的。语句是"让计算机做事的一条命令",每条语句以 分号 ; 结尾。

通俗理解 :如果把一个 C 程序比作一本菜谱,那么函数就是"一道菜的做法",语句就是做法里的每一个步骤------"切菜"、"下锅"、"放盐"。分号就是一个步骤结束的句号。

2. C 语句的 5 大分类(重点,必须记住)

类别 形式 作用 例子
① 控制语句 if、for、while、switch、return 等 控制程序执行的流程和顺序 if (a > 0) b = 1;
② 函数调用语句 函数名(参数); 调用一个函数完成某功能 printf("hello");
③ 表达式语句 表达式; 把一个表达式变成一条命令 a = b + 1;、i++;
④ 空语句 ; 什么都不做(占位) while (getchar() != '\n') ;
⑤ 复合语句 { ... } 把多条语句打包成"一条" { a=1; b=2; c=a+b; }

3. 三条容易踩坑的规则

规则1:表达式 ≠ 语句,加分号才成语句

c 复制代码
a = b + 3;    // 这是语句(有分号)
a = b + 3     // 这是表达式,单独出现会报错

规则2:复合语句的 {} 后面不要加分号

c 复制代码
{ x = 1; y = 2; }    // ✅ 正确
{ x = 1; y = 2; };   // ❌ 多了分号(多数编译器只警告,但属于坏习惯)

规则3:空语句不是"没写完",它是合法的

c 复制代码
for (i = 0; i < 100000; i++) ;   // 循环体故意留空,只做延时/等待

4. 为什么要学这个知识点?有什么用?

  • 看懂编译器报错 :80% 的初学报错(expected ';' before '}')都是分号问题,理解"语句"概念后一眼就能看懂报错。
  • 写出自洽的代码 :知道 {} 是一条语句,才能理解后面 if...else 的配对规则(第5章)。
  • 读懂别人的代码 :for(;;); 这种"看着像乱码"的写法,本质就是"空语句"。

4.1.2 赋值语句 ★★★(本章第一个重点)

1. 基本格式

c 复制代码
变量名 = 表达式;

功能 :先计算右边表达式的值,再把这个值存入左边的变量中。

c 复制代码
int a;
a = 3 + 5;      // 先算 3+5=8,再把 8 放进 a

通俗理解 :变量就是一个贴了标签的盒子 ,= 就是"把右边的东西装进左边的盒子里"。所以左边必须是盒子(变量),不能是数字。

2. 最最最重要的区分:= 是赋值,== 才是判断相等

这是 C 语言初学者的第一大坑。

符号 名称 含义 例子
= 赋值运算符 把右边的值放进左边变量 a = 5;
== 关系运算符 判断两边是否相等,结果是 1 或 0 a == 5

经典 Bug 演示:

c 复制代码
int a = 10, b = 20;

if (a == b)    printf("相等\n");
else           printf("不相等\n");   // 输出:不相等  ✅

if (a = b)     printf("相等\n");     // ⚠️ 注意这里是 = 

上面 if (a = b) 干了什么?

  1. 把 b(20)赋给 a,a 变成 20;
  2. 赋值表达式的值是 20,非 0 为"真";
  3. 所以输出"相等"------逻辑完全错了,而且编译器通常只给一个警告。

通俗理解 :== 是"问一下:这两个一样吗?"(只看不动手);= 是"把右边的东西塞给左边"(动手搬运)。用错 = 就像你想问"你是不是王明?",结果直接把对方改名成王明。

防坑技巧 :写判断时把常量写在左边 ,即使误写 = 也会立刻报错:

c 复制代码
if (5 == a)   // ✅ 习惯写法
if (5 = a)    // ❌ 但编译器立刻报错:5 是常量不能被赋值 ------ 帮你抓住 Bug

3. 赋值表达式本身有"值"

a = b = c = 5; 这种多重赋值 之所以能写,就是因为"赋值"这个动作完成之后,还会返回一个结果(就是被赋的那个值)。

c 复制代码
int a, b, c;
a = b = c = 5;    // 从右往左:c=5 → b=5 → a=5
printf("%d %d %d", a, b, c);   // 输出:5 5 5

通俗理解 :像"接力传水",一桶水(5)从 c 传给 b,再传给 a,最后三个变量都是 5。

注意顺序:

c 复制代码
int a = 1, b = 2;
a = b = 10;      // b=10, 然后 a=10

4. 复合赋值运算符(简写形式)

完整写法 简写 说明
a = a + 3 a += 3 加
a = a - 3 a -= 3 减
a = a * 3 a *= 3 乘
a = a / 3 a /= 3 除
a = a % 3 a %= 3 取余

例子:统计购物车总价

c 复制代码
#include <stdio.h>
int main(void)
{
    int total = 0;        // 购物车总价
    total += 15;          // 加一本书 15 元
    total += 8;           // 加一支笔 8 元
    total += 32;          // 加一个杯子 32 元
    printf("总价 = %d 元\n", total);   // 输出:总价 = 55 元
    return 0;
}

为什么有用 :累加、累减是编程里最高频的操作(求和、计数、扣钱),+= 写起来短,读起来也清晰。

5. 赋值时的类型转换(自动"改装")

把一个值装进盒子时,如果盒子大小和值不匹配,C 会自动转换,可能丢东西。

c 复制代码
int a;
a = 3.99;        // a = 3   (小数部分被直接砍掉,不是四舍五入!)
c 复制代码
float f;
f = 10 / 3;      // f = 3.000000 !(10/3 是整数除法先算出 3,再转成 3.0)
c 复制代码
float f;
f = 10.0 / 3;    // f = 3.333333 ✅(有一个是小数就做小数除法)

通俗理解 :整数除法就像"分月饼只数整块",10 块月饼 3 人分,每人 3 块,剩下 1 块直接不管了。想要"精确分",就得让数字带小数点(10.0)。

6. 为什么学赋值语句?

  • 它是程序保存数据的唯一手段。不赋值,算出来的结果就没地方放。
  • = 与 == 混淆是笔试面试高频陷阱题。
  • 理解"赋值表达式有值",才能看懂 while ((c = getchar()) != EOF) 这种高级写法(见 4.2.2)。

4.2 数据的输入和输出

4.2.0 先搞懂一个底层概念:缓冲区 ★

知识点说明:你从键盘敲的东西,并不是"敲一个字符就立刻送到程序手里",而是先进入一块叫**缓冲区(buffer)**的"临时水池",等满足条件了才一次性交给程序。

通俗理解 :就像水龙头和水池 。你敲键盘是往水池里倒水,程序是接水的杯子。水池满了,或者你按了回车("放水阀"),水才会流到杯子里。这个机制叫行缓冲。

这有什么用? 后面 scanf 和 getchar/gets_s 混用时"莫名其妙跳过输入"的问题,根源全在缓冲区残留。理解了它,你就理解了本章 50% 的疑难杂症。


4.2.1 putchar 函数 ------ 输出一个字符

1. 函数原型

c 复制代码
int putchar(int c);   // 头文件:stdio.h

2. 功能与参数

项目 说明
功能 向屏幕(标准输出)输出一个字符
参数 一个字符或其 ASCII 码值
返回值 输出的字符的 ASCII 码;失败返回 EOF
头文件 #include <stdio.h>

3. 三种常见用法

c 复制代码
putchar('A');        // ① 直接写字符常量,输出 A
putchar(65);         // ② 写 ASCII 码值,65 就是 'A',输出 A
putchar('\n');       // ③ 输出转义字符,作用是换行(最常用!)

4. 完整例子:用 putchar 输出 "OK" 并换行

c 复制代码
#include <stdio.h>
int main(void)
{
    putchar('O');
    putchar('K');
    putchar('\n');     // 换行
    return 0;
}

运行结果:

复制代码
OK

5. 例子:把小写字母转成大写输出

c 复制代码
#include <stdio.h>
int main(void)
{
    char ch = 'a';
    putchar(ch - 32);    // 'a' 的 ASCII 是 97,减 32 变 65,即 'A'
    putchar('\n');
    return 0;
}

输出 :A

通俗理解 ASCII :字符在计算机里都对应一个编号,'a'=97,'A'=65,小写比大写正好大 32。所以"减 32"就是"变大写"。

6. putchar 与 printf 的选择

场景 用哪个
只输出一个字符、或拼字符 putchar(更轻快、更直观)
输出数字、字符串、带格式内容 printf

7. 为什么要学 putchar?

  • 它是最简单的输出函数,用来入门"输出"概念刚刚好。
  • 输出字符图形(星号三角形)、逐字符处理字符串时必须用它。
  • 理解了"字符 = 数字",就理解了字符运算的底层逻辑。

4.2.2 getchar 函数 ------ 输入一个字符 ★

1. 函数原型

c 复制代码
int getchar(void);   // 头文件:stdio.h

2. 关键点:为什么返回 int 而不是 char?

项目 说明
功能 从键盘(标准输入)读入一个字符
参数 无
返回值 读到的字符的 ASCII 码;读到末尾/出错返回 EOF
注意 每调用一次,只吃掉一个字符

原因(重要) :char 类型的取值范围是 -128~127,而 EOF 一般定义为 -1 。如果返回 char,读到的第 255 号字符可能被误判成 EOF,程序会提前结束。所以用 int 来"装得下 EOF"。

通俗理解 :就像用一个小瓶子装水,还要专门留出一格表示"没水了"。如果瓶子太满(用 char),就分不清"满瓶"和"空瓶"。用大一号的瓶子(int)就没这个问题。

3. 基础用法:读一个字符并显示

c 复制代码
#include <stdio.h>
int main(void)
{
    char ch;
    ch = getchar();     // 等你敲一个字符并按回车
    putchar(ch);
    putchar('\n');
    return 0;
}

运行示例:

复制代码
x↵
x

4. 用 getchar 清空缓冲区(超实用技巧)

c 复制代码
while (getchar() != '\n') ;   // 一直读,直到把回车('\n')吃掉为止

为什么需要它 :scanf("%d", &a) 读完数字后,你敲的回车还留在缓冲区里 。接着用 scanf("%s",...) 或 gets_s 时,它会立刻把那个回车当输入------程序就"跳过输入"了。上面这行代码就是"把水管里的剩水放干净"。

5. 完整案例:统计一行有多少个字符

c 复制代码
#include <stdio.h>
int main(void)
{
    int count = 0, ch;

    printf("请输入一行文字:");
    while ((ch = getchar()) != '\n')   // 不是回车就计数
        count++;

    printf("这一行共有 %d 个字符\n", count);
    return 0;
}

运行示例:

复制代码
请输入一行文字:hello world↵
这一行共有 11 个字符

逐句解释(这是本章最经典的写法,务必看懂):

  1. (ch = getchar()) 先把读到的字符赋给 ch;
  2. 整个赋值表达式的值就是那个字符;
  3. 再拿它和 '\n' 比较,不相等就继续循环。

通俗理解:像安检传送带------每件行李(字符)先拿出来放手上(赋值),再看是不是终点标志(回车),不是就继续往下传。

6. 为什么要学 getchar?

  • 它是控制"一个字符一个字符"处理的最基础手段(数字符、加密解密、判断大小写)。
  • 它是解决输入残留问题的万能钥匙(几乎所有混合输入 Bug 都靠它修)。
  • (ch = getchar()) != EOF 是读取"不定长度输入"的标准写法,面试必考。

4.2.3 printf 函数 ------ 格式化输出 ★★★

1. 函数原型

c 复制代码
int printf(const char *格式控制字符串, 输出表列);

2. 功能

按"格式控制字符串"指定的样式,把后面各项的值输出到屏幕。

一般格式:

c 复制代码
printf("普通文字%格式字符 普通文字%格式字符...", 值1, 值2, ...);

3. 格式控制字符串的两个成分

成分 作用 例子
普通字符 原样照打(含空格、标点、汉字) printf("结果是:");
格式声明 %... 按格式替换成后面的值 %d → 整数
c 复制代码
int a = 25;
printf("今年 %d 岁\n", a);
//   ↑原样  ↑替换  ↑原样 ↑换行
// 输出:今年 25 岁

4. 常用格式字符表(必须背熟)★

格式字符 输出类型 例子 输出结果
%d int 十进制整数 printf("%d", 25) 25
%ld long 长整型 printf("%ld", 123456L) 123456
%c char 单个字符 printf("%c", 'A') A
%s 字符串 printf("%s", "hello") hello
%f float/double 小数 printf("%f", 3.14) 3.140000
%e 指数形式小数 printf("%e", 1234.5) 1.234500e+03
%u 无符号整数 printf("%u", 100) 100
%o 八进制 printf("%o", 8) 10
%x 十六进制 printf("%x", 255) ff
%% 输出一个 % printf("80%%") 80%
%5d 右对齐,占 5 格 printf("[%5d]", 25) [ 25]
%-5d 左对齐,占 5 格 printf("[%-5d]", 25) [25 ]
%05d 不足补 0 printf("%05d", 25) 00025
%.2f 保留 2 位小数 printf("%.2f", 3.14159) 3.14
%8.2f 占 8 格,2 位小数 printf("[%8.2f]", 3.14159) [ 3.14]

格式总结口诀 :% [对齐-] [宽度] [.小数位数] 类型字符

5. 例1:输出一个学生的成绩单(表格对齐)

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("%-8s%-6s%-8s\n", "姓名", "学号", "成绩");
    printf("%-8s%-6d%-8.1f\n", "张三", 1001, 92.55);
    printf("%-8s%-6d%-8.1f\n", "李四", 1002, 78.44);
    return 0;
}

运行结果(整齐的表格):

复制代码
姓名    学号  成绩    
张三    1001  92.6    
李四    1002  78.4    

为什么要用 %-8s 而不是直接打空格 :用空格手动对齐,一旦字数不同就全乱;用宽度控制,让编译器自动补空格,永远整齐。

6. 例2:温度换算(展示 %f 的小数位控制)

c 复制代码
#include <stdio.h>
int main(void)
{
    float f = 98.6;                 // 华氏度
    float c = (f - 32) * 5 / 9;     // 摄氏度

    printf("华氏 %.1f 度 = 摄氏 %.2f 度\n", f, c);
    return 0;
}

输出 :华氏 98.6 度 = 摄氏 37.00 度

7. 例3:printf 的返回值(表示"输出了几个字符")

c 复制代码
#include <stdio.h>
int main(void)
{
    int n = printf("hello");   // 输出 5 个字符
    printf("\n输出字符个数 = %d\n", n);   // 5
    return 0;
}

8. 三大易错点

易错1:格式字符与参数类型必须一一对应、个数一致

c 复制代码
printf("%d %d", 5);        // ❌ 少一个参数 → 第二个是垃圾值
printf("%d", 3.5);         // ❌ 类型不匹配 → 输出乱码数字

易错2:% 后面漏写字母 / 忘了转义

c 复制代码
printf("完成 100%");       // ❌ % 后面没有格式字符,结果不可预期
printf("完成 100%%");      // ✅ 用 %% 表示一个 %

易错3:不要在 printf 里同时修改同一个变量(未定义行为)

c 复制代码
int i = 5;
printf("%d %d\n", i++, i++);   // ⚠️ 输出结果不确定!不同编译器不同

通俗理解 :就像让两个助手同时 从同一个盒子里取东西并放东西,谁先谁后没有规定,结果自然不可靠。这种写法考试题里常见,答案是"结果不确定/未定义"。

9. 为什么要学 printf?

  • 它是90% 的结果输出手段,不会 printf 就等于程序没长嘴。
  • 格式控制(对齐、精度)能做出专业报表,在数据类程序中非常实用。
  • "格式字符与参数匹配"是笔试基础题,printf 返回值、求值顺序是面试陷阱题。

4.2.4 scanf 函数 ------ 格式化输入 ★★★

1. 函数原型

c 复制代码
int scanf(const char *格式控制字符串, 地址表列);

2. 功能

按"格式控制字符串"规定的格式,从键盘读入数据,存入指定变量的内存地址。

3. 两个最重要的规则 ★★★

规则1:变量名前面必须加 &(取地址符)

c 复制代码
int a;
scanf("%d", &a);     // ✅ 正确:把读到的数放进 a 所在的"地址"
scanf("%d",  a);     // ❌ 错误:没有地址,程序不知道往哪放 → 运行崩溃

通俗理解 :寄快递必须写收件地址 ,光写收件人名字(变量名)快递员找不到地方。&a 就是"a 的收件地址"。

例外 :数组名本身代表地址,scanf("%s", str); 不加 &(字符串部分会讲)。

规则2:格式串里的普通字符,输入时必须原样敲

c 复制代码
scanf("%d,%d", &a, &b);    // 你必须输入:3,5 ↵
scanf("%d %d", &a, &b);    // 输入 3 5 ↵ 或 3↵5↵ 都行(空格=任意空白)
scanf("a=%d,b=%d", &a, &b); // 必须输入:a=3,b=5 ↵

通俗理解 :%d 是"填空处",其他字符是"印好的字"。你填表格时,印好的"姓名:"不能涂改,只能在空格处填内容。

4. 常用格式字符(与 printf 类似)

格式字符 含义 示例输入
%d int 整数 25
%ld long 123456
%f float 3.14
%lf double ⚠️ 3.14
%c 单个字符(含空格) A
%s 字符串(到空白为止) hello
%x 十六进制整数 ff

⚠️ scanf 里的特殊规定 :%f 对应 float*,%lf 对应 double* 。这和 printf 不同(printf 中 %f 就能输出 double)------这是高频考点。

5. 抑制符 *(只读不存)

c 复制代码
scanf("%*d %d", &a);    // 输入:10 20 ↗  → 10 被丢弃,a = 20

用途:跳过不需要的数据(比如跳过年份只取月份)。

6. 完整案例:输入长宽求矩形面积

c 复制代码
#include <stdio.h>
int main(void)
{
    float length, width, area;

    printf("请输入长和宽(用空格隔开):");
    scanf("%f %f", &length, &width);

    area = length * width;
    printf("长=%.2f 宽=%.2f 面积=%.2f\n", length, width, area);
    return 0;
}

运行示例:

复制代码
请输入长和宽(用空格隔开):3.5 2.0↵
长=3.50 宽=2.00 面积=7.00

7. 案例:输入"2025-06-18"这种格式的日期

c 复制代码
#include <stdio.h>
int main(void)
{
    int y, m, d;
    printf("请输入日期(格式 2025-06-18):");
    scanf("%d-%d-%d", &y, &m, &d);    // 普通字符 '-' 必须原样输入
    printf("%d年%d月%d日\n", y, m, d);
    return 0;
}

运行示例:

复制代码
请输入日期(格式 2025-06-18):2025-06-18↵
2025年6月18日

8. scanf 的四大坑 ★★

坑1:%c 会"吃掉"前面的空格和回车

c 复制代码
int a; char ch;
scanf("%d", &a);
scanf("%c", &ch);     // ⚠️ ch 里是残留的 '\n',而不是你想要的字符!

修法(三选一):

c 复制代码
scanf("%d %c", &a, &ch);      // 方法1:格式串中加个空格(推荐)
getchar();                    // 方法2:手动吃掉回车
scanf("%d", &a);
while (getchar() != '\n') ;   // 方法3:清空整行
scanf("%c", &ch);

坑2:%s 遇到空格就停止

c 复制代码
char name[20];
scanf("%s", name);
// 输入:Li Lei ↵   → name 里只有 "Li"," Lei" 留在缓冲区

→ 这正是下一节 gets_s 要解决的问题。

坑3:输入类型不匹配,程序直接卡死

c 复制代码
int a;
scanf("%d", &a);   // 你输入 abc ↗
// scanf 读不到数字,直接返回 0,a 里是垃圾值,缓冲区还留着 abc

坑4:输入个数不足,程序会一直等

c 复制代码
scanf("%d %d", &a, &b);   // 只输了一个 5 就回车,程序继续等第二个数

9. scanf 的返回值(有用!)

返回成功读入并赋值的数据个数 ,出错或遇到文件末尾返回 EOF。

c 复制代码
int a, b;
if (scanf("%d %d", &a, &b) == 2)
    printf("输入成功:%d + %d = %d\n", a, b, a + b);
else
    printf("输入格式错误!\n");

为什么有用 :这是防御性编程的第一步------先检查输入是否合法,再做计算。写正式程序必备。

10. 为什么要学 scanf?

  • 它是程序接收用户数据的主要入口,没有输入的程序只能算固定答案。
  • 格式串中"普通字符原样输入"这一特性,能一次性解析 2025-06-18、a=1,b=2 这类结构化输入,非常强大。
  • & 号、%lf、%c 吃回车,是笔试面试的"必考三连"。

4.2.5 puts 函数 ------ 输出一行字符串

1. 函数原型

c 复制代码
int puts(const char *str);   // 头文件:stdio.h

2. 功能与特点

特点 说明
输出内容 输出一个字符串
自动换行 输出完自动追加一个换行 '\n'(和 printf 不同!)
遇到 '\0' 停止 输出到字符串结束标志为止
返回值 非负值成功,EOF 失败

⚠️ 易错理解 :puts 不是把 '\0' 显示成换行,而是输出完字符串后自动加一个 \n 。'\0' 本身是"字符串的句号",不显示。

3. 基础用法对比

c 复制代码
#include <stdio.h>
int main(void)
{
    puts("Hello, C!");          // 输出并自动换行
    printf("Hello, C!");        // 输出但不换行
    printf("\n");               // 需手动换行
    printf("%s\n", "Hello, C!"); // 等价于 puts("Hello, C!");
    return 0;
}

输出:

复制代码
Hello, C!
Hello, C!
Hello, C!

4. 案例:输出个人名片

c 复制代码
#include <stdio.h>
int main(void)
{
    puts("======== 个人名片 ========");
    puts("姓名:张三");
    puts("职业:C语言学习者");
    puts("==========================");
    return 0;
}

为什么要用 puts :每一行都要换行时,puts 比 printf 短,也更清楚地表达"我只是打一行字"的意图。

5. puts 和 printf 怎么选?

需求 用哪个
只输出一个完整字符串,且要换行 puts(简洁)
要拼数字、格式化、不换行 printf
输出字符串中间带变量 只能 printf

6. 为什么要学 puts?

  • 它是最省事的"打印一行文字"方式,写提示信息时特别好用。
  • 理解"字符串以 \0 结尾"这个核心概念,是后面学习字符串、数组、指针的基石。
  • 与 printf、puts、gets_s、scanf("%s") 的区别是面试常问对比题。

4.2.6 C11 标准函数 gets_s ------ 安全地读入一行 ★★

1. 背景:为什么会有 gets_s?

旧函数 gets() 的致命缺陷 :它不检查缓冲区大小 ,你输入多少它就往数组里塞多少。输入一长串,就会溢出数组边界 ,覆盖掉别人的内存------这是著名的"缓冲区溢出漏洞",是很多病毒攻击的入口。因此 C11 标准正式删除了 gets()。

通俗理解 :gets() 像一个不看容量就倒水的人 ,你给它一个小杯子,它不管照样倒,水漫出来弄坏旁边的设备。gets_s() 则会先问"杯子多大?",倒到快满就停手。

2. 函数原型

c 复制代码
char *gets_s(char *str, rsize_t n);
// 参数1:存放字符串的数组(缓冲区首地址)
// 参数2:最多读入的字符个数(含结尾 '\0')
// 返回值:成功返回 str;失败或读到 EOF 返回 NULL

头文件 :#include <stdio.h>(部分编译器需 <stdlib.h>,MSVC 支持最好)

3. 功能与行为 ★

行为 说明
读取范围 读入一整行 ,直到遇到回车 '\n'
能读空格和制表符 这是它相对 scanf("%s") 的最大优势
换行处理 丢弃读到的 '\n',替换成 '\0'
越界保护 输入超过 n-1 个字符时报错/清空,不会溢出

4. 基础案例:读入并显示完整姓名

c 复制代码
#include <stdio.h>
int main(void)
{
    char name[20];

    printf("请输入你的姓名:");
    gets_s(name, 20);          // 最多读 19 个字符 + 1 个 '\0'

    printf("你好,%s!\n", name);
    return 0;
}

运行示例:

复制代码
请输入你的姓名:Li Lei↵
你好,Li Lei!

对比 :若换成 scanf("%s", name);,输入 Li Lei 只会得到 Li," Lei" 被丢在缓冲区------这正是 gets_s 的价值。

5. 案例:读入一句话并输出

c 复制代码
#include <stdio.h>
int main(void)
{
    char sentence[100];

    printf("请输入一句话:");
    gets_s(sentence, 100);

    puts("你输入的是:");
    puts(sentence);
    return 0;
}

运行示例:

复制代码
请输入一句话:I love C language↵
你输入的是:
I love C language

6. gets_s 与"兄弟函数"对比表 ★★★

函数 能否读空格 遇回车 是否安全 是否保留 \n 备注
gets() ✅ 能 丢弃 ❌ 极不安全,C11 删除 否 不要用!
gets_s(str, n) ✅ 能 丢弃 ✅ 安全 否 本章推荐
fgets(str, n, stdin) ✅ 能 保留 ✅ 安全 是 标准 C,跨平台最稳
scanf("%s", str) ❌ 遇空格断开 留在缓冲区 ⚠️ 有隐患 --- 快速读单词

额外提醒 :如果编译器不支持 gets_s(如老版 GCC),请用 fgets 代替,用法:

c 复制代码
fgets(name, 20, stdin);   // 会把末尾的 '\n' 也读进来,需手动去掉

7. 为什么要学 gets_s?

  • 安全意识:知道"不检查边界的输入函数"是安全隐患,这是程序员的基本素养。
  • 能读带空格的整行文本 ,而 scanf("%s") 做不到。
  • gets / gets_s / fgets / scanf("%s") 的区别是面试必考对比题。

4.3 技术解惑

4.3.1 scanf 函数和 gets_s 函数的区别 ★★★

这是本章最核心的对比,很多初学者分不清。请从 5 个维度理解:

1. 对比表

对比维度 scanf("%s", str) gets_s(str, n)
读取范围 只读到第一个空白字符为止 读一整行(直到回车)
能否读空格 ❌ 不能,空格是"结束符" ✅ 能,空格只是普通字符
能否读制表符/回车 都会终止读取 制表符能读,回车终止
结尾处理 自动加 '\0' 丢弃 '\n',自动加 '\0'
长度限制 可用 %19s 限制,否则不安全 必须给长度参数 n,安全
格式化能力 强(可 %d%s%f 混用) 弱(只读字符串)
典型用途 读单词、读"姓名 成绩"这类结构 读整句、地址、自我介绍

2. 最直观的实验对比

c 复制代码
#include <stdio.h>
int main(void)
{
    char a[50], b[50];

    printf("用 scanf 输入:");
    scanf("%s", a);                    // 输入:Hello C World

    while (getchar() != '\n') ;        // 清掉缓冲区残留

    printf("用 gets_s 输入:");
    gets_s(b, 50);                     // 输入:Hello C World

    printf("scanf 结果:[%s]\n", a);   // [Hello]
    printf("gets_s 结果:[%s]\n", b);  // [Hello C World]
    return 0;
}

运行示例:

复制代码
用 scanf 输入:Hello C World↵
用 gets_s 输入:Hello C World↵
scanf 结果:[Hello]
gets_s 结果:[Hello C World]

通俗理解:

  • scanf("%s") 像一个只认"单词"的收银员,看到空格就认为这条结束了,剩下的一半留给下一位顾客。
  • gets_s 像一个认真抄写整行便签的人,空格、标点都照抄,直到你按下回车才停笔。

3. 什么时候用哪个?(实用口诀)

你想输入什么 用什么
一个单词(姓名、密码、英文名) scanf("%s", ...)
一整句话、地址、留言 gets_s(...)
数字和字符串混着输(如"张三 90") scanf("%s %d", ...)
需要跨平台、编译器不支持 gets_s fgets(...)

4. 混合使用的经典 Bug 及解法

Bug 场景 :先 scanf("%d") 读年龄,再 gets_s 读姓名。

c 复制代码
int age;
char name[20];
scanf("%d", &age);    // 你输入:20 ↵
gets_s(name, 20);     // ⚠️ 直接跳过!name 里是空串

原因 :scanf("%d") 只吃掉了 20,那个回车 '\n' 还留在缓冲区 ;gets_s 一看立刻有"一行结束了"的信号,于是读到一个空行。

三种修法:

c 复制代码
// 方法①:加一个空格在格式串中(只对后续 scanf 有效)
scanf("%d ", &age);

// 方法②:吃掉一个字符(简单场景)
scanf("%d", &age);
getchar();

// 方法③:清空整行(最稳妥,强烈推荐)★
scanf("%d", &age);
while (getchar() != '\n') ;
gets_s(name, 20);

5. 为什么这个知识点重要?

  • 这是**"输入函数选型"的核心判断依据**,写真实项目时天天用。
  • 缓冲区残留是初学 C 语言最让人抓狂的现象,理解原理后一通百通。
  • 面试中"scanf 和 gets 的区别"几乎是必考题。

4.3.2 C 语言的输入和输出问题(常见疑难汇总)

疑难1:为什么 C 语言"没有"专门的输入输出语句?

答 :C 语言的核心只提供运算、控制、函数调用 等机制,输入输出全部由标准库函数 (scanf、printf 等)实现。

通俗理解 :C 语言本体只提供"发动机和轮子",收音机、空调(I/O 功能)都是选装配件 (库函数)。所以必须写 #include <stdio.h> 来"取配件"。

实际意义:这也让 C 语言的 I/O 可以被替换(如换文件、换网络),非常灵活。

疑难2:为什么必须要写 #include <stdio.h>?

答 :stdio.h 是 standard input output header (标准输入输出头文件),里面是 printf、scanf 等函数的说明书(声明) 。不包含它,编译器不知道 printf 怎么用,会报"未声明的标识符"。

疑难3:输入输出是"函数",效率会不会低?

答 :scanf/printf 确实比纯计算慢(涉及系统调用和格式解析),但对普通程序完全够用。竞赛/高频数据场景 会用更快的 getchar 手写读入、putchar/puts 输出。

效率排序(快 → 慢):

复制代码
getchar 手写解析  >  gets_s / puts  >  scanf("%s")  >  printf 格式化

疑难4:为什么输出的浮点数总是"3.140000"?

答 :%f 默认保留 6 位小数 。用 %.2f 控制精度即可。

c 复制代码
printf("%.2f", 3.14159);   // 3.14

疑难5:为什么 printf 的参数求值顺序不确定?

答 :C 标准没有规定 函数实参的求值顺序(可能从左到右,也可能从右到左),由编译器决定。所以 printf("%d %d", i++, i++) 的结果是未定义的,不同编译器结果不同。

通俗理解:标准只是说"这些材料都要下锅",但没规定"先拿哪一样"。所以你不能指望某种特定顺序。

正确做法 :一次只做一件事。

c 复制代码
printf("%d ", i);
i++;
printf("%d\n", i);   // ✅ 清晰、无歧义

疑难6:为什么 scanf 读字符串不要加 &?

答 :数组名本身就已经是"地址",str 等价于 &str[0]。再加 & 就变成了"地址的地址",类型不匹配,会报警告甚至崩溃。

c 复制代码
char name[20];
scanf("%s",  name);     // ✅
scanf("%s", &name);     // ❌ 类型是 char(*)[20],不匹配

疑难7:输入被"跳过"了怎么办?(万能排查清单)

  1. 上一步是不是用了 scanf("%d/%f")?→ 用 while(getchar()!='\n'); 清缓冲区
  2. 是不是用了 scanf("%c")?→ 格式串前加空格 " %c"
  3. 是不是格式串里的普通字符没按格式敲?→ 检查 -、,、a=
  4. 是不是输入类型不对(该输数字却输了字母)?→ 用 scanf 返回值检查

疑难8:Windows 下的 getchar() 为什么"读不到"东西?

答:有些 IDE(如老版 VS)在程序结束时窗口一闪而过,其实数据读到了,只是你看不到。解决方法:

c 复制代码
printf("按回车退出...");
getchar();     // 用一次 getchar "卡住"窗口
return 0;

或使用 system("pause");(Windows 专用)。

为什么要看"技术解惑"?

  • 这些都是真实学习中一定会遇到、但课本正文很少讲清的问题。
  • 每个疑难背后都藏着一个核心机制(缓冲区、类型匹配、标准规定),理解机制比记答案更有用。
  • 面试官爱从这些"看起来不起眼"的细节切入,考察你是否真的懂 C。

4.4 常见面试题解析

【面试题1】= 和 == 有什么区别?下面代码输出什么?

c 复制代码
int a = 1, b = 2;
if (a = b)  printf("Yes");
else        printf("No");

答:

  • = 是赋值运算符 ,== 是关系运算符(判断相等)。
  • a = b 把 b(2)赋给 a,整个赋值表达式的值为 2(真)。
  • 输出:Yes ,同时 a 变成 2。

加分点 :提示用 if (2 == a) 这种"常量在左"的写法来避免此类错误。


【面试题2】下面的 printf 输出什么?

c 复制代码
int i = 3;
printf("%d %d %d\n", i++, i++, i++);

答 :结果是未定义的(未定义行为) ,不同编译器可能输出 5 4 3、3 4 5 或其他值。

原因 :C 标准没有规定函数实参的求值顺序。不要这样写,应该分开写:

c 复制代码
printf("%d %d %d\n", i, i, i); i += 3;

【面试题3】putchar('A' + 1) 会输出什么?

答 :输出 B。

解释 :字符 'A' 的 ASCII 码是 65,'A' + 1 = 66,66 对应的字符是 'B'。putchar 参数可以是字符常量,也可以是 ASCII 整数值。


【面试题4】getchar() 的返回类型为什么是 int 而不是 char?

答 :因为要区分"读到的字符 "和"出错/文件结束(EOF) "。EOF 通常是 -1,而 char 只有 8 位(-128~127 或 0~255),可能与合法字符值冲突。用 int 能确保 EOF 唯一可辨识。

延伸:因此标准读入写法是

c 复制代码
int ch;
while ((ch = getchar()) != EOF) { ... }

【面试题5】写出下面语句的输出

c 复制代码
printf("[%5.2f]\n", 3.14159);
printf("[%-5.2f]\n", 3.14159);
printf("[%05d]\n", 42);

答:

复制代码
[ 3.14]
[3.14 ]
[00042]

解释 :5.2 表示总宽度 5、保留 2 位小数 ;- 表示左对齐 (不足补空格在右侧);05 表示宽度 5、不足补 0。


【面试题6】scanf、gets、gets_s、fgets、scanf("%s") 的区别?

答题要点(对比维度从 4 个方面展开):

维度 要点
读取范围 scanf("%s") 遇空白停止;其余读整行到回车
空格处理 scanf("%s") 不读空格;gets/gets_s/fgets 读空格
安全性 gets 无长度检查,C11 已删除 ;gets_s/fgets 有长度限制
换行符 gets/gets_s 丢弃 \n;fgets 保留 \n

标准答案推荐 :用 fgets 或 gets_s 代替 gets;用 scanf("%s") 只在确定输入无空格时使用。


【面试题7】puts 和 printf 的区别?

答:

  1. puts 只能输出一个字符串 ,printf 可以格式化输出多种类型。
  2. puts 自动加换行 ,printf 需要手动 \n。
  3. puts 遇 '\0' 停止输出,printf("%s", ...) 同理。
  4. puts 更简洁,printf 更灵活。

【面试题8】下面的程序能读入完整姓名吗?为什么?如何修改?

c 复制代码
char name[20];
scanf("%s", name);

答 :不能 。scanf("%s") 遇空格就停止,输入 Li Lei 只会读入 Li,剩下的 Lei 留在缓冲区,还会污染下一次输入。

修改:

c 复制代码
gets_s(name, 20);            // 方法1
// 或
scanf("%19[^\n]", name);     // 方法2:读到回车为止
// 或
fgets(name, 20, stdin);      // 方法3(会多读一个 '\n')

【面试题9】编程题:用 getchar 统计输入中的字母、数字、其他字符个数

c 复制代码
#include <stdio.h>
#include <ctype.h>
int main(void)
{
    int ch, letter = 0, digit = 0, other = 0;

    printf("请输入一行文字:");
    while ((ch = getchar()) != '\n')
    {
        if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
            letter++;
        else if (ch >= '0' && ch <= '9')
            digit++;
        else
            other++;
    }
    printf("字母:%d 数字:%d 其他:%d\n", letter, digit, other);
    return 0;
}

考察点 :getchar 循环读入、字符的 ASCII 范围判断、复合语句的使用。


【面试题10】编程题:交换两个变量的值(三种方法)

c 复制代码
// 方法1:中间变量法(最易懂,推荐)
int t = a; a = b; b = t;

// 方法2:加减法(无额外变量,但可能溢出)
a = a + b; b = a - b; a = a - b;

// 方法3:异或法(无额外变量,不会溢出)
a = a ^ b; b = a ^ b; a = a ^ b;

考察点 :赋值语句的执行顺序、表达式求值。面试建议:先说方法1,再展示方法3 更显功底。


4.5 课后练习

一、选择题

1. 以下哪个是正确的赋值语句?

A. a + b = c; B. a = b = 5; C. 5 = a; D. a = ;

2. printf("%5.2f", 3.14159); 的输出是( )

A. 3.14 B. 3.14 C. 3.14159 D. 03.14

3. scanf("%d,%d", &a, &b); 要求用户输入( )

A. 3 5 B. 3,5 C. 3;5 D. 35

4. 下列关于 gets_s 的说法正确的是( )

A. 会把输入的 \n 也保存到数组中

B. 可以读入包含空格的字符串

C. 不需要包含 stdio.h

D. 和 gets() 用法完全一样

5. getchar() 的返回值类型是( )

A. char B. int C. void D. double

参考答案

  1. B(A 左边不是变量;C 左边是常量;D 右边表达式为空)
  2. B(总宽 5、2 位小数,右对齐补 1 个空格)
  3. B (格式串中的 , 必须原样输入)
  4. B (gets_s 丢弃 \n,需 stdio.h,比 gets() 多了长度参数)
  5. B (为容纳 EOF,必须是 int)

二、判断题(对的打 √,错的打 ×)

  1. putchar() 一次可以输出一整个字符串。( )
  2. puts("hi") 输出完会自动换行。( )
  3. scanf("%d", &a); 中的 & 可以省略。( )
  4. printf 的返回值是输出内容的字符个数。( )
  5. gets() 是 C11 标准推荐使用的安全函数。( )
  6. scanf("%f", &d); 可以用来读入 double 变量 d。( )
  7. 赋值语句 a = 5; 执行后,表达式 a = 5 的值是 5。( )

参考答案

  1. × (putchar 一次只输出一个字符)
  2. √
  3. × (普通变量必须加 &,数组名除外)
  4. √
  5. × (gets 因不安全已被 C11 删除)
  6. × (double 必须用 %lf)
  7. √

三、填空题

  1. C 语句分为 5 类:控制语句、、表达式语句、、____。
  2. printf("%c", 66); 的输出是 ______。
  3. 要用 scanf 读入一个 double 类型变量 x,应写 scanf("____", &x);。
  4. 输入函数 gets_s 的两个参数分别是 ______ 和 ______。
  5. 想清空输入缓冲区,可以使用语句 while (getchar() != '____') ;。

参考答案

  1. 函数调用语句;空语句;复合语句
  2. B
  3. %lf
  4. 字符数组首地址(缓冲区);最多读入的字符数 n
  5. \n

四、写出下列程序的运行结果

1.

c 复制代码
#include <stdio.h>
int main(void)
{
    int a = 3, b = 5, t;
    t = a; a = b; b = t;
    printf("%d %d\n", a, b);
    printf("%d\n", a = b = 10);
    return 0;
}

2.

c 复制代码
#include <stdio.h>
int main(void)
{
    int i = 3;
    printf("%d\n", i = i + 1);
    printf("%5d|%05d|%-5d|\n", i, i, i);
    printf("%.2f%%\n", 3.14159);
    return 0;
}

3. (假设输入为 12 34)

c 复制代码
#include <stdio.h>
int main(void)
{
    int a, b;
    scanf("%2d%2d", &a, &b);
    printf("%d %d\n", a, b);
    return 0;
}

参考答案

1.

复制代码
5 3
10

解释:交换后 a=5, b=3;a = b = 10 使 b=10, a=10,赋值表达式的值为 10。

2.

复制代码
4
    4|00004|4    |
3.14%

解释:i = i + 1 后 i=4;%5d 右对齐、%05d 补零、%-5d 左对齐;%% 输出百分号。

3.

复制代码
12 34

解释:%2d 表示最多读 2 位数字 ,所以前两位 12 给 a,接着 34 给 b。


五、程序填空

1. 读入一个字符,若是小写字母则转成大写输出。

c 复制代码
#include <stdio.h>
int main(void)
{
    char ch;
    ch = ______;                 // ① 读入一个字符
    if (ch >= 'a' && ch <= 'z')
        ch = ______;             // ② 转大写(提示:减去一个数字)
    ______(ch);                  // ③ 输出
    putchar('\n');
    return 0;
}

2. 读入整行文字并原样输出。

c 复制代码
#include <stdio.h>
int main(void)
{
    char str[100];
    printf("请输入:");
    ______(str, 100);            // ④ 安全读入一整行
    puts(______);                // ⑤ 输出
    return 0;
}

参考答案

① getchar() ② ch - 32 ③ putchar ④ gets_s ⑤ str


六、编程题

1. 编写程序:输入圆的半径 r(小数),计算并输出面积和周长(保留 2 位小数)。

提示:面积 πr²、周长 2πr,取 π = 3.14159。

2. 编写程序:输入两个整数,输出它们的和、差、积、商(商保留 2 位小数)。

3. 编写程序:用 getchar 逐个读入一行字符,倒序输出。

4. 编写程序:输入"姓名 语文 数学 英语"(如 张三 90 85 92),输出姓名和三科平均分(1 位小数)。

5. 编写程序:输入华氏温度 F,输出摄氏温度 C = 5(F-32)/9(保留 1 位小数)。

6. (提高)输入一串字符,用 putchar 输出由 * 组成的直角三角形(行数由输入决定)。

例如输入 3,输出:

复制代码
*
**
***

参考答案(编程题)

1.

c 复制代码
#include <stdio.h>
int main(void)
{
    float r, area, perimeter;
    printf("请输入半径:");
    scanf("%f", &r);
    area = 3.14159f * r * r;
    perimeter = 2 * 3.14159f * r;
    printf("面积=%.2f 周长=%.2f\n", area, perimeter);
    return 0;
}

2.

c 复制代码
#include <stdio.h>
int main(void)
{
    int a, b;
    printf("请输入两个整数:");
    scanf("%d %d", &a, &b);
    printf("和=%d 差=%d 积=%d\n", a + b, a - b, a * b);
    if (b != 0)
        printf("商=%.2f\n", (float)a / b);   // 注意:转成 float 才是精确除法
    else
        printf("除数不能为 0!\n");
    return 0;
}

3.

c 复制代码
#include <stdio.h>
int main(void)
{
    char s[100];
    int i, len = 0;

    printf("请输入一行:");
    while (len < 99 && (s[len] = getchar()) != '\n')
        len++;

    printf("倒序输出:");
    for (i = len - 1; i >= 0; i--)
        putchar(s[i]);
    putchar('\n');
    return 0;
}

4.

c 复制代码
#include <stdio.h>
int main(void)
{
    char name[20];
    int a, b, c;

    printf("请输入姓名 语文 数学 英语:");
    scanf("%s %d %d %d", name, &a, &b, &c);   // %s 不加 &
    printf("%s的平均分是 %.1f\n", name, (a + b + c) / 3.0);
    return 0;
}

5.

c 复制代码
#include <stdio.h>
int main(void)
{
    float F, C;
    printf("请输入华氏温度:");
    scanf("%f", &F);
    C = 5.0f * (F - 32) / 9;
    printf("摄氏温度 = %.1f\n", C);
    return 0;
}

6.

c 复制代码
#include <stdio.h>
int main(void)
{
    int n, i, j;
    printf("请输入行数:");
    scanf("%d", &n);

    for (i = 1; i <= n; i++)
    {
        for (j = 1; j <= i; j++)
            putchar('*');
        putchar('\n');
    }
    return 0;
}

附:本章核心速查卡

函数 头文件 作用 关键注意
putchar(c) stdio.h 输出 1 个字符 参数可为字符或 ASCII 码
getchar() stdio.h 输入 1 个字符 返回 int ,用于判断 EOF
printf(fmt, ...) stdio.h 格式化输出 %d %c %s %f;% 要写成 %%
scanf(fmt, ...) stdio.h 格式化输入 变量加 & ;double 用 %lf
puts(str) stdio.h 输出字符串 自动换行
gets_s(str, n) stdio.h 安全读入一行 能读空格;替代不安全的 gets()

本章三条铁律:

  1. = 是赋值,== 是比较,别混。
  2. scanf 读普通变量要加 &;格式串里的普通字符要原样输入。
  3. 混合输入时,记得清缓冲区:while (getchar() != '\n') ;
相关推荐
道尔柯南1 小时前
C++ 入门基础详解:从第一个程序到核心语法特性
开发语言·c++
盛世宏博智慧档案1 小时前
Modbus TCP+PoE以太网温湿度传感器项目部署全流程,附网络调试实操
开发语言·tcp/ip·php·以太网·传感器·温湿度
ACP广源盛139246256731 小时前
Type-C 扩展坞方案选型笔记|国产 DP1.2 转 HDMI2.0 桥接芯片 GSV2201D@ACP 评估
开发语言·笔记·硬件架构·硬件工程·hdmi·国产芯片
旖旎夜光1 小时前
【LangGraph实战】LangGraph 学习笔记(四):持久化——从线程记忆到跨会话长期记忆
人工智能·笔记·python·学习·ai编程·langgraph
kdxiaojie1 小时前
MPU6050学习
笔记·学习·mpu6050
yuniko-n1 小时前
【JUC】线程间通信
java·开发语言
yuniko-n2 小时前
【JUC】关于 Java 线程介绍
java·开发语言
geovindu2 小时前
rust: Borg Pattern(续)
开发语言·后端·设计模式·rust·博格模式
aqiu1111113 小时前
【C++算法打怪专栏】8. 字符串转换整数 (atoi)
开发语言·c++·算法