【在更】基础 | C 语言笔记(使用 Visual Studio 2019)

一、简介

学习资源

王道训练营-C语言教程

历史

语言:C 语言 ← B 语言 ← A 语言
开发环境:编辑器 + 编译器 + 调试器
系统:Linux ← UNIX

不同语言之间的区别

C/C++:可移植性强的语言,Windows 和 Linux 的接口有差异,汇编语言。
Java/Python:跨平台语言,可从 Windows 平台移植到 Linux 平台使用

学习目标

  1. 理解程序的执行过程
  2. 掌握程序调试能力

断点及调试窗口设置

内存的变化过程

在编译器行号左侧灰色部分点击出现红点(断点)。设置断点
当程序运行到断点时(上图出现黄色箭头),执行步骤截止至断点之前。此时 i 的地址内容未发生改变。
i 的地址
i 的地址所指向的内容
点击逐过程,程序运行下一步后,i 地址的内存发生改变,变为 i 的取值 10(十六进制表示为 0a。小端显示:低地址在前,高地址在后,实际存储内容为 00 00 00 0a)。
逐过程
存储内容改变

点击逐过程,程序执行输出 hello world
点击逐过程 2
输出:
hello world 打印结果
点击逐过程,程序运行 2019 版隐藏的 system("pause"),打印【按任意键关闭此窗口】。此时程序执行 C 语言内镶嵌语言的命令 pause按任意键关闭此窗口
C 语言也可嵌套其他语言,如微软的语言 BAT(微软处理脚本)、Python 等。

Debug 模式与 Release 模式的区别

  • Debug 版本的存在是为了方便程序员开发和调试,性能和体积不是它的重点;
  • Release 版本是最终交给用户的程序,性能和体积是需要重点优化的两个方面。

因此只有在 Debug 模式下才能对代码进行调试。

逐过程(F10)与逐语句(F11)的区别

  • 逐过程(F10):当前函数一步一步往下走。
  • 逐语句(F11):到达某个函数时需要进入函数。
    进入函数后可通过跳出(Shift+F11)跳出函数,此时函数内接下来的代码被快速执行后回到上一级函数。
  • 继续:直接跳到下一个断点。

声明子函数

格式void 函数名(传递的参数) {函数内容};
注意:当传递数组时,参数部分表示为 数组名[] 即可。因为传递数组时,子函数并不知道数组长度,所以数组长度需要另外定义与传递,即 void 函数名(数组名[],数组长度的变量名) {函数内容};

C/C++/Java 出错

编译错误:代码编写错误,无法编译生成
执行错误:打印结果不符合预期

编译原理解析

程序的编译过程

程序的编译过程

关于编译器解决方案与项目

  • 一个解决方案可以存在多个项目;一个项目可以存在多个 .c 文件,但只能存在一个 main 函数。
  • 添加已有 .c 文件:①拖拽进源文件 ②源文件右击添加现有项。
  • .obj 文件(目标文件):存储及其指令(0/1 字符串)。位于项目的 Debug 文件夹中,生成失败依旧会产生 .obj 文件。
  • 将需要运行的项目右击选择“设为启动项目”,调试时默认运行该项目。
  • 编译器中的“生成”为增量编译,“重新生成”为重新编译。

编译与链接的区别:

  • 编译:C 语言 → 汇编语言 → 机器指令。
  • 链接:将函数符号替换为地址。

报错时出现 error LNK2019 为链接错误。双击报错可定位,可能是链接源的错误或链接位置的错误。

二、数据类型、运算符与表达式

学习目标

  • 数据类型
  • 常量与变量的差异
  • 整型、浮点型、字符型的原理及应用
  • scanf 函数的原理及应用
  • 运算符与表达式

2.1 数据类型的分类

数据类型的分类

2.2 常量

程序运行过程中值不发生改变的量。

例:int i = 10 10 为常量 / 立即数

内存
32 位控制台应用程序的地址范围
PC 指针:程序计数器,存储一个地址值,指向当前马上要给 CPU 译码器的指令。每执行一条则向下走一条指令(每条指令不一定等长)。

译码器读取 i = 10,在栈空间开辟 4 字节内存,将 10 复制到栈空间。
变量:在栈空间上有一个存储的位置,可改变。
常量:在编译后即被放入译码器的代码段,不可改变。

64位文件可用的虚拟地址空间大小: 2 32 × 4   G 2^{32}×4\ G 232×4 G

2.3 变量

  • 变量名:以一个名字代表一个对应的存储单元地址。不能与关键字同名。
  • 从变量中取值:通过变量名找到内存中存储单元的地址,并从该存储的单元中读取数据。
  • 命名规则:标识符只能由字母、数字和下划线组成,第一个字符必须为字母或下划线。
  • C语言区分大小写。
  • 先定义后使用。

2.4 整型数据

2.4.1 符号常量

定义:#define 变量名 常量(末尾不加分号)。
程序中用到的常量名位置直接替换为常量内容。

