【速过C primer plus】第四章

​ 本章重点:

​ 初识字符串

​ 初识预处理器

​ 控制台输入与输出

文章目录

  • 【速过C primer plus】第四章
    • 一、字符串常见陷阱
      • 1)`\0`结束符可以不写,但是必须给它预留空间
      • 2)手动加入的`\0`也会被识别并且作为结尾
      • 3)`%s`在scanf中无法存储空格
      • 4)`strlen()`和`sizeof()`有区别
      • 5)`strlen()`判定的结束标记是`\0`
    • 二、C预处理器的别名机制
    • 三、输入与输出
      • 1)宽度与精度
      • 2)默认参数提升
      • 3)返回值
      • 4)形象理解:scanf输入原理

一、字符串常见陷阱

1)\0结束符可以不写,但是必须给它预留空间

#include#pragma warning (disable:4996)int main(){char i[3] = "123";printf("%s", i);}//输出结果:123烫烫虁虐乴

​ 从输出看,很明显发生了溢出,这说明C风格的字符串必须遵守:字符串储存数要始终大于储存字符量至少1个字符(字节),否则会发生溢出,原因是因为字符串需要存储一个字节的\0字符来表明字符串结束。

2)手动加入的\0也会被识别并且作为结尾

#include#include#pragma warning (disable:4996)int main(){char a[40] = "1\0ABC";printf("%s,%c",a,a[2]);}//输出结果为1,A

​ 虽然在准备字符串时,给定了"1\0ABC",但是由于加入了\0字符串结束识别符,字符串的实际长度只有1,即只将1存储了进去。

​ 但是后面存储的字符是依旧存在的,但是并不算做字符串范围内,这里使用了a[2]强行越界读取,编译器报告了一个警告,但是依旧读取出了结果。

3)%s在scanf中无法存储空格

#include#pragma warning (disable:4996)int main(){char i[40];scanf("%s",i);printf("%s", i);}//输入:fasdaw a7221//输出结果:fasdaw

​ 在scanf()读取到空格时字符串发生了截断,只把fasdaw读取了,后面的a7221并没有读取。这是因为scanf的读取规则是:遇到第一个空白(空格、制表符或者换行符)时不再读取输入。之后会介绍原因。

4)strlen()sizeof()有区别

#include#include#pragma warning (disable:4996)int main(){char i[40];scanf("%s",i);printf("%d,%d", strlen(i),sizeof(i));}//输入:asd//输出:3,40

strlen()是用于计算字符串实际长度的,它会以\0为结束符来计算到底有几个实际字符存入了该字符串。

sizeof()是用来计算字符串容量的,无论存储了几个字符进去,他只会显示该字符串的最大容量。

sizeof()不是计算被测单位的字节数的吗?为什么可以直接显示字符数量?

​ 答:因为一个字符(char)的定义就是8位(1个字节),所以在用于字符串计算字符时,字节数和字符数是相等的。但是在计算其他单位时,结果很明显表示的是字节数。

5)strlen()判定的结束标记是\0

#include#include#pragma warning (disable:4996)int main(){char a[3] = "123";printf("%d",strlen(a));}//输出:15

​ 看上去很奇怪,明明a只有3个空间却输出了15的长度。

​ 原因是,strlen()在计算字符串长度时,要以\0作为结束识别符,但本例中a后面原本留给\0的位置被3占用了(溢出了),导致该字符串没有结束符。字符串前后的存储空间并不是空的,只不过并不在字符串的存储范围内,他们通常是一些垃圾数据。此时strlen()在超过字符串限制后会继续往后读取垃圾数据,直到它遇到\0才会结束读取,所以出现了字符量远大于字符串容量的情况。

二、C预处理器的别名机制

#include#pragma warning (disable:4996)#define A aint main(){char a[3] = "12";printf("%c", A[1]);a[1] = '4';printf("%c", A[1]);A[1] = '2';printf("%c", a[1]);}//输出结果:242

