一、sizeof操作符
首先在开始今天的论述之前我们首先需要了解一下sizeof操作符,这是一个关键字,用来专门计算对应类型的长度
以上是一个关于 sizeof 的一个简单的代码,这是我当初一直会犯的一个错误,也就是错误的认为 sizeof 返回的是 int 类型,但是我们运行的时候就会发现上述的代码会报错,主要的原因是因为 sizeof 返回的类型是 size_t,不是 int。
正是因为类型不匹配的原因导致编译器发出警报,但是 Windows 64 环境下这个代码依然会输出 4,可以跑,只是类型会发生错误,属于未定义行为隐患(未定义行为:C 标准没有规定这个代码会输出什么结果,编译器想如何处理就如何处理。结果不可以进行预测,不一定会报错,但是可能会在未来留下隐患)。
上述的这个表就是对应的类型参照,这里我们只需要将对应的类型改为zu,对应的警告自然就消失了。
但是这里又衍生了一个问题,就是为什么sizeof返回size_t踩坑?
答:首先我们举例一个经典的代码:
int a = 2; if( a - sizeof(int) < 0 ) { printf("小于0"); } else { printf("大于等于0"); }我相信这里初学者一定会认为这里的答案会输出小于0,但是恰恰相反,结果是一个大于0的数字,这是为什么呢?
答:a-sizeof(int)的结果根本不是-2,而是一个大于的巨大正数,一个巨大的正数的结果当然不小于0,所以会走下面那一个分支。
根本原因:sizeof返回的类型不是int
这个就是整个问题的起点,sizeof的结果类型不是int,而是size_t(size_t是C语言中的无符号整数类型),一个无符号整型在与int一起进行计算的时候,会触发C的寻常算数转换,规则大致如下:
1、两个操作数先各自做整数提升
2、类型不同的时候向着“等级更高”的类型进行靠拢
3、当两者等级相同的、但是一个是有符号一个是无符号,有符号会被转化为无符号的
a的类型是int,sizeof(int)的类型是size_t,后者的等级不低于前者,触发第三条规则:a会被隐式转换为size_t。表达式实际就变成如下的内容:
(size_t)2 - (size_t)4这里因为无符号的减法会发生回绕,所以不会得到负数:
C标准规定无符号整数的运算结果按照2^N取模(N是该类型位宽)。所以2-4在无符号域里不是-2,而是绕到最大值的附近:
所有我们就可以看出小于0的是永远不可能成立的。
(2)sizeof的操作数不是类型而是表达式的时候可以直接省略掉后面的括号
在C语言标准中,sizeof是运算符,不是函数,这里我们区分的是
sizeof ( 类型名 ) // 语法1:类型,括号**必须写** sizeof 表达式 // 语法2:表达式,括号可以省略a、操作数是类型:括号是不可以省略的,必须写作sizeof(int),sizeof int就会发生报错
b、操作数是表达式(变量、常量、运算式子):外面的这一层括号就是可以省略的
(3)sizeof后面的表达式是不直接参与运算的,根据表达式的类型得到对应的大小
核心:sizeof是编译期运算符(大部分情况),编译器只看这个表达式最终是什么类型,不会直接计算其中的代码
通过上面这段代码就可以更加直接的看出问题所在,在这段代码中++这个副作用完全没有执行,a的值依旧是10,没有变为11。
(4)sizeof的计算结果是size_t类型的
二、算数操作符+ - * /
上述的这些操作符都是双目操作符。
注意:操作符也叫做运算符,是不一样的翻译,二者的本质是一样的
1、这里我们着重讲一下除法
(1)对于除法而言,如果两边都是整数的话,计算的就是整数的除法,不会得到小数的,得到的就是整除之后的商。
(2)除号两端只要有小数,计算的就是小数的除法
对于上述两点的论述,我们可以做出如下的总结:
a、两边都是整数类型的时候——整数除法(截断取商,直接舍弃小数部分,不是四舍五入)
5 / 2; // 两边int整数,结果int,等于 2 ,小数0.5直接扔掉 7 / 3; // 2 9 / 4; // 2重点:向0方向进行截断,正数负数都是朝0的方向砍去小数,不是向下取数。
这里需要注意的一点是即使你使用double类型进行接收,只要两边是int,除法依旧先做整数的除法!
b、规则2:只要任意一边是浮点数(float/double),就会触发浮点除法
另外一个整数会隐式的转换为浮点数,做真正的小数运算
5.0 / 2; //左边double,2提升double → 2.5 5 / 2.0; //右边double,5提升double →2.5 5.0 / 2.0; //两边都是double →2.5c、%f是用来打印小数的,默认为6位小数
2、对于取模运算,我们这里简要的叙述一下:
核心的硬性规则:%的两个操作数,必须全部是整数类型(int/short/long等),不会是浮点数。
在C语言中,a/b是整数除法,向零进行截断,余数a%b的符号与被除数a保持一致
(1)正数的情况(简单)
7 % 3; // 1,7 = 2*3 + 1 10 % 2; // 0,整除余数为0 8 % 5; // 3(2)负数(高频的考点,比较容易错)
余数的符号是与被除数的符号保持一致,不是看右边!
7 % 3 → 1 -7 % 3 → -1 //被除数是-7,余数负 7 % -3 → 1 //被除数是7,余数正 -7 % -3 → -13、赋值运算符
int main() { //int a = 0;//初始化 //a = 10;//赋值 int a = 3; int b = 5; int c = 0; //c = b = a + 3;//连续赋值,从右向左依次赋值的。 //c = (b = a + 3); b = a + 3; c = b; printf("c = %d b = %d\n", c, b); return 0; }以上就是对赋值运算符的一个简单运用
4、单目操作符:
只有一个操作数++ -- + -
++是一种自增的操作符,分为前置++与后置++;--是一种自减的操作符,分为前置--与后置--。
以下为对应的应用举例:
int main() { int a = 5; //a = a + 1; //a += 1; //++ 自增1的操作符 //a++;//后置++ ++a;//前置++ printf("%d\n", a); return 0; }这里我们需要记忆一下前置++与后置++的不同点:
int main() { int a = 5; //int b = a++;//后置++: 先使用,后+1 //b = a, a=a+1 int b = ++a;//前置++:先+1,后使用 //a=a+1, b=a printf("a = %d b = %d\n", a, b);//6 6 return 0; }这里有一个代码我们需要注意一下:
int main() { int a = 1; printf("%d\n", (++a) + (++a) + (++a));//错误的代码 return 0; }这段代码就是典型的未定义行为,代码逻辑发生错误。
在语法层面,这一段代码可以编译通过,但是不会报编译错误。但是在C语言层面上,这是未定义的行为,结果没有标准的规定吗,不同的编译器跑出来的结果是不一样的,所以这就是这一段代码为什么错误的原因。
三、printf与scanf的概念
首先简要的了解一下二者的作用是什么
输出数据:printf 输入数据:scanf
(1)printf:
printf是C语言的标准库函数,格式化输出到屏幕上,其实就是讲数据按照指定的格式进行打印,以下是最简单的运用:
int main() { printf("hello\n"); printf("world\n"); return 0; }在继续往下讲解的过程中,我们这里需要补充一下占位符的概念
(2)占位符
printf()可以在输出文本中指定占位符,所谓的“占位符”,就是可以在这个位置使用其他的值去代入。
int main() { //printf("%d", 100); printf("there are 3 apples\n"); printf("there are %d apples\n", 3); printf("there are %d apples\n", 5); printf("there are %d apples\n", 10); return 0; }在这个例子中,句子中的%d其实就是一个占位符,表示这个位置是可以使用其余的数值去替代的,占位符的第一个字符一定是%,第二个字符一定表示的是占位符的类型,%d表示这里可以输入一个整数;printf的第二个位置表示的就是替换占位符的位置,上面的例子中就是3替换掉了%d
接着就衍生出了多个占位符的概念:
这里需要注意的是printf的参数与占位符是一一对应的关系,如果有n个占位符,那么printf的参数就有n+1个,如果参数少于对应的占位符,可能会输出内存中的任何数值。
这里我们补充一个比较特殊的做法:
这里在%d之前在加上一个%就会打印出%d
原因:在printf的格式串中,%%本身就是一个转义序列,它的含义就是“输出一个普通的%字符”,所以格式在被扫描的时候流程就是这样的:首先就是在第0位,系统看到%,往下看一位就是%,识别为%%,两个字符一起被消费掉;对于第二位d,就是一个普通的字符,所以就会将d输出出来,最终为我们输出的就是%d。
所以这里的关键就在于:%后面看到的字符决定它是啥:
printf从左到右扫描格式串:每当遇到%,就会自动紧跟着看一个字符:
1、如果下一个字符是%,这是一个字面百分号的转义,两个字符一起消费,输出一个%,不读取任何的参数
2、如果下一个字符是别的,这就是一个转换说明符,会去可变参数列表里取一个参数来格式化
占位符的其余用法:
printf允许限定占位符的最小宽度:
这里%之后5的含义:最小字段宽度,要求这个数据至少占5个字符的位置,不够就使用空格进行补充,这里123只有三个字符,所以左边补了两个空格,实际的输出就是__123
这里我们补充一些配套使用的方法:
注意:这里%05d与%.5d的输出结果是一样的,但是二者的机制是不一样的,前者是“使用0填充到宽度5”,后者是“至少输出5位数字”。区别在于负数上会暴露,%05d配上-123打印的是-0123(0补在符号之后),而%.5得到的是-00123.
对于小数而言,这个限定符会限制所有数字的最小宽度。%f默认到小数点后六位。
这里我们需要明确的是在默认的情况之下,printf()并不会消失加号,如果想让数字显示加号,可以在占位符中添加一个+,所以在上面的例子中,%+d可以保证显示的数字中总是显示正负号。
这里表示的就是小数点之后保留几位数字。
注意:限定宽度的占位符与保留几位的小数是可以同时使用的
如上所示,最小宽度与小数位数为两个限定值,都是可以使用*进行替代的,通过printf进行参数的传入,输出部分字符串。
%s是用来输出字符串的,默认就会全部输出,如果只想输出开头的部分,可以使用%.[m]s指定输出的长度,其中[m]表示的就是一个数字,表示输出的长度。
概念补充:
对于C语言常见的概念符如下:
这里我们需要明确的一点就是:
在上面所以的占位符中,除了%c之外,其余的所有占位符都会默认自动忽略起始的空白字符,%c不会忽略空白字符,总是返回当前第一个字符,无论这个字符是否空格,下面就是一个例子:
在这一段代码中,这里没有打印x,因为输入的前面是有空的,所以遇到第一个字符就读走了。
但是如果我们在%c前面加上一个空格,就表示跳过零个或者多个空白字符。
接下来我们在着重的说一下占位符%s,它其实不可以简单的等同于字符串,它的规则是:
a、从当前第一个非空白字符开始读起来,直到遇到空白字符为止,先解释一下这里:
我们以一个简单的例子进行举例:
对于上面的这一段代码,假设我们输入的是hello world,很多人会认为这里输出的还是输入的答案
但是结果却恰恰相反,这里输出的是hello,原因就是scanf在使用%s的时候,会先跳过开头的空白字符,然后从第一个非空白字符开始读取,直到遇到下一个空白字符为止,所以这里输出的是hello。
b、因为%s不会包含字符,所有是没有办法读取多个字符的,除非多个%s一起使用,下面就是对应的例子:
此时我们输入hello world,对于的输出分别是hello以及world,但是我们这里需要注意的一点就是多个%s仅仅是读取多个单词,而不是将带空格的内容作为一个完整字符串进行保存。
c、另外,当scanf()遇到%s占位度,会自动在字符串的末尾加上一个空白字符\0,下面就是对应的例子:
这里我们就看到scanf在字符串的后面加上了一个\0,这就是工作的原理。
但是这里我们一定会有疑问,如果我就想要输入一个带空格的字符串该咋办?以下这张图片就是对应的方法:
这里%[^\n]表示的意思就是连续读取所有不是换行符的字符,直到遇到换行符为止。
(3)scanf:
当我们有了变量之后,我们需要给变量输入值就可以使用scanf函数了,如果需要将变量的值输出在屏幕上的时候可以使用printf函数:
这里在vs中会报错,主要是
(1)系统认为scanf是不安全的
(2)这里建议使用scanf_s去替代
(3)如果不想出现这个错误提示,就使用_CRT_SECURE_NO_WARNINGS
1、这里我们先讲述第一个问题,那就是为啥系统会认为scanf是不安全的?
答:核心的原因在于scanf不会检测字符串是否超过了数组的长度,所以存储字符串的时候很有可能已经超过了字符串的长度,导致意想不到的结果,我们以下面这个代码为例:
char buf[10]; scanf("%s", buf); // 用户输入 100 个字符会怎样?在这里我们想数组中输入100个字符,由于scanf只会看到buf这个指针,它看不到它指向的数组的大小是多少,于是它就会将所有的输入全部写进去——写穿数组、覆盖栈上相邻的局部变量与返回地址。这正是最经典的缓冲区溢出漏洞。
2、scanf_s:
a、这是函数提供的一个函数,这个函数的使用与scanf是有区别的
b、这个函数是vs提供的,其余的编译器是不认识的,如果你的代码中使用了scanf_s,就会存在跨平台问题
(不建议大家使用scanf_s函数,就使用scanf函数即可)
3、解决方法:
a、就是在当前的代码所在的第一行加上#define _CRT_SECURE_NO_WARNINGS
b、一劳永逸的方法:新建的.c文件的第一行默认就加上上述的代码
scanf函数用于读取用户的键盘输入,程序运行这个语句的时候会在自动的停下来,等待用户的输入;将其存入变量,它的原型就定义在stdio.h中。
注意:变量之前必须加上&运算符(指针变量除外),因为scanf传递的本身就不是一个值,而是一个地址,即将变量i的地址指向用户输入的数值
如果这里是指针变量(比如字符串变量),就不需要加&运算符
scanf在输出数据的时候会自动过滤掉空白的字符,包括空格、制表符、换行符等。
scanf处理数据时的原理:
用户的输入先放入缓存中,等下按在回车键的之后,按照占位符对缓存进行解读,解读用户的输入时,会从上一次解读遗留的第一个字符开始,或者遇到第一个不符合条件的字符为止
这里的读写规则只需要看下面这幅图即可:
这里-13被整数读走了,.45e12被浮点数读走了。下面我们依次讲解一下对应的顺序:
(1)首先对于整数的读取,开头的0-1空格直接跳过,-是一个合法的可选字符,接受;1、3为数字序列,接受;.不是十进制整数的合法序列所以这里解析终止
(2)这里我们依据对应的规则5,从上一次遗留地方开始,也就是下标为5的位置,这里我们首先需要了解一下:%f的合法构成是:[正负号]数字序列[指数部分],其中小数点两边的数字至少有一边存在即可,所以.45与45.都是合法的浮点数的写法:
位置5-7.读到.45是一个合法的序列,接受;位置8-10,读到e12,指数部分(e/E+可选符号+十进制数字),合法,接受;#不是浮点数的一部分,所以终止。
3、scanf的返回值:
这里我们需要知道的是scanf()的返回值是一个整数,表示成功读取的变量的个数。
如果没有读到任何项,或者匹配失败,就返回0;如果再读取到任何数据之前,发生了读取错误或者遇到读取到文件的末尾,就返回常量EOF(-1)。
下面的这张图片就解释了这一点:
如果我们想要让scanf读取失败就需要键盘上的ctrl+z键即可。
但是我们这里就会有一个问题就是scanf的返回值的作用是什么?scanf的返回值一般在竞赛的oj题目中使用的是比较多的,比较经典的就是下面的这段代码:
这里大家就会发现一个问题就是这样的代码仅仅只能测一组数据,这一组测试完就结束了,但是如果我们想要多测几组数据的时候需要咋做呢?以下就是对应的方法:
这就是scanf返回值的应用,只要scanf可以读到对应的值,这个循环就可以一直执行。
(4)赋值忽略符:
有的时候,用户的输入可能不符合预定的格式,就比如下面的代码:
这里出现问题的原因在于我们这里需要输入的格式中有-,但是后续我们输入的时候直接使用空格进行分割,计算机一直找,所以就会出错。
scanf的第一个参数中的格式一定要与输入的格式相互匹配才可以,所以我们在scanf中给的是啥格式,我们输入的时候就需要时啥格式。
为了避免这种情况,scanf提供了一个赋值忽略符*,只要将*加到任何占位符的百分号后面,这个占位符就不会有任何的返回值,解析之后就会被抛弃,对应的代码如下:
这里我们将*放在%c的后面就表示这个占位符没有对应的变量,解读之后就不需要进行返回。