例:输出为 7
输出为 7 程序
3+2*2=7
其中,查看预处理效果方法:右击项目 → 属性 → 配置属性 → C/C++ / 预处理文件 → 是。重新生成后在项目 → Debug 文件夹 → 打开 main.i 文件,前几万行为头文件(.c 文件中开头带 # 行)的展开,文件末尾为预处理结果即需要查看的部分 → 查看后将“是”改回为“否”,否则之后运行报错。

作用:提高程序可读性、便于修改。

2.4.2 整型常量的不同进制表示 2 / 4 / 8 / 16

进制转换
1个字节(byte) = 8 位(bit)。
1 KB = 1024 byte;1 MB = 1024 KB;1 GB = 1024 MB。

CPU 寻址时最小访问单元为 1 字节。
数据总线每次从内存中取 8 字节。
内存为小端模式存储。

输出表示
%d 表示十进制
%o 表示八进制
%x 表示十六进制

2.4.3 补码

CPU 中的处理单元:译码器、加法器、乘法器等,不存在减法器。进行减法运算需要使用加法器与补码。

  • 补码是计算机对负数的表示方式,是原码(去掉符号后字符的二进制表示)取反后加 1 的结果。
  • 当最高位为 1 时表示负数。

例:2-5=2+(-5),存储 (-5) 时需要用补码表示,即需要将原码 5=0x0000 0005 取反后得 0xffff fffa,加 1 后得 -5=0xffff fffb。加 2(有进位)后得到计算结果 0xffff fffd。
        当最高位为 1 时,要得到原码才能知道 0xffff fffd 的值,即堆其取反后加 1(也可减 1 后取反或取反后加 1,结果相同)得到 3,所以其值为 3。

注:Windows 中先定义的地址较大,微软在两个变量间设置了 8 个字节的保护空间。

2.4.4 整型变量

整型变量包括 6 种类型,分别为

  • 有符号基本整型 (signed) int
  • 有符号短整型 (signed) short (int)
  • 有符号长整型 (signed) long (int)
  • 无符号基本整型 unsigned int
  • 无符号短整型 unsigned short (int)
  • 无符号长整型 unsigned long (int)

注:括号表示其中的内容是可选的。

有符号基本整型与无符号基本整型的最高位所代表的意义如下
有符号基本整型与无符号基本整型的最高位所代表的意义
不同整型变量表示的整型数范围如下表,超出范围会发生溢出导致计算出错。

只有整型数会产生溢出

不同整型变量表示的整型数范围
注:32 位的 long 长 4 字节,64 位的 long 长 8 字节。一般为 64 位(服务器)。

:将 short 类型的 i=32767 加 1,得到 -32768。
32767+1
输出
-32768
        32767 转换为二进制表示为 0111 1111 1111 1111,加 1 后得到 1000 0000 0000 0000,最高位为符号位,表示 -32768。
        -32768 的原码与补码相同。

注:有符号类型输出时用%d(十进制)、%o(八进制)、%x(十六进制)==,无符号类型输出时用%u,具体如下:
输出字符格式

2.5 浮点型数据(精度丢失)

2.5.1 浮点型常量

两种形式如下,常用的为指数形式
表示浮点型常量的两种形式
注意:字母 e(或 E)表示 10 的幂次,e 前底数部分必须有数字,e 后的指数必须为整数。

注:形如 -.1e-3 为正确写法,表示 -0.0001。

2.5.2 浮点型变量

浮点数的数值范围有效数字
IEEE-754 浮点型变量存储标准
浮点型数据的组成(标准 4 字节浮点型 float)
浮点型数据的组成
指数部分表示 2 的幂次,小数部分表示输入数值的二进制表示中的小数部分。

浮点型数据按照指数形式存储

  • 系统把一个浮点型数据分成小数部分(M)和指数部分(E)并分别存放。
  • 指数部分采用规范化的指数形式,指数也分正、负(符号位,用 S 表示)。

IEEE-754 浮点型变量存储标准(以 4.5=0100 0000 1001 0000 0000 0000 0000 0000 为例):
IEEE-754 浮点型变量存储标准
S:符号位。S=1 时为负( ( − 1 ) 1 (-1)^{1} (1)1),S=0 时为正( ( − 1 ) 0 (-1)^{0} (1)0)。
E:指数部分。十进制数输入转为二进制数。存储前,指数部分都要加 127=0111 1111(IEEE-754 规定),因为需要表示负数。
M:小数部分。在二进制数小数点前保留一位(即小数点前有且只有一个 1)的前提下,小数点后的部分被存储在这里。

十进制数 4.5 分为整数部分 4 与小数部分 0.5,二进制表示为 100.1:

  • 整数部分 4 = 2 2 = 100 4=2^{2}=100 4=22=100
  • 小数部分 0.5 = 2 − 1 = 0.1 0.5=2^{-1}=0.1 0.5=21=0.1

使用指数形式表示为 1.001 × 2 2 1.001×2^{2} 1.001×22,由于此处为二进制数,故指数为 2 的 2 次幂。

符号位 S=0,指数部分 E=2 存储为 2+127=129=1000 0001,小数部分 M 即 001。存入结果如上表。

精度丢失

  1. 数值范围
            以 float 类型为例。浮点型数据指数部分占 8 位。
            由于特殊数(1111 1111)与非规格数(0000 0000)的存在,指数部分最大取到 1111 1110,最小取到 0000 0001。
            指数的最大表示范围到 11111110 − 01111111 = ( 2 9 − 2 ) − 127 = 254 − 127 = 127 1111 1110-0111 1111=(2^{9}-2)-127=254-127=127 1111111001111111=(292)127=254127=127 比特。二进制浮点数的指数部分最大到 2 127 ≈ 1.701412 e 38 2^{127}≈1.701412e38 21271.701412e38,即数值范围最大到 1 0 38 10^{38} 1038;同理,计算 2 − 126 ≈ 1.175494 e − 38 2^{-126}≈1.175494e-38 21261.175494e38 可得到数值范围最小到 1 0 − 38 10^{-38} 1038
  2. 有效数字
            以 float 类型为例。小数部分 23 位,能够表示的有效数字 2 23 = 8388608 2^{23}=8388608 223=8388608,即最大到 0.8388608。因此小数部分可表示 1~6 位的所有小数(如 0.999999)与 7 位小于 0.8388608 的小数。

由于浮点型变量计算时为二进制指数部分 E 与小数部分 M 相乘(即进行移位),则有效数字在 6~7 位之间,达到逼近效果。

例 1:float f = 1.23456789e10 加 20
计算过程
输出
结果
f 仅在赋值存入时即产生精度丢失。
由于浮点型常量默认按 double 型运算,输出时 %f 格式为 double 型,占 8 个字节。具体参考【转】printf格式串中的%f的输出格式和内容

例 2:float f == 1.456?
计算过程
输出
运行结果
单步调试:
设断点计算
f 值精确到小数点后 7 位
f 值精确到小数点后 7 位,不等于 1.456。

正确写法:判断两个浮点数是否相等,直接用减法。
两数相减的绝对值在精度范围内。
减法判断两浮点数是否相等
输出
相等

存在部分浮点数相乘(移位)相等(如 4.5),但为保证准确性仍需直接计算。

例 3:double f =1.23456789e10
使用 double 类型
输出
输出结果正确
由于 double 型浮点数有效数字为 15~16 位,输出结果正确。

2.6 字符型数据

2.6.1 字符型常量

单引号括起来的一个字符。
正确示例:‘a’、‘A’、‘1’、’ '。
错误示例:‘abc’、“a”、“”。

ASCII 表
ASCII 码表
ASCII 表的存储与输出:
查找 ASCII 码值并显示对应的字符

单个字符

:字符 A
代码
输出
输出
上述程序若赋值 65 则输出不变。

以带符号整数形式输出为 65 是因为,在 ASCII 表中,字符 A 对应的十进制数为 65。

注:ASCII 表中十进制为 0 对应的字符为空字符,十进制为 32 对应的字符为空格。

汉字
注:汉字为 GBK 编码(其一),每个汉字占 2 个字节。

:打印 0xCC
打印汉字
输出
烫烫烫烫烫烫!!!!!

2.6.2 字符数据在内存中的存储形式及其使用方法

字母大小写转换
大小写字母间的 ASCII 值差 32。

:大写字母 ASCII 值 +32
输出字符值 +32
输出
输出 a

转义字符
转义字符

  1. “\n” 表示光标跳至新建的下一行行首。
  2. “\r” 表示跳至该行行首。
  3. “\b” 光标前退(向左)一个字符。
  4. “\t” 即 Tab 键,在黑窗口中输出 4 或 8 个空格(无标准规定)。
  5. “\” 输出每一个 “\” 前都需要加入转义字符 “\” 用于输出。
  6. “\0” 表示结束符,即NULL。使用时写作 ‘\0’。
  7. “\ddd” 表示转义字符后输入 1~3 位十进制数,输出对应八进制数的 ASCII 字符。
  8. “\xhh” 中 “\” 表示之后的 “hh” 从十六进制转换为十进制,输出为十进制数的 ASCII 字符。
  9. 光标后(右侧)存在字符,再输入字符将会覆盖光标后原有字符(功能类似于键盘上的 Insert)。


代码
输出
输出
注:注释中存在未加 " " 符号的 \ 将导致编译错误。 原因未知 \colorbox{Orange}{原因未知} 原因未知

2.7 字符串型常量

  • 由一对双引号括起来的字符序列
  • 不可以将字符串型常量赋给字符型变量,C 语言中没有定义字符串型变量的关键字。例如:先用语句 char c 定义字符型变量 c,后令 c=“a” 或 c=“CHINA”,这样的赋值都是非法的。
    注意:“” 与 “a” 都是字符串常量,“” 为空字符串。

:字符变量赋值
代码
输出
输出 0
解析
查看内存,c 与 *p 在内存中的状态如下:
c 与 *p 的地址
c 与 *p  的内存
显然,字符串 “A” 并未被存放至字符型变量 c 的地址上,反之被存放的是字符 “0”。 同时,顺着 *p 的地址网上爬,在
0x00427B30 地址找到存入的字符串 “A”。
字符串 "A" 存放的位置
查表可知,字符 “0” 对应的十进制 ASCII 码为 48,转换为计算机的十六进制表示即为 30。 因此,当编译器执行 char c = "A"; 时,先将字符串 “A” 在内存中任意选择一个位置存入,随后将存入地址的最低位存入变量 c 的位置。
因此不可将字符串直接赋值给字符型变量,而应当将其赋值给字符型变量的指针。

注:若使用 cpp 文件编译则编译失败。

  • C 语言规定,每个字符串型常量的结尾加一个字符串结束标志,以便系统据此判断字符串是否结束。C 语言规定以字符串 ‘\0’ 作为字符串结束标志。

例如:“CHINA” 在内存中的存储结果,占用 16 字节
CHINA 占用 16 字节

2.8 混合运算

“运算”包括数学运算与赋值运算。
不同类型的数据先要转换为同一类型再进行运算,不同类型的转换级别如下:
数据类型级别
从短字节到长字节的类型转换是由系统自动进行的,编译时不会给出警告;若反向进行,则编译时编译器会给出警告。

注:在保证数据内容不会因长字节向短字节转换时产生截断而导致数据丢失时,可以使用强制转换,即在变量前插入 (数据类型),编译不会发出警告。

2.8.1 数值按 int 型(4 个字节)运算

:两个较大整型常量相乘溢出
程序
解析

  • 计算 j 时,两个较大常量默认按 int 型运算,乘积超出 long 型变量长度,产生溢出。
  • 计算 k 时,两个较大常量强转 long 型运算,乘积超出 long 型变量长度,产生溢出。
  • 计算 m 时,两个较大常量强转 long long 型运算,乘积未超出 long long 型变量长度,但输出时乘积超出 %ld 输出格式长度,产生溢出。
  • 计算 i 时,两个较大常量强转 long long 型运算,以 %lld 格式输出,不产生溢出。

printf("%lld\n", 131072*131072) 中直接输出方法相同。

注:在 64位操作系统下,long 型占 8 字节,使用 long 型输出即可。 虽然但是我用 x64 编译没有成功过也不知道为啥 … … (挠头 \colorbox{Orange}{虽然但是我用 x64 编译没有成功过也不知道为啥……(挠头} 虽然但是我用 x64 编译没有成功过也不知道为啥……(挠头

2.8.2 浮点型常量默认按 double 型(8 个字节)运算

:加法运算
程序
输出
输出
解析

  • 第 6-7 行进行 12345678900.0+1 运算时按 double 型进行运算,但在赋值运算时产生精度丢失。
  • 第 8-9 行在进行加法运算、赋值运算及输出时都采用 double 型,输出正确值。
  • 第 10 行与第 8-9 行类似,但不将其赋值给变量,直接进行加法运算并输出。

:除法运算
程序
输出
输出

注:赋值给 float 型变量的常数需要带小数部分,否则编译器默认变量为 int 型运算。

2.9 常用的数据输入/输出函数

标准输入函数:scanf、getchar。
标准输出函数:printf、putchar。

2.9.1 scanf 函数的原理

当一个进程启动时,内存中的内核区域(详见 2.2 常量 → 内存)开放系统调用。键盘输入被写入标准输入缓冲区,scanf 函数从标准输入缓冲区中读取输入。

系统调用:内核在每个进程开启时为其维护三个缓冲区(即三块内存),由高地址向低地址分别为:标准输入缓冲区、标准输出缓冲区、标准错误输出缓冲区,大小分别为 4k。

当 scanf 读取的缓冲区中没有数据时会产生阻塞,scanf 进入睡眠(CPU 占用率为 0%),在完成输入并键入回车后 scanf 才继续运行。

缓冲区原理
缓冲区是一段内存空间,分为读缓冲和写缓冲。C 语言缓冲的三种特性如下:

  1. 全缓冲:当填满标准 I/O 缓存后才进行实际 I/O 操作。
    典型代表:对磁盘文件的读写操作。
  2. 行缓冲:当在输入和输出中遇到换行符时,将执行真正的 I/O 操作。这时,我们输入的字符先存放到缓冲区中,等按下回车键换行时才进行实际的 I/O 操作。典型代表:标准输入缓冲区(stdin)和标准输出缓冲区(stdout)。
  3. 不带缓冲:即不进行缓冲。
    典型代表:标准出错情况(stderr),这使得出错信息可以直接尽快地显示出来。

scanf 函数

# include <stdio.h>
int scanf(const char *format, ...);
//scanf(字符型常量格式, 存储地址);
//scanf_s(字符型常量格式, 存储地址, 读取字符长度);

:使用 scanf 时 %d 与 %c 的区别(& 为取址符,* 为指针)
程序
输出(输入 123):
输出
取消 8-9 行注释
程序
输出(仅输入一串 123):
输出
注:由于使用 scanf 编译失败,报错说明 C4996 提示将 scanf 更改为 scanf_s。二者的不同点在于 scanf 函数不会进行边界检查,可能造成数据溢出,为已弃用的函数;
        scanf_s 在调用时需提供一个数字表明最多读取多少位字符,因而能够进行边界检查。在使用 scanf_s 替代 scanf 后,第 10 行发出警告,只需将其更改为 scanf_s("%c", &c, sizeof(c)); 即可。


解析

  1. 程序中有两行 scanf 函数,运行时只有 1 次输入的原因:
            当 scanf 的格式为 %c 时,scanf 函数仅从标准输入缓冲区中读取一个单一的字符;而当 scanf 的格式为 %d 时,scanf 函数将读取输入的一整串十进制整数。
            取消注释后,仅当程序运行至第 8 行时输入一串十进制数字,之后再无输入而直接结束程序。因为当输入 123 并键入回车后,第 8 行 scanf 函数以 %d 格式读取标准输入缓冲区中的串 123\n 并打印 123。当程序运行至第 10 行时,scanf 函数以 %c 格式直接读取标准输出缓冲区中的 \n ,由于已经获得行缓冲中执行 I/O 时需要的回车(\n),程序直接运行第 11 行进行打印;倘若第 10 行 scanf 函数以 %d 或 %f 格式读取标准输入缓冲区,由于 \n 不属于 1~9 之间的十进制数或小数点,标准输入缓冲区中的 \n 将被 scanf 函数忽略或删除(若缓冲区内存在空格同理)。此时程序暂停,需要再次将字符输入标准输入缓冲区才能继续运行。
            可在 9-10 行之间插入 rewind(stdin);(2017 与 2019 版本)或 fflush(stdin);(更早的版本)清除缓冲区。
  2. scanf 函数参数中,第一个 “,” 后的参数为存储地址,必须使用取址符 &,否则会产生参数类型的报错。因为 scanf 函数是将数据存放在变量的存储地址上的,若未加入 &,编译器检测到的将会是一个未初始化的值。另外,在未加入 & 的前提下给变量赋值会产生访问冲突报错,相当于更改赋值数对应地址的内容。

注:sizeof 与 strlen 的区别

sizeofstrlen
性质操作符库函数
参数数据的类型或变量结尾为 \0 的字符串
计算结果在编译时计算结果在运行是计算结果
计算方式计算数据类型占内存的大小计算字符串实际长度

参考:sizeof和strlen的区别 - 徒梦 - 博客园

2.9.2 scanf 函数的循环读取

:使用 while 语句实现 scanf 函数的循环读取
程序
输出
输出

数据类型输入输出
int123123
char11
floata不断打印1

解析

  • 程序第 7 行的 EOF 即为 -1。
  • scanf 的返回值即成功赋值的变量数量,当没有匹配值时返回 0,发生错误时返回 EOF。
  • 定义变量 ret 用于监测 scanf 函数的返回值:
    注:由于赋值运算符 = 优先级低于关系运算符 !=ret = scanf_s("%d", &i) 外侧需加一层 ()
    程序
    当输入 123 时,i 值为 123,scanf 函数返回值为 1
    输入 123
    当输入 1 时,i 值为 1,scanf 函数返回值为 1
    输入 1
    当输入 a 时,i 值仍为 1,scanf 函数返回值为 0,开始循环打印上一次输出 1
    输入 a
    由于输入值为 123 与 1 时,变量 i 赋值成功,scanf 函数返回值为 1;标准输入缓冲区删除输入值,程序正常输出 i。

    当输入值为 a 时,变量(整型)i 与(字符)a 的类型不匹配,赋值失败,scanf 函数返回值为 0;标准输入缓冲区不删除(因为未匹配成功)也不忽略(scanf 函数仅在 %d 与 %f 格式时能忽略缓冲区中的 \n )字符 a,程序输出 i 的内容仍为上一次赋值 1。
  • 通过清除缓冲区解决不匹配造成不断打印的问题:
    使用 rewind 接口,在上一个 scanf 函数后插入 rewind(stdin);
    清除缓冲区
    输出(输入 3 次 ctrl+z 或点击停止测试结束运行):
    正常输出

2.9.3 多种数据类型混合输入

:混合输入整型、字符型与浮点型数据
程序
输出(输入 100 a 1.1):
输出
程序不断打印 i=100,c= ,f=-107374176.000000

解析
当程序运行至第 11 行之前,各个变量内存状态如下:
第 11 行
此时 ret=2,意味着有两个参数成功获得返回值。对照各个参数的实际内容,获得返回值的为变量 i 与变量 c,变量 f 仍为初始化值。
变量 f
查看内存,100 被成功赋值给变量 i(对应 ASCII 码为字符 d),而赋给变量 c 的是一个空格。
左变量 i,右变量 c
猜测是输入的 100 与 a 之间的空格被赋值给变量 c。更改 1 ^{1} 1代码第 9 行如下
更改程序
一般只需在 %c 前加入空格即可。
如果你使用的是 scanf 函数那么不出意外问题已经解决,但是使用 scanf_s 函数的朋友们会发现运行后发生程序异常:
程序异常
同时,变量 f 内存中仍为初始化值,变量 c 的内存中成功出现字符 a。但之后出现一串内存的哀嚎,参考C语言scanf_s()函数的用法,猜测是未定义 scanf_s 函数的字符读取长度而出现的函数越界读取。
c 内存
烫烫烫烫烫烫烫烫烫烫d
更改 2 ^{2} 2代码第 9 行如下
2次更改程序
输出
输出
若需要限制浮点数输出位数,可在 %f 之间输入限制的大小,如 %5.2f

未解之谜: \colorbox{Orange}{未解之谜:} 未解之谜:

  1. 设定字符 c 读取长度之前,输入的浮点型数 1.1 去哪了? \colorbox{Orange}{设定字符 c 读取长度之前,输入的浮点型数 1.1 去哪了?} 设定字符 c 读取长度之前,输入的浮点型数 1.1 去哪了?
  2. 定义变量时,int i; 的位置在 char c; 之前,而运行后发现变量 i 的位置在变量 c 之后的原因?
    :先定义的变量在高地址。

:混合输入双精度浮点型数据
在上例基础上加入 double 型变量,改动第9、10、12行
程序
输出
输出

数据类型输入输出
int100100
charaa
float1.11.100000
double1.1-92559604291193448741463169458822077475889920292718459471527936.000000

解析
在第 12 行设断点,运行发现变量 f 与 d 地址上的数值相同,都是输入的 1.1(计算方式详见 2.5.2 节)
内存
f d
视频里这个例子没有解释的很详细,我找到的解释是基于 scanf 函数与 printf 函数对 %f 格式的不同理解:关于scanf的%f和%lf,及printf的%f
由于变量 f 为长 4 字节的浮点数类型,变量 d 为长 8 字节的浮点数类型。
在为变量申请空间时,f 的内存范围为 0x012FF9C0-0x012FF9C3,d 的内存范围为 0x012FF9B0-0x012FF9B7。
根据小端模式的原则,两组输入 1.1 的十六进制表示为 3f 8c cc cd,在内存中存放形式为 cd cc 8c 3f。
当程序运行至 scanf_s 函数时,scanf_s 函数根据 %f 格式将指针指向变量 f 的 0x012FF9C3 与变量 d 的 0x012FF9B3 将赋值传入。
当程序运行值 printf 函数时,printf 函数仅将 %f 格式解释为 “打印浮点数”,由于浮点型常量默认按 double 型(8 个字节)运算,因此 printf 函数按从地址 0x012FF9B7 到地址 0x012FF9B0 顺序打印内存,即 cc cc cc cc 3f 8c cc cd。
基于以上,更改代码第 10 行如下
更改
%lf 为双精度浮点型格式,占 8 个字节。
输出
输出

2.9.4 getchar 函数介绍

getchar 函数可以一次从标准输入缓冲区读取一个字符,等价于 char c; scanf("%c", &c);
语法格式:

#include <stdio.h>
int getchar(void);

getchar 函数返回 int 型是为了方便起见。在函数出错时,函数返回值为 -1,-1 不在 ASCII 码范围内,使用 char 型表示不方便,而使用 int 型表示方便的同时变量的长度要求(4 字节)不影响 char 型变量(1 字节)本身。

2.9.5 putchar 函数介绍

输出字符型数据时使用 putchar 函数,其作用是向显示器设备输出一个字符。
语法格式:

#include <stdio.h>
int putchar(int ch);

putchar 函数输出时不会在末尾补充换行符号,但 2019 版本的 Visual Studio 会在末尾自动生成换行。

:getchar + putchar
程序
输出
输出1
输出2

2.9.6 printf 函数介绍

printf 函数可以输出各种类型的数据,包括整型、浮点型、字符型、字符串型等,实际原理是 printf 函数将这些类型的数据格式化为字符串后,放入标准输出缓冲区,然后通过 \n 来刷新标准输出,并将结果显示到屏幕上。
语法格式:

#include <stdio.h>
int printf(const char *format, ...);

printf 函数根据 format 给出的格式打印输出到 stdout(标准输出)和其他参数中。
字符串格式(format)由两部分组成:显示到屏幕上的字符和定义 printf 函数显示的其他参数。我们可以指定一个包含文本在内的 format 字符串,也可以是映射到 printf 的其他参数的 “特殊” 字符,如下列代码所示:

int age = 21;
printf("Hello %s, you are %d years old\n", "Bob", age);

输出:

Hello Bob, you are 21 years old

:printf 函数输出对齐
程序
输出
输出
%3d 靠右对齐;%-3d 靠左对齐;%10s 默认靠右对齐,前面加入 5 个空格。

2.10 运算符与表达式

2.10.1 运算符分类

C 语言提供了 13 种类型的运算符,如下所示:

  1. 算术运算符(+ - * / %)
  2. 关系运算符(> < == >= <= !=)
  3. 逻辑运算符(! && ||)
  4. 位运算符(<< >> ~ | ^ &)
  5. 赋值运算符(= 及其扩展赋值运算符)
  6. 条件运算符(?:)
  7. 逗号运算符(,)
  8. 指针运算符(* 和 &)
  9. 求字节数运算符(sizeof)
  10. 强制类型转换运算符((类型))
  11. 分量运算符(. ->)
  12. 下标运算符([])
  13. 其他(如函数调用运算符 ())

运算符优先级可参考:C语言运算符优先级(超详细)

2.10.2 算术运算符及算术表达式

算术运算符优先级(由高到低):乘(*)、除(/)、取余(%) → \rightarrow 加(+)、减(-)
适用数据类型

  • 取模(%)运算符接收两个整型操作数,将左操作数除以右操作数,返回值为余数。
  • 其余几种运算符既适用于浮点型数又适用于整型数。当操作符的两个操作数都是整型数时,执行整型数运算,其他情况下执行浮点型数运算。

:逆序输出
逆序输出
输出
输出

2.10.3 关系运算符与关系表达式

  • 由关系运算符大于(>)、小于(<)、是否等于(==)、大于等于(>=)、小于等于(<=)、不等于(!=)组成的表达式称为关系表达式,关系表达式的值只有真和假,对应的值为 1 和 0。
  • 由于 C 语言中没有布尔类型,所以在 C 语言中 0 值代表假,非 0 值即为真。
    例如,关系表达式 3>4 为假,因此整体值为 0;关系表达式 5>2 为真,因此整体值为 1。
  • 关系优先级(由高到低):大于(>)、大于等于(>=)、小于(<)、小于等于(<=) → \rightarrow 等于(==)、不等于(!=)
    关系运算符的优先级低于算术运算符。

:判断年份
判断年份
输出
输出
解析
根据 if 语句的条件,编译器先判断 3<year,如果是则值为 1,否则为 0;无论关系表达式的值是真是假,编译器判断 关系表达式的值<10 是的结果都为真,打印 year is ok\n
更改代码第 9 行如下
更改
输出
输出

:判断闰年(替换上例 9-14 行)
判断闰年

2.10.4 逻辑运算符与逻辑表达式

  • 逻辑运算符逻辑非(!)、逻辑与(&&)、逻辑或(||)与数学上的非、与、或一致。
  • 逻辑运算符优先级:逻辑非(!)高于算术运算符,逻辑与(&&)和逻辑或(||)低于关系运算符
  • 逻辑表达式的值只有真和假,对应值 1 和 0。

:短路运算(逻辑与
逻辑与
输出
输出
当逻辑与(&&)运算符前的表达式成立,运算符后的表达式才执行。即使运算符后的表达式优先级高于逻辑与运算符

短路运算(逻辑或
逻辑或
输出
输出

2.10.5 位运算符

位运算符包括左移(<<)、右移(>>)、按位取反(~)、按位或(|)、按位异或(^)、按位与(&)。

  • 左移:高位丢弃,低位补 0,相当于乘以 2。工作中很多时候申请内存时会用左移,例如要申请 1GB 大小的空间,可以使用 malloc(1 << 30)
    注: 1 K B = 2 10 b y t e 1KB = 2^{10}byte 1KB=210byte, 1 M B = 2 20 b y t e 1MB = 2^{20}byte 1MB=220byte, 1 G B = 2 30 b y t e 1GB = 2^{30}byte 1GB=230byte, 1 T B = 2 40 b y t e 1TB = 2^{40}byte 1TB=240byte
  • 右移:低位丢弃,正数的高位补 0,负数的高位补 1,相当于除以 2
    移位比乘法和除法的效率高负数右移,对偶数来说是除以 2,但对奇数来说是先减 1 后除以 2
    例如,-8>>1,得到的是 -4,但 -7>>1 得到的并不是 -3 而是 -4。
    另外,对于 -1 来说,无论右移多少位,值永远为 -1。
  • 异或:相同的数进行异或时结果为 0,任何数和 0 异或的结果是其本身。
    找到一组数两两成对的数中唯一一个只出现一次的数,可通过异或查找,最终的异或值即为唯一不成对的数。
  • 按位取反数位上的数是 1 变为 0,0 变为 1。
  • 按位与和按位或:用两个数的每一位进行与和或。

:左移
左移
输出
左移输出
注:除赋值运算与算术运算以外,其余都不会改变变量值。

:左移位运算符可能改变值的正负
程序
输出
输出
解析(参考 2.8.1 示例):
由于整型数默认按 int 型计算,编译器计算 i << 1 时按照 int 型计算,获得 int 型的值。

  • 程序第 8 行,printf 函数将 int 型(4 个字节)结果输出,不影响值的最高位,因此输出结果为正确值。
  • 程序第 10 行,编译器将计算的 i << 1 int 型结果赋给 short 型(2 个字节)变量 i,在程序第 10 行将 short 型变量 i 输出。此时由于原有值 0x70ff 在左移 1 位后得到值的最高位为 1,printf 函数则将 short 型变量作为负数输出。

同理
程序
输出
输出

:按位与、按位或、按位异或
程序
输出
输出

:找出唯一不成对的数
程序
输出
输出

:仅保留二进制数最低位的 1。
程序
输出
输出
将原数值与其负值按位与,得到结果。

:按位取反
按位取反
输出:-6
按位取反的结果即原数值的负数减 1。

2.10.6 赋值运算符(赋值操作符)

  • 优先级:除逗号运算符以外最低。
  • 赋值运算符左边的数称为左操作数,右边的操作数称为右操作数。
  • 占用内存、可被赋值的变量称为左值。


程序
代码中两种正确情况(7)(9)都可能写成错误情况(8)。

2.10.7 条件运算符与逗号运算符

  • 条件运算符是 C 语言中唯一一种三目运算符,代表有三个操作数。
  • 运算符也称操作符。
  • 逗号运算符的优先级最低,逗号表达式的整体值是最后一个表达式的值。
    如:while(rewind(stdin), scanf("%d %d %d", &a, &b, &c) != EOF) 中 while 循环是否结束取决于 scanf("%d %d %d", &a, &b, &c) != EOF) 这个给关系表达式的真假。

:找出两个数中的最大值
两数最大
输出
输出

:找出三个数中的最大值
三数最大
输出
输出
程序第 9 行也可写作 max_value = i > j ? (i > k ? i : k) : (j > k ? j : k);

2.10.8 自增、自减运算符及求字节运算符

  • 自增、自减运算符即对变量本身进行加 1、减 1 操作。
  • 来源于 B 语言。
  • 不能用于常量。

:i++
程序
输出
输出
解析
++ 在变量 i 之前时,程序第 7 行 j = i++ > -1; 等同于 j = i > -1; i++;。前一语句判断为假,即 j=0;后一语句 i=-1+1=0。

:++i
程序
输出
输出
解析
++ 在变量 i 之后时,程序按照正常的优先级计算。++i 计算 i=++i=0+1=1;j = !i 将 i=1 取反判断为假,即 j=0。

三、选择与循环

学习目标

  • 选择结构程序设计
  • 循环结构程序设计

3.1 选择结构程序设计

3.1.1 关系表达式与逻辑表达式

根据表达式最后运算步骤的类别确定该表达式的类型。

3.1.2 if 语句

流程图
左侧为仅 if 语句,右侧为 if + else 语句。
流程图
多分支语句
多分支语句
if 语句的嵌套
if语句的嵌套


if - else if - else语句
输出
输出

注意:if 判断后不能加 ;,即以下写法是错误的:
错误

3.1.3 switch 语句

  • 用于判断的一个变量有几个或几十个可能的值时。避免过多的 else if 语句。
  • switch 的参数只能是整型或字符型,不可以是浮点数。
  • 由于 switch 每匹配一种情况,之后的将不再匹配并直接输出。因此在每一种情况之后都需加入 break;
    注意:此时case需以 ; 结尾。
  • 在所有 case 之后可加入 default 语句防止输入值与设定的 case 情况不匹配。

:打印每个月的天数
switch语句
输出
输出
对程序进行优化
优化

3.2 循环结构程序设计

3.2.1 goto 语句

  • goto 语句对应着汇编语言中的 jmp 跳转,体现了循环结构的本质。C 语言中的 while、do while 和 for 循环在程序编译时,都要拆解为汇编语言中的 jmp 跳转。
  • goto 语句也被称为无条件转向语句,具体语句格式goto 语句标号;,语句标号的命名规则与 C 语言中变量的命名规则一致。
    例如:goto label_1; 这种写法是合法的,而 goto 123; 这种写法是不合法的。
  • 使用场景:一种是向上跳转实现循环,另一种是向下跳转实现中间的部分代码不执行。

:goto 向上跳转
1-100求和
输出
输出

:goto 向下跳转
向下跳转
输出
输出
注意:标签与 goto 不能跨函数使用。

3.2.2 while 循环

  • while 语句用于实现 “当型” 循环结构,其一般形式为 while(表达式) 语句;,当表达式的值非 0 时,执行 while 语句中的内嵌语句。
  • 特点:先判断表达式,后执行语句。具体流程如下:
    流程
  • 当表达式的值非 0 时,就会执行语句,从而实现语句多次执行的效果。为避免程序进入死循环(不停地进行循环操作),在语句中需要有让表达式趋近于假的操作来使程序跳出循环。

注意:当使用多层嵌套循环语句时出现报错,调试时断点从外层循环向内层尝试设置。


程序
输出
输出

3.2.3 do while 循环

特点:先执行循环体,后判断循环条件是否成立。
与 while 循环的区别:do while 循环的第一次循环一定会执行。


程序
输出
输出

3.2.4 for 循环

使用场景

  • 循环次数已经确定的情况
  • 循环次数不确定而只给出循环结束条件的情况

一般形式for(表达式 1; 表达式 2; 表达式 3) 语句;

  1. 先求解表达式 1。
  2. 求解表达式 2,若其值为真(值为非 0),则先执行 for 语句中指定的内嵌语句,后执行第 3 步。若其值为假(值为 0),则结束循环,转到第 5 步。
  3. 求解表达式 3。
  4. 转回第 2 步继续执行。
  5. 循环结束,执行 for 语句下面的语句。

注:三条表达式都可省略,; 不可省略。但不建议这样写。


程序
输出
输出
注:在 for 条件后加 ;,即for (i = 1, total = 0; i <= 100; i++); 不会产生死循环,但不能输出正确结果。此时程序第 7 行会执行 100 次,随后转到程序第 8 行执行。

四、数组

借助 C 语言提供的数组,通过一个符号来访问多个元素。

学习目标

  • 一维数组的原理及使用方法
  • 二维数组的原理及使用方法
  • 字符数组的使用方法
  • str 等系列函数的使用方法

4.1 一维数组

4.1.1 数组的定义

  • 数组是指一组具有相同数据类型的数据的有序集合。
  • 格式类型说明符 数组名[常量表达式];,如 int arr[5];

声明数组时要遵循以下规则

  • 数组名的命名规则和变量名的相同,即遵循标识符命名规则。
  • 在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。
  • 常量表达式中可以包含常量和符号常量,但不能包含变量。也就是说,C 语言不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行过程中变量的值。

4.1.2 一对数组在内存中的存储

以语句 int mark[100]; 为例,定义的一堆数组 mark 在内存中的存放情况如下:
数组的存放情况

  • 每个元素都是整型元素,占用 4 字节。
  • 元素的引用方式是 数组名[下标],所有访问数组 mark 中的元素的方式是 mark[0],mark[1],…,mark[99]。

注意:因为数组元素是从 0 开始编号的,所以没有元素 mark[100]。

数组的初始化方法:

  1. 在定义数组时对数组元素赋初值。例如,int a[10]={0,1,2,3,4,5,6,7,8,9};
    不能写成 int a[10];a[10]={0,1,2,3,4,5,6,7,8,9};
  2. 可以只给一部分元素赋值。例如,int a[10]={0,1,2,3,4};
    定义 a 数组有 10 个元素,但花括号内只提供 5 个初值,表示只给前 5 个元素赋初值,后 5 个元素的值为 0。
  3. 如果要使一个数组中全部元素的值为 0,那么可以写为 int a[10]={0,0,0,0,0,0,0,0,0,0};,或 int a[10]={0};
  4. 在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组的长度。例如,int[]={1,2,3,4,5};

错误示范:

int main()
{
	int arr[5]={0};
	arr={1,2,3,4,5};
}
int main()
{
	int arr[5]={0};
	arr[5]={1,2,3,4,5};
}

:访问越界
访问越界
输出
输出
异常
解析
由于先定义的变量在高地址。根据地址从高到低排列,变量依次为 int j int arr int i
程序运行至第 9 行时
行 9 地址
行 9 地址
运行第 9 行时,地址 0x0075FE88-0x0075FE8B 被写入数值 20;
运行第 10 行时,地址 0x0075FE8C-0x0075FEF 被写入数值 30;
运行第 11 行时,地址 0x0075FE90-0x0075FE93 原先变量 j 的值被覆盖,写入数值 40。
行 12 地址
此时打印变量 i 与变量 j 地址上的内容,结果即为 i=3,j=40。同时弹出异常提示:“变量 ‘arr’ 周围的堆栈已损坏”。
注:8 字节保护空间仅存在于 Windows 系统,在 Linux 系统中不存在保护空间。

4.1.3 栈空间和数组

Windows 操作系统中单个栈空间大小是 1MB, Linux 操作系统中单个函数的栈空间大小是 10MB(Linux 操作系统下可以修改)。
程序执行时,main 函数被分配一个栈空间。函数内每定义一个变量或调用函数时,main 函数的栈空间就为该变量或函数分配一个空间;当变量或函数执行结束后,系统会自动回收栈空间。
由于函数栈空间存在上限,当变量大小超过栈空间上限时需要使用堆空间。

:栈空间溢出(爆栈)
栈空间溢出
输出
输出
异常
解析
由于 arr 数组为 int 类型,因此每个 arr[i] 占 4 个字节,当申请的 arr 数组长度为 250,000 时,占用的栈空间大小为 250,000×4=1,000,000 字节=1MB。由于Windows 操作系统中单个栈空间大小是 1MB,当申请的数组长度超过 250,000 时就会发生爆栈。
此外,当递归调用次数过多时也会发生爆栈。(一般工作场景中不使用递归,因为递归的性能低于非递归)

4.2 二维数组

4.2.1 二维数组的定义与引用

  • 定义的一般形式类型说明符 数组名[常量表达式][常量表达式];
  • 二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储第一行的元素,后顺序存储第二行的元素。
    a[3][4] 为例,数组元素的获取依次是从 a[0][0]a[0][1],直到最后一个元素 a[2][3]。如图:
    存储顺序


二维数组
内存
内存

4.2.2 二维数组的初始化及传递

二维数组初始化的 4 种方法:

  1. 分行给二维数组赋初值。例如,int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    注意:此时数组中最大的元素为 a[2][3],值为 12。
  2. 将所有数据写在一个花括号内,按数组排列的顺序对各元素赋初值。例如,int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
  3. 对部分数组元素赋初值。例如,int a[3][4]={{1},{5},{9}};
    效果如下图:
    部分元素赋初值
  4. 如果对全部元素赋初值,那么定义数组是可以不指定第一维的长度,但要指定第二维的长度。例如,int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; 等价于 int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};

:二维数组的存储与传递
程序
输出
输出
解析

  • 子函数的定义与调用:由于二维数组传递的指针为数组指针(一维数组传递的时整型指针),无法传递数组的行但可以传递数组的列。因此子函数 two_arr_print 中传递的数组参数列数与原有列数需保持一致。除此之外还需另外传递数组的行数。在 main 函数中调用函数时需要传递数组名与数组具体函数。
    注:可通过更改子函数中传递参数的列数与 main 函数中引用子函数的行数参数改变数组输出的形状。如在 main 函数中引用 two_arr_print(arr, 4); 并将子函数声明为 void two_arr_point(int arr[][3], int row),输出效果如下
    输出
  • 两个 for 循环中的行数与列数表示:由于行数在参数中被直接传递,因此直接将行数用 row 表示即可。对于二维数组,arr[i] 即表示第 i 行的数组元素,因此只需将任意一行的数组长度除以变量类型长度可得到列数,即 sizeof(arr[0]) / sizeof(int)

4.3 字符数组

4.3.1 字符数组的定义及初始化

//例 4.1
//while循环读取一行字符:读入一行,输出一行
void scanf_while() {
	char c;
	while (scanf_s("%c", &c, 1)!=EOF) {	//EOF用于判断是否读到末尾
		putchar(c);	// 输出一个字符,返回值表示是否正常输出
	}
}

int main() {
	scanf_while();
}
/* 输入两次ctrl Z+回车退出,ctrl+Z为流结束标志 */

定义
char c[10];

初始化方式:

  1. 对每个字符单独赋值进行初始化。
    c[0]='f'; c[1]=' '; c[2]='a'; c[3]='m'; c[4]=''; c[5]='h';
  2. 对整个数组进行初始化。
    char c[10]=['l','a','m','h']

工作中一般使用字符数组初始化:
char c[10]="hello";
因为 C 语言规定字符串的结束标志为 '\0',而系统会在字符串常量末尾自动加一个 '\0',为了保证处理方法一致,一般会人为地在字符数组中添加 '\0'。所以字符数组存储的字符串长度必须比字符数组少 1 字节。

// 字符数组的初始化
void char_arr_init1() {
	char c[10] = { 'h','e','l','l','o' };
	printf('%s\n', c);	// printf用%s格式打印整个字符串
	/* 字符数组后五个元素全0,即字符串末尾存在结束符00(对照ASCII码),
	可直接用字符串格式打印 */
}

void char_arr_init2() {
	char c[5] = { 'h','e','l','l','o' };
	printf('%s\n', c);
	/* 字符数组中全为字符,末尾不存在结束符,
	直接用字符串格式打印将超出数组范围直至读取到00 */
}

void char_arr_init3() {
	char c[10] = "hello";
	printf('%s\n', c);
	/* 末尾自动产生结束符并赋值给数组 */
}

void char_arr_init4() {
	char c[5] = "hello";
	printf('%s\n', c);
	/* 末尾自动产生结束符,但无法赋值给数组,产生访问越界 */
}

int main(){
	char_arr_init1();	// 字符数组依次初始化
	char_arr_init2();	// 打印的字符串越界
	char_arr_init3();	// 更常用的初始化方法
	char_arr_init4();	// 访问越界报错
}
void string_print(char c[]) {
	int i = 0;
	while (c[i]) {
		putchar(c[i++]);	// putchar每次打印单个字符
		/* putchar(c[i++]);分为两步
		1. putchar(c[i]);
		2. i+=1;
		若写成putchar(c[++i]);则
		1. i+=1
		2. putchar(c[i]);
		在逻辑上错误*/
	}
}

void char_arr_init() {
	char c[6] = "hello";
	string_print(c);
}

int main(){
	char_arr_init();
}

若仅定义而不进行初始化,则之后赋值字符串时数组中未被赋值的位置为 cc,即字符串末尾不存在结束符 00。

void scanf_while() {
	char c;
	char arr[20];
	int i = 0;
	while (scanf_s("%c", &c, 1)) {
		arr[i++] = c;
	}
	printf("%s\n", arr);
}

int main(){
	scanf_while();
}
/* 此时内存中的字符数组如下:
0x005CF81C  68 65 6c 6c  hell
0x005CF820  6f 0a cc cc  o.??
0x005CF824  cc cc cc cc  ????
0x005CF828  cc cc cc cc  ????
0x005CF82C  cc cc cc cc  ????
*/
/* 输出如下(共7行):
hello
^Z

hello
烫烫烫烫烫烫烫烫烫烫烫烫?
烫烫<_詄鵟

*/

4.3.2 gets 函数与 puts 函数

gets 函数类似于 scanf 函数,用于读取标准输入。二者区别如下:

  • scanf 函数读取到空格时结束。
  • gets 函数读取空格可继续读取。

格式如下:
char *gets(char *str);

// scanf的例1
void string_scanf() {
	char arr[100];
	while(scanf_s("%s", arr, 100)) {
		printf("%s\n", arr);
	}
}

int main(){
	// 读一个字符串
	string_scanf();
}

/******************************
输入:hello
输出:hello
内存:
0x012FFB50  68 65 6c 6c  hell
0x012FFB54  6f 00 fe fe  o.??	// 内存中包含结束符00
0x012FFB58  fe fe fe fe  ????
0x012FFB5C  fe fe fe fe  ????
0x012FFB60  fe fe fe fe  ????
******************************/
/******************************
输入:hello world
输出:
hello
world
第一行输出时内存:
0x00CFFD58  68 65 6c 6c  hell
0x00CFFD5C  6f 00 fe fe  o.??
0x00CFFD60  fe fe fe fe  ????
0x00CFFD64  fe fe fe fe  ????
0x00CFFD68  fe fe fe fe  ????
第二行输出时内存:
0x00CFFD58  77 6f 72 6c  worl
0x00CFFD5C  64 00 fe fe  d.??
0x00CFFD60  fe fe fe fe  ????
0x00CFFD64  fe fe fe fe  ????
0x00CFFD68  fe fe fe fe  ????
两次输出时改变的内存地址相同。
******************************/
// scanf的例2(改进例1)
void string_scanf() {
	char c[100];
	char d[100];
	while(scanf_s("%s%s", c, 100, d, 100)) {
		printf("%s---%s\n", c, d);
	}
}

int main(){
	// 读一个字符串
	string_scanf();
}

/* 输入:hello world
输出:hello---world */

使用 gets 读取一行字符


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值