前言
目前学习的内容,要和函数的实现,操作系统以及Linux函数库做交叉,对于任何一种编程语言来说都是一种进阶。学习的一种目的也在于开阔眼界,看到一种方法的多种实现。
程序异常描述
编译器玄学报错
从项目属性——高级中,将SDL检查改为否,然后%f转义字符改为%f*,这样才能输出结果
但是数值98.5被拆分成两次输出
OS—-缓冲分类
关于缓冲的C标准
非交互式设备使用全缓冲,标准错误不能使用全缓冲
缓存原理
scanf通过转义字符指定匹配并且在缓冲区删除该字符。如果缓冲区仍然有数据,比如上次scanf语句存储在缓冲区的’\n’,会接着被scanf语句获取到,这时候将其赋值给字符数据类型,再进行打印仍然有值的输出;
编译器细节处理
如下图,visual stido 会自动对数据初始化,内存地址对应的十六进制数据全为c;然而GCC却不会,gcc会保留程序上次执行时分配的数据,也就是脏数据;
visual studio 编译处理1
首先对内存地址以及十六进制编码的观察,需要在调试终止的时候,否则编译器会提示调试过程中内存不可用;
所以只有采用全部中断的方式,继续输入,这时候能够在监视区获取到i的内存地址和值,但是ret不能获取到
原因在于它的获取必须要在while循环中的赋值语句结束之后,正常的调试顺序就是后面的逐过程,但是编译器直接会报错,导致程序不能边调试边执行;
visual studio编译环境的报错处理
参考与引用:
https://blog.csdn.net/Angelina__/article/details/106320368
程序执行错误的排除技巧
增量编写代码,边写代码边执行,用printf语句查看输出
Ctrl + Z中断程序运行的操作原理
相当于ASCⅡ码表中的26,当scanf语句读取到26的时候,就知道程序出现问题
调试逻辑:
1,在程序执行过程中打断点,程序会停止并且等待输入;
2,先打断点,然后执行调试,输入值之后回车确认,然后点逐过程;在调试过程中,内存窗口不能搜索变量的内存区,但是可以读取变量的内存地址;
产生的问题:
本打算用excel查找功能,先把整个内存区的所有地址复制下来,直接搜索,没想到Ctrl + A没能选中全部,只是选了一页,那么就无法定位微软对c的初始化,cc cc cc cc;
ASCⅡ码表
字符型
逐步调试,实验字符类型的输出
如何调出内存一中c对应于内存中的值呢?
断点调试第七行——监视中输入c指针(&取地址符号)——将值复制到内存选区中的地址栏
说明了什么?
c是一个字符常量,在ASCⅡ中的值为六十五,但在内存中是以十六进制存储的,所以这里的值转换为十进制是:
4 * 16 + 1 = 65
可否直接写65呢?
可行的!效果如下图,原理是:编译器根据输出语句转义字符中的%c,在内存中寻找变量c的值,找到六十五之后在ASCⅡ码表中找六十五对应的字符,然后输出。
拓展,汉字如何编码?
使用GBK编码,两个字节,在内存中依然是0101代码
程序 = 数据 + 逻辑
程序 = 数据结构 + 算法
将变量定义直接写在最前面,增强可读性,理解代码的作用和实现
VS快捷键
如何快速实现大写字母转小写字母而不改变原有变量的值呢?
直接在输出语句的转义字符中,对原有变量,字符型对应的变量直接做加法即可。65 + 32 = 97对应的就是小写字母a
\r转义字符有什么作用?
回到行首,在第十三行语句中,但打印到\r的时候,光标移动到行首,再打印d并且换行,所以输出为dbc
如何实现删除效果?
在第试试行,从\b开始退格,将光标退到c字符之前,然后用空格替换c,最后再退格到b之前,换行输出结果
斜杠加数值的转义声明有何意义?
细节: 反斜杠打印出来之后,由于没有换行。这一行的反斜杠会和下一行的字符粘在一起,所以需要人为设置 换行
123转换为八进制为83,对应的ASCⅡ是S,所以输出就是S。
0d有什么作用?
在每一个字符串结尾都有这个代码,标识字符串的结束
混合运算
如果发生精度丢失应该怎么办?
不同数据类型的变量之间传递数据,以赋值的形式,会发生精度丢失,所以需要在赋值的时候强制转换数据类型,大的数据类型和小的数据类型之间的转换由情况决定。即使前端已经限定了数值的大小,传递给后端的时候,后端同样需要处理数据大小的合法性,否则由黑客抓包,获取数据存储格式后会对程序发起攻击。
在这段代码中,将j的值传递给i的时候,由于j的数据类型是long类型,比i的类型大,所以需要向下转换,便于数据的存储和表达。
如何解决相乘造成的数值的溢出呢?
在声明数据类型的时候,直接声明变量为64位八个字节的数据类型,在输出语句的转义声明中直接使用%ll
字符变量的赋值
但我们给变量赋值的时候,一定要区分字符和字符串类型,仔细审查变量和数据类型是否完全一致。
如果将字符串赋值给字符数据类型,那么打印的值会随机抽取内存地址的第一位,因此打印的结果会随着运行时内存分配的不同而不同。
混合运算
在混合运算中,不同数据类型之间以赋值的方式传递值,可能会发生数据丢失,为避免此情况,可以强制转换数据类型;但数据传入的时候加判断,数据合法则强制转换数据类型;
注意:数据丢失的警告,需要使用打印语句才能够调出,赋值的过程并不需要直接使用变量;
应用:前端传过来的数据,后端仍然需要做数据检查,否则黑客可以利用抓包工具获取数据存取方式进而发起攻击;
编译原理:在编译的时候不会将值拿过去,而是对变量做数据类型检查;
浮点运算的默认大小
第一个打印的值只有7位精度,单精度浮点数f只有4字节的存储空间,能够表示的精度是6~7位,所以只保证1~7位是正确的,后面的都是近似值;
注意第五行和第七行,先赋值后计算和先计算后赋值不一样。前者会发生数据丢失,后者不会;
第二个打印的值是正确的浮点型常量,它是按8字节即double型进行运算的,同时%f会访问寄存器8字节的空间进行浮点运算,因此可以正常输出。
补充:大多数运算给四个字节存储或者表示即可,位数的增加会直接增加硬件成本;Python没有编译步骤,执行时动态扩容;
scanf语句
实现原理:对系统调用的封装,这个系统调用包含三个部分,标准输入,标准输出,标准错误。使用系统调用缓冲区读取数据
语句特性:1,按照转义字符指定类型从缓冲区读取数据;2,会自动忽略whitespace,先执行删除,然后阻塞;
scanf循环获取输入
程序执行分析
循环体结束的条件:但scanf的逻辑值为-1的时候,但是scanf只存在针对字符的0和针对非零以外的所有数字均视作1,所以这是一个无线循环语句;
ret的值也只有0/1俩种情况,但输入为数字的时候,程序会立即执行printf语句打印值,但是当输入为字符的时候,由于scanf语句无法自动删除缓冲区内的\n,也就是回车符,而且此时scanf语句的逻辑判断符合循环条件,那么该循环会一直打印上一次的输入;
解决办法
vs2017 & 2019使用rewind(stdin)清空缓存区, 其他版本使用fflush(stdin);
使用for循环控制循环输入的字符个数【未教学】
混合输入
语句控制技巧
第十八行,用一个变量接收scanf_s语句获取输入后的逻辑值,但这里必须加括号,限定运算优先级,否则这条控制语句将彻底变为赋值语句,ret的值只有1、0两种情况
执行原理:
scanf语句获取到的数据, 实际上是 数值100和空格键,所以缓冲区还会存留上次没有完成的a,由于是字符,会不断的循环输入
解决策略
在获取输入的语句中,转义字符中对应的两个值之间留一个空格,那么输入时的空格就会被填进去,也就不会影响之后的输入;
比较第二幅图与第三幅图,修改之处在于空格和对f转义字符的类型限定,由于变量d的数据类型是double,所以转义字符也是%lf。
第二幅图是老师的输出结果,由图中观察可以得知,即使输入的值,数值上是相同的,我们可以看到内存中的十六进制的数值,内存地址是一样的,但是最后打印在屏幕上的字符却不一样,就是因为数据类型:一个是float数据类型,一个是double数据类型;
getchar()函数的使用
设计初衷
功能:从标准输入中读取并且返回下一个字符,如果达到文件尾,会返回ROF,所以这里会使用强制类型转换,所以getchar()的数据类型是整数,在ASC中没有-1这个数值;
printf()转义字符控制对齐
对比图中第一行和第三行数据,可以发现,hello语句前面存在空格,这里有五个,可以转义字符规则来控制对齐
scanf()循环获取输入—-多种数据类型
代码
#include <stdio.h>
int main()
{
int i, ret;
char j;
float k;
while ((ret = scanf("%d %c%f", &i, &j, &k)) != EOF)
{
printf("i = %d, j = %c, k = %f", i, j, k);
}
printf("Hello World!\n");
getchar();
return 0;
}
知识点
对于转义字符%d %f来说,它能够自动忽略空格和换行符,但是对于%c来说,一切符号都应该从缓冲区读取出来
所以在这份代码中,为了不让%c错误读取到空格,所以在获取输入语句中前置一个空格;
在代码设计中,满足ret等于EOF,只有读取到文件末尾,或者程序异常,所以这是一个不会被打破的循环语句;
double数据类型
错误代码
正确代码
知识点
double数据类型是以八个字节存储的,但是float只使用四个字节存储,当把double类型的变量,使用float型变量打印出来的时候,就会因为类型匹配的问题导致值不够存储,从而变为负数。
这告诉我们,编程一定要严谨规范,转义字符和数据类型,以及参数个数严格匹配,否则就会出现各种问题;
编程习惯
在使用主函数运行多个子函数的程序中,为每一个子函数编写注释,以便快速了解子函数的功能;
getchar()
函数定义说明
虽然getchar()设计上,函数的返回值是字符,但是由于ASCⅡ中没有负数,所以返回值为int,但是int兼容char数据类型
对齐方式
算术运算符-关系运算符-位运算符
运算符分类
(1)算术运算符(+-*/ %)。 | |
---|---|
(2)关系运算符(><==>=<=!=)。 | |
(3)逻辑运算符(! && ||)。 | |
(4)位运算符(<<>>~|^ &)。 | |
(5)赋值运算符(=及其扩展赋值运算符)。 | |
(6)条件运算符(?:)。 | |
(7)逗号运算符()。 | |
(8)指针运算符(*和&)。 | |
(9)求字节数运算符(sizeof)。 | |
(10)强制类型转换运算符((类型))。(11)分量运算符( | ->)。 |
(12)下标运算符(O)。 | |
(13)其他(如函数调用运算符0))。 |
取余运算符
取余运算符的使用
#include <stdio.h>
#include <stdlib.h>
// 取余运算符
int main(void)
{
int i = 12321;
while (i)
{
printf("%d\n", i % 10); // 不保存运算结果就直接输出,必须要有换行符,否则不会xiang4对联一样显示
i = i / 10; // 辗转相除
}
return 0;
}
// 20210419 20:55
将数字转换为字符
留意参照ASCⅡ码表,码值为48刚好对应数字零,也就是说这时候运算值是几,加上48后就是几;
而且由于是辗转相除,数值始终在0~9之间,不会出现大于9的数字。
#include <stdio.h>
#include <stdlib.h>
// 取余运算符 演示将数值转化为字符
int main(void)
{
int i = 12321;
while (i)
{
printf("%c\n", i % 10 + 48); // 或者写成 i % 10 + '0', 因为ASCⅡ第第十八位就是0
i = i / 10;
}
return 0;
}
// 20210419 20:58
关系运算符
使用&&运算符连接中间值判断,在形式上正确的东西,在计算机的逻辑中未必正确;
错误代码
如果写成下面的形式,那么代码会出错,原因在于前一个不等于语句的运算结果为真,那么值为1,1这个值恒定小于右边的数字;
正确代码应为:
/*judge_leap-year.c -尝试几种不同的运算符优先级-*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int year;
while (scanf("%d", &year)!=EOF)
{
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) // && > || 逻辑与的优先级大于逻辑或
{
printf("This year is a leap year\n");
}
else
{
printf("This is not a leap year\n");
}
}
return 0;
}
// 20210420 21:40
逻辑运算符(短路运算)
第一种
在内核和中间件中非常喜欢使用短路运算;
由于 与运算 的条件是全真为真,一假为假,所以只要输入值不正确,则会提示系统错误;
老师说设置这样的语句可以用来记录系统日志,静待观之;
/*-calculation_short-circuit.c 短路运算(逻辑运算)-*/
#include <stdio.h>
#include <stdlib.h>
// 在内核和中间价中非常喜欢使用短路运算
int main(void)
{
int j = 0;
j == 0 && printf("System is error.\n");
return 0;
}
// 20210420 21:55
第二种
注意:一旦与运算符前面的条件已经为假之后,后面的条件判断不会执行;
这样的操作是为了不写if语句却能实现该语句的功能,减少代码的数量并且提高书写速度;
/*-calculation_short-circuit.c 短路运算(逻辑运算)-*/
#include <stdio.h>
#include <stdlib.h>
// 在内核和中间价中非常喜欢使用短路运算
// 这样写代码是为了避免写if语句
int main(void)
{
int j = 1, i = 5;
j == 0 && (i = 3); // 直接不执行后面的赋值语句
printf("i = %d\n", i);
return 0;
}
// 20210420 21:55
第三种 — 逻辑非运算符
逻辑非运算符则和逻辑与运算符刚好相反,一假为真,全假为假,所以即使逻辑非运算符前一个条件语句为假,后面的语句依然会判断执行;
/*-calculation_short-circuit.c 短路运算(逻辑运算)-*/
#include <stdio.h>
#include <stdlib.h>
// 在内核和中间价中非常喜欢使用短路运算
// 这样写代码是为了避免写if语句
int main(void)
{
int j = 1, i = 5;
j == 0 && printf("System is error.\n"); // 直接不执行后面的赋值语句
j == 0 ||printf("System is error two.\n");
return 0;
}
// 20210420 22:15
位运算符
位图—数据去重
文件有三种权限,可读可写可执行;权限存储用位,一个字节可以存储多种权限;
存储框架,MySQL也用位;
检验逻辑运算能力;
第一个版本 — 演示左移位运算
// 左移:高位补零,低位丢弃
// 能产生值变化的运算方式就两种,赋值运算和自增减
// 左移乘二,但不能循环移位,最后会将正数数值转换为负数数值
/*-calculation_bit.c 详细解答位运算符的使用逻辑-*/
#include <stdio.h>
#include <stdlib.h>
// 左移:高位补零,低位丢弃
// 能产生值变化的运算方式就两种,赋值运算和自增减
// 左移乘二,但不能循环移位,最后会将正数数值转换为负数数值
int main(void)
{
int i = 5; // malloc (i << 30/20/10) 分别对应 1GB 1MB 1KB
printf("%d\n", i << 1); // 左移一位,相当于原值乘以2,运算效率比乘号高
printf("%d\n", i); // 左移之后i的值有没有变化呢?也是思考究竟是引用还是赋值?
return 0;
}
// 20210420 22:27