有句话说得好,C是万物之源
C作为编程入门的首选语言已流行数十年,作为使用C的编程者,理应深入了解其基本特性
此篇文章记录我在学习C语言过程中遇到的困难,整理成知识点展列如下:
//随着学习的深入不定时更新,最后一次更新时间:2022_01_13
缓冲区与scanf函数及其错误格式输入
用户的输入分为缓冲输入与无缓冲输入,无缓冲输入为回显用户输入的字符后立即重复打印该字符,缓冲输入为用户输入的字符被收集并储存在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符
为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误,当最后按下Enter键时,传输的是正确的输入。 虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中,用户希望按下一个键就执行相应的指令。因此,缓冲输入和无缓冲输入都有用武之地。
缓冲分为两类:完全缓冲I/O和行缓冲I/O。完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是 512 字节和4096字节。行缓冲I/O指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,在按下Enter键后才刷新缓冲区。
直接调用操作系统的函数被称为底层 I/O,由于计算机系统存在差异,C不可能为普通的底层I/O函数创建标准库,然而从较高层面上,C还可以通过标准I/O包来处理文件。引入标准I/O的概念我们首先介绍计算机系统的差异,如不同的系统储存文件的方式不同。 有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。 有些系统用最小字节来衡量文件的大小,有些系统则以字节块的大小来衡量。如果使用标准 I/O 包,就不用考虑这些差异。因此,可以用 if (ch == ‘\n’)检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O函数会在两种表示法之间相互转换。
C程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
C语言中的scanf函数为标准I/O包的成员,可以实现用户的输入,对标准输入流stdin进行处理,键盘输入采取行缓冲模式,我们来了解一下scanf函数
int scanf(const char *format, ...)
用户的输入数据存入缓冲区,输入’\n’指令后,scanf函数对其进行扫描,依次按format形式赋给变量,如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,返回EOF.
在编程人员设计程序时,往往希望实现这样的功能:如果用户输入错误,提示用户输入正确格式,并重新输入,如下:
//要求用户输入数字,否则报错,重新输入
while (!scanf("%d", &x)) {
printf("Format Error.Please try again.\n");
}
那么scanf的问题随之而来,如果用户输入错误,错误格式的输入会滞留在缓冲区内,下一次scanf从缓冲区内读取时,会直接读取他而不是用户的输入从而报错,并陷入无限循环,显然这不是用户想要看到的
怎么处理呢?这时候就用到了getchar()函数,getchar函数可以读取缓冲区内的第一个字符,因此可设计读到’\n’才退出的循环以清空缓冲区,故程序可进行如下改良
while (1) {
if (!scanf("%d", &x)) {
while (getchar() != '\n');
printf("Format Error.Please try again.\n");
continue;
}
break;
}
显然,问题得到了改善,但没有完全解决,假如用户在一组输入里输入了正确的格式混合错误的格式,如要求int型输入下输入了111aaa,scanf会先扫描到’1’,直到’a’出现,把111存入目标变量中,aaa留在缓冲区中,并返回1,不会进入if条件判定,如此以来情况更恶劣了,不但没检测到用户的错误输入,还把错误字符存入了缓冲区,影响之后检测输入的正确性
那么如何处理呢?这时候需要用到getchar()函数与goto语句,由于用户输入正确的格式后缓冲区内会留下’\n’,在判定用户输入正确后可进行二次判定,如果getchar()读取缓冲区内不是’\n’,代表用户输入正确的部分之后有错误冗余并清除缓冲区,之后用goto语句回到第一次输入,改良如下
while (1) {
retry:
if (!scanf("%d", &x)) {
while (getchar() != '\n');
printf("Format Error.Please try again.\n");
continue;
}
break;
}
if (getchar()!= '\n') {
while (getchar() != '\n');
printf("Format Error.Please try again.\n");
goto retry;
}
类型在内存中的存储与printf函数的输出规则
类型的存储规则如下:
- char: 以ASCII码值的二进制形式存储,占用1字节,8bit位
- short、int、long:以补码形式存储,无符号类型第1bit位为符号位,1为负,0为正
- float:占用4字节,32bit位,第1位为符号位S,接着8bit位为指数E,剩下23位为有效数字M
符号位S: 1为负,0为正
指数位E与有效数字M:
将浮点数绝对值的二进制形式表出,假设为10110101,将其转变为1.0110101 * 27 ,将指数7加上32位的阶码数127以无符号整型存储在E中,将小数点后的0110101按序存储在M中,不足的用0补满,如有超过在尾部截断
如果E内全为1,值为无穷
如果E内全为0,值为0
- double:占用8字节,64bit位,第1位为符号位S,接着11bit位为指数E,剩下52位为有效数字M
存储模式与浮点型类似,64位的阶码数为1023
在C标准库中,printf()函数是标准输出函数,按照用户输入的格式读取变量,如果变量与用户输入的格式不相等,会不可预知的产生错误
如:按%d格式化打印float num = 9.0 ,32位系统的结果如下
浮点数9.0等于二进制1001.0,即1.001 * 23
因此在32位系统下 S = 0, E=3+127=130 , M=1.001, 二进制存储形式为
0 10000010 00100000000000000000000
其二进制表示的值为:1091567616,与结果相符
强制类型转换
在语句和表达式中如果使用混合类型,C会采用以下规则进行自动类型转换:
- 算术运算式中,低类型转换为高类型
- 函数传参时,实参类型转换为形参类型
- 赋值表达式中,表达式右侧类型转换为表达式左侧类型
- 存在返回值的函数中,返回表达式类型转换为返回值类型
- float在参与传参时,转换为double (K&R时代的C,float参与运算也会转换为double以提高精度)
- char、short在参与运算、传参时,发生整形提升,转换为int,整形提升规则为:在内存中存储的补码,全补符号位至4字节长
类型级别的高低如下所示:
除了C语言内的隐士转换,编程者还可以根据强制转换语句进行类型转换,转换规则如下:
向高类型转换:
- short、char转换为int,发生整形提升
- int、long转换为float、double,数值不变,补齐小数点位,按float、double存储规则进行存储
向低类型转换:
- float、double转换为int、long,数值不变,去除小数点位,按int、long
- 其余情况均发生截断
针对上述规则举几个例子:
char ch;
int i;
float fl;
变量赋值:
fl = i = ch = 'C';
字符’C’被作为1字节的ASCII值储存在ch中.整数变量i 接受由’C’转换的整数,即按4字节储存67.最后,fl接受由67转换的浮点数 67.00
整形提升与截断:
ch = 'C';
ch = ch + 1;
字符变量’C’参与运算,发生整形提升转换为int,值为67,然后运算,结果+1变为68,截断成1字节储存在ch中