​ 别名机制:define除了可以定义常量以外,还是可以给变量定义别名。定义了别名后预处理器给的符号和原本的变量名都可以修改同一段存储的数据。

#include#pragma warning (disable:4996)#define A ABCint sum(){int ABC = 5;}int main(){int ABC = 10;printf("%d", A);}//输出结果:10

​ 别名机制可以读取main函数中的变量和全局变量。

​ 注意:自定义函数中的变量预处理器是无法使用的。本例中如果删除main函数中的ABC,将会报错,A无法拿到sum函数中的ABC的值。

三、输入与输出

1)宽度与精度

1.基础说明

printfscanf在输入输出时可以在占位符上添加修饰符,其中使用最多的就是宽度与精度。

%d加入宽度后:%数字d

%d加入精度后:%.数字d,请注意scanf是不可以描述精度的,scanf中只允许使用宽度。

​ printf中一个占位符可以同时存在宽度与精度:%数字.数字d

2.用法解释

宽度:

#include#pragma warning (disable:4996)int main(){int a = 123;printf("%4d\n", a);a = 12345;printf("%4d\n", a);a = 12;printf("%4d\n", a);}//输出结果:// 123//12345//12

​ 宽度是用来限制打印出结果占用的字符格。如果实际宽度大于限制宽度,则计算机会采用更大的宽度来输出全部字符;如果实际宽度小于限制宽度,则计算机会默认在数字左边补充空格保证对其。


精度:

#include#pragma warning (disable:4996)int main(){double a = 123.123;printf("%.4f\n", a);a = 12345;printf("%.4f\n", a);a = 123.123456;printf("%.4f\n", a);a = 123.111111;printf("%.4f\n", a);}//输出结果://123.1230//12345.0000//123.1235//123.1111

​ 精度是用来限制浮点数输出的,如果没有超出精度限制,则会再小数点后补充和精度相当的0;如果超过精度,则会遵循四舍五入的方式给小数点后的值进行舍入。

2)默认参数提升

​ 此处的部分解释引用了行者三个石的[博客][[C语言可变长参数函数与默认参数提升_行者三个石的博客-CSDN博客][https://blog.csdn.net/qq_33706673/article/details/84679343]

​ 引子:在scanf中如果要输入double变量,需要使用%lf;在printf中如果要输出double变量,则可以使用%f,虽然%lf也可以使用,但是在说明中,%lf是用来处理long double形式的,为什么会这样?

​ 解决这个问题需要从基础的C标准说起:

​ If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. – C11 6.5.2.2 Function calls (6)


​ 如果一个函数的形参类型未知, 例如使用了Old Style C风格的函数声明,或者函数的参数列表中有 …1,那么调用函数时要对相应的实参做Integer Promotion,此外,相应的实参如果是float型的也要被提升为double类型,这条规则称为Default Argument Promotion。

​ 1:参数列表有... :C语言提供了一种可变长参数函数,在函数的参数不确定时,可以用...作为替代,在函数实例化之后可以提供任意个数的参数参与函数计算。恰巧,printf的参数列表就是使用...作为替代的:int printf(const char *format, …);

C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_startva_argva_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:

void va_start(va_list ap, last);// 取第一个可变参数(如上述printf中的i)的指针给ap,last是函数声明中的最后一个固定参数(比如printf函数原型中的*fromat);type va_arg(va_list ap, type); // 返回当前ap指向的可变参数的值,然后ap指向下一个可变参数;type表示当前可变参数的类型(支持的类型位int和double);void va_end(va_list ap); // 将ap置为NULL

当一个函数被定义为可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。va_start使ap指向第一个可选参数。va_arg返回参数列中的当前参数并使ap指向参数列表中的下一个参数。va_end把ap指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。

​ 简单解释可以认为:因为函数对参数列表的不可见性,可以认为参数列表是一种类似于线性表的数据结构,而C语言提供了访问这个线性表的三个函数:取首值(va_start)、向下遍历一个(va_arg)、清除当前指针标签(va_end),通过这几个宏函数才能对传入的不可见参数进行操作。

​ 在调用可变长参数列表时,会默认触发默认参数提升机制:

——float类型的实际参数将提升到double——char、short和相应的signed、unsigned类型的实际参数提升到int——如果int不能存储原值,则提升到unsigned int

​ 所以对于double来说,%f是其原本定义下的输出符号,反而是float变量,在经过printf函数后会因为默认参数提升机制变为double,所以%f是为double准备的。

​ 而对于scanf来说,默认参数提升无法对指针起效,所以%f给float变量,%lf给double变量。

3)返回值

​ printf的返回值在正常情况下是返回输出了多少个字符,如果出现错误情况,printf会返回-1

​ scanf的返回值在正常情况下是返回成功读取的项数。如果没有读取到任何项,则返回EOF(一般情况下定义为-1)。

4)形象理解:scanf输入原理

引子:

#include#pragma warning (disable:4996)int main(){int a;char c;scanf("%d",&a);scanf("%c", &c);printf("%d,%c是结果", a, c);}//输入>>3 [enter]//输出<<//3,//是结果

​ 可能会有人遇到这种情况,明明写了两个scanf,在输入完一次之后,第二个scanf就不进行了,这时候我们换一种输入方式:

输入>>3A[enter]输出<<3,A是结果

​ 1.为什么会这样?我一次性输入了所有需要的值,但是第一个scanf只取了一个,第二个scanf也取了一个,完成了输入输出,这次没有出错,但是为什么我从始至终只输入了一次?

​ 分析第一次输入输出情况:

输入>>3 [enter]输出<<3,是结果

​ 明显发现,3,和是结果分行了,这说明c并不是没有接收值,而是c接收了一个[enter](换行符),并且输出了。在scanf的定义中,遇到空格、换行符会停止输入,便产生了这种情况。原本第一次输入时输入了3与换行符两个字符,由于是换行符,所以第二个scanf检测到后就直接跳过,没有输出。

结局方案:在产生这种情况时,可以在两个scanf之间加入一个getchar()来吞并多出来的换行符。

​ 2.但是为什么换行符会被赋值给了c?

​ 可以形象的理解为,scanf的输入和赋值是两部分组成,输入时通过一个输入池进行管理。运行到scanf位置时,输入池打开时,可以向里面输入任何字符都会被保存在输入池中(包括enter键的换行符),识别到换行符时关闭输入池,scanf按顺序从输入池中取出相应的字符进行赋值。

​ 如果在第一次打开输入池时,输入的值满足了后续赋值所需要的数据个数,同样运行到该scanf位置上时,会直接从输入池中取出所需的值进行赋值,而不再开放输入池。

#include#pragma warning (disable:4996)int main(){int a, b;scanf("%d", &a);scanf("%d", &b);printf("%d,%d是结果", a,b);}//输入>>1 3//输出<<1,3是结果

​ 3.还有一个问题,为什么第二个scanf接收值不是char类型时,不会发生scanf被吞并的现象?

​ 还是刚刚那串代码,却允许我们连续输入:

输入>>3[enter]1[enter]输出<<3,1是结果

​ 对比有char的情况,这种却可以运行的。scanf并没有将池子里多出的[enter]赋值给b,而是在遇到第二个scanf时重新开启了输入池,在输入结束后,将检测到的数字赋值给b。

​ 原因有两个:

​ 1.scanf自身有一定的输入池检测能力,它能清楚被赋值的数到底需要什么类型的值,如果类型不匹配,则会选择一种方式输出:输出乱码或者输出换行符之后的东西。

​ 2.接收值有char变量时,降低了scanf对于字符类型的灵敏度,这时候换行符将不再会被检测为无关数据,而是会以可能存在的输入值保存在输入池中,所以在最初的例子中,scanf实际在输入池中识别到的字符是3和换行符两个,满足了后续输入,所以第二个输入池不再开放,将换行符赋给了c。