第二章:数据类型
第三节 printf和scanf函数介绍
int printf(const char *__restrict __format, ...);
int scanf(const char *__restrict __format,...)
先来说说char* 这是一个字符串,在这里我们把它叫格式化字符串,格式化字符串有一些特殊要求。我们下面再说明中具体介绍。
根据我们前面所说函数的定义,int是函数返回结果的数据类型,printf和scanf是函数名,括号内是函数参数,函数第一个参数的数据类型是字符串。但是大家注意到没有,这个__format字符串前面有一个__restrict,这个__restrict是干啥的,有啥作用。(这其实才是学C的难点,也是精髓。const这里先不介绍,后面会专门介绍这个,有点小麻烦)
__restrict我只能在这里给大家简单的说一下。比方说,有一个变量int a,这个变量必然有一个地址和它对应,叫做&a,那么我们现在有一个指针变量int *p1, 还有一个指针变量int *p2,这个时候,是不是可以让p1=&a,然后p2=&a。此时p1和p2就指向同一块内存。我们此时修改*p1的值,比方说,*p1=223。那么*p2的值是多少呢?显然也是223。
现在我们再来看下面一段代码:
int a,b,c;
int *p1,*p2,*p3;
p1=&a;
p2=&a;
p3=&a;
*p1=1;
*p2=2;
*p3=3;
*p1=11;
*p2=12;
*p3=13;
这一段代码执行结果是,a变量的值是13。而且,
*p1=1;
*p2=2;
*p3=3;
*p1=11,
*p2=12;
这五句话,可以省略不执行(先不要管程序员脑子抽了,写出这五句话的问题)。这是我们人很明确能看出来的问题,但我们的gcc编译器未必能看出这个问题。因为一个指针到底指向那一块内存,这个是不确定的,如果编译器去一个一个指针的检查,这是很费时间的。另外,程序执行情况很复杂,有可能同一句话,在不同的执行环境中,其中的指针到底指向那一块内存有时候还无法确定,因此,编译器准确知道每一条C语句中的指针到底指向那一段内存,是不可知的。如果编译器能够知道p1,p2,p3指向的是同一块内存,编译器是不是就可以把那五句废话删除,只执行最后一句*p3=13。这其实就是编译器的优化,所谓优化,就是可以打乱程序执行顺序,删除某些语句,不影响程序最终执行结果(任何情况下都不影响程序最终执行结果,也就是不改变程序执行逻辑)。在优化后,程序的执行速度,或者程序所占内存,或者执行程序所需要的电量可以大大降低。记住,这就是编译器的优化。也就是,编译器知道的信息越多,其所能做的优化也就越多。__restrict这个关键字就是告诉编译器,__format这个字符串指针,指向了一个字符串,且这个字符串只有__format这个指针指向这个字符串,没有其他指针指向这个字符串。我们通过__restrict这个关键字,将这个信息告诉了编译器,编译器得到的信息越多,能做出的优化也就越多。这个就是这个__restrict关键字的作用,其实就是给编译器提供额外的信息,让编译器能够做出足够的优化。(其实我们编写程序的过程,就是我们将我们要做的事情,通过计算机编程语言的形式告诉编译器,编译器再根据我们提供的信息,给我们生成可执行程序。所以,我们必须在写程序的时候,准确知道,我们每一句话都告诉了编译器什么信息)。
好了,讲解上述这个__restrict是想告诉大家,C语言本身的学习重点,难点不是在语言本身,而是在语言的外延。
为了很好的理解上面两个函数,我们再简单的说说C语言的字符串。字符串其实就是一堆字符按照顺序串在一起,就形成字符串了。但是字符串无论串多少字符,总得有个尽头吧,咋表示这个尽头呢?有两种办法,一种办法是字符串开头是一个占固定字节数的数字,例如两个字节,然后后面是字符串。最前面的占两个字节的数字就是字符串中字符的个数。这样是不是就把一个字符串说明白了。还有一个办法,就是我们使用一个特殊字符,这个字符有编码,但没有图形和这个编码对照。用这个字符来标记字符串的结束。这样,只要我们在字符串中,看见这个特殊字符,我们就知道,这个字符是字符串最后一个字符,字符串到此结束。(这个特殊字符,可以没有图形与之对应)。C语言采用后一种办法表示字符串,最后结尾字符的编码为0。
说明:
我们在电脑屏幕上看见的字符,其实是一个图形,这个图形对电脑来说,没有任何意义,但是我们人看到这个图形,就会产生一些想法。例如我们看见‘A’这个字符图形,是不是就知道,这个可能代表一,一个的意思。但电脑只认为这是一个图片,代表啥意思,不知道。我们把常用的字符图片进行编码,这些编码在电脑里面就代表这个字符图片。编码当然是二进制数据了,如果我们把某个二进制数据的类型定义为char字符类型的话,电脑就会根据这个编码,找出对应的字符图形。如果我们要求显示这个字符的话,程序就会将这个编码对应的图片显示给我们。
在字符中,一般是用编码来标识一些字符图形的,但是还有另外一种情况。比方说,我们在屏幕上显示字符,已经显示到一行的末尾了,这个时候是不是需要重新起一行来显示。但是电脑是傻的,必须要有对应的命令,才执行对应的操作。这个时候控制字符就登场了,这个字符没有图形和其对应,但是他有一个操作动作和这个字符编码对应,这就是控制字符。例如有一个控制字符就是让电脑换一行开始显示字符。
现在有一个比较恶心的事情,啥呢?对于有图形对应的字符,例如‘B’,我们可以通过单引号包围字符B的形式,来给某个变量赋值,说这个变量代表字符‘B’,但我们如何将一个控制字符赋值给某个变量呢。其实这个问题就是,控制字符常量如何表示?
为此,人们又发明了一种字符,这种字符叫转义符。这种字符,有编码,有图形与之对应,因此可以输入,也就是可以写出这种字符常量。有了这个转义符就好办了,这个转义符可以对其后紧跟的一个字符的含义进行重新解释。下面是例子:
现在我们规定‘\’就是一个转义符。而‘n’是一个普通的字符n。那么把它俩合并起来‘\n’,这个字符是啥意思呢?由于有转义符‘\’的存在,n就不是一个普通的字符了,他就是一个控制字符,表示换行的意思。通过这种方法,我们就可以输入控制字符常量了。
聪明的兄弟肯定又问了,那么我想输入‘\’字符,不把他作为转义符使用,咋整。简单,你只要输入‘\\’就可以了。‘\’本来是转义符,但转义符后面紧跟的‘\’意思就被前一个‘\’进行意思转换了,后边一个‘\’就没有其本来转义符的意思了,他就是一个普通字符,就是一个图片了。
这里再介绍一下占位符。占位符其实和字符的本质没啥关系,但是和printf和scanf函数有关系。而且这种技术在编程中经常使用,索性就在这里一起介绍了。所谓占位符,就是先占个茅坑,至于这个茅坑上蹲谁,先不管,先把这个位置占了。在程序执行的时候,按照一定的规则,找一个字符或者字符串,再把这个坑给填上,或者说把位置补上。通常的占位符由‘%*’来表示。其中*代表一些能够看见的字符。例如‘%d’就是最常见的占位符,这个占位符必须用有符号整数来进行替换。(前面我们也看见过这个)
好了,有了以上对字符串的附加说明后,
先介绍printf函数,这个函数第一个参数是一个字符串,这个字符串中可以包含占位符,控制字符。后面的省略号,代表了这个函数除了__format这个字符串参数外,还可以有多个其他类型的参数,这些参数是啥类型,有多少个,由__format这个字符串中的占位符来决定。
下面是一个例子:
/************printf-1.c*****************/
#include<stdio.h>
int main()
{
printf("this is a test\n"); //__format参数内没有占位符,因此只输出一个字符串。注意有转义符\n
printf("char %hhd short %hd int %d long %ld long long %lld\n",(char)1,(short)2,(int)3,(long)4,(long long)5);
/*
注意__format参数内有5个占位符,对应的5个值分别是1,2,3,4,5。
%hhd 显示char类型的有符号整数,以10进制的方式显示
%hd short 10进制方式显示
%d int 10进制方式显示
%ld long 10进制方式显示
%lld long long 10进制方式显示
*/
printf("unsigned char %hhu unsigned short %hu unsigned int %u unsigned long %lu unsigned long long %llu\n",
(unsigned char)1,(unsigned short)2,(unsigned int)3,(unsigned long)4,(unsigned long long)5);
/*
%hhu 显示unsigned char类型的有符号整数,以10进制的方式显示
%hu unsigned short 10进制方式显示
%u unsigned int 10进制方式显示
%lu unsigned long 10进制方式显示
%llu unsigned long long 10进制方式显示
*/
printf("%x,%o,%b\n",100,100,100);
/*
%x 以16进制的形式显示 unsigned int类型的值
%o 以8进制的形式显示 unsigned int类型的值
%b 以2进制的形式显示 unsigned int类型的值。
思考一下,如果以8进制的形式显示unsigned long类型的值咋显示,是不是%lo就可以了?是的,规律可以叠加的
hh 表示被显示的数占一个字节
h 表示被显示的数占2个字节
默认表示被显示的数占4个字节
l 表示被现实的数占的字节数是long类型所占字节数
ll 表示被现实的数占sizeof(long long)字节数。
这个也可以和%x,%o,%b进行叠加。
*/
printf("%#x,%#o,%#b\n",100,100,100);
/*
#表示,在显示的数字前面,添加标识,标识这个数是十六进制,八进制或者二进制的数,具体是几进制的,要看#后面是x,o,b。
上面一行printf的显示效果是0x64,0144 0b1100100
可以看出,0x标识16进制,0标识8进制,0b标识二进制。以上格式,其实就是16进制,8进制,二进制无符号常数的表示方法。
*/
printf("char %c,char* %s\n",'A',"yangdamao-maopaihuo");
/*
这个没啥说的,%c是字符的占位符,%s是字符串的占位符。
*/
printf("float %f %lf %Lf\n",(float)0.1,(double)0.2,(long double)0.3);
/*
%f 显示float类型,或者double类型的浮点数
%lf 显示float类型,或者double类型的浮点数
%Lf 显示long double类型的浮点数
*/
printf("%10.1s %10.2s %10.3s %10.4s\n","abce","abce","abce","abce");
printf("%-10.1s %-10.2s %-10.3s %-10.4s\n""","abce","abce","abce","abce");
/*
先说百分号“%”后面的“-”代表啥意思,这个是标识对齐方式的,有“-”标识左对齐,没有标识右对齐,
大家对比一 下上面两个printf的输出,就知道有啥区别了。
然后在说10.1,10.2,10.3,10.4代表啥意思,10代表的是,后面对应要显示的内容,占了10个字符的宽度,10.1 中,
小数点后面的1,2,3,4分别代表,显示了1个字符,2个字符,3个字符,4个字符。大家看显示内容,也可以看 出来。
当然,你可以改变这些数字的值,来调整你想要的显示格式。
*/
printf("%20.2f\n",100.2345);
/*
这个百分号后面的20,依然是显示内容所占的字符宽度。这个时候,小数点后面的2标识显示2位小数(四舍五入)。
*/
printf("%20.10d\n",10010);
/*
显示效果是右对齐的0000010010
可以看出来,整个显示内容占了20个字符宽度,而显示了10个字符。大家应该明白20.10的含义了吧,不足10个字 符,前面用0来补齐。
*/
return 0;
}
通过上面的例子,然后大家在自己试验一下,应该能够掌握printf函数显示各种数据的方法,而且一般常用的格式,大家应该也都能自己整出来了,如果还整不出来,想必是智商问题了,要学会联想,然后在根据自己想象进行推理,验证。你可以修改上面的例子程序,看看输出的效果。
我们再来看看scanf函数:
/*********************scanf-1.c********************/
#include<stdio.h> //scanf函数的声明也在stdio.h这个文件中
int main()
{
int length;
printf(“请输入您老人家的身高:”);
scanf(“%d”,&length);
printf(“我老人家的身高是 %d \n”,length);
return 0;
}
这个程序很简单,就是输入一个身高,然后显示出来。这个scanf函数的第一个参数也是__format,我们叫他格式字符串,和printf的第一个参数其实是一样的,也有占位符,上面这个程序中%d不就是有符号十进制整数的占位符吗?这个占位符对应的后面的参数是&length。我们知道&length指的是length这个变量的内存地址,也叫指针。这个scanf函数的用处是,我们在键盘上输入一个数字,或者字符串,或者字符,然后scanf函数把键盘输入的内容,转换成一定的格式(根据占位符所规定的格式来进行转换,占位符规则和printf中占位符规则一样,这里不再啰嗦)。所以,上述程序中,是将键盘输入转换成一个有符号的int类型的整数,然后把这个整数保存在length变量所在的内存位置。
说明:
大家还记得前面我说的,有时候必须使用变量的地址,使用变量的名字是没有办法完成我们所要的功能的。这里这个scanf其实就是这样,scanf函数接收的是length变量的地址,因为他要把内容保存到这个内存地址上。而对于变量的名字,scanf函数是不在乎的。大家先理解到这里,后边还有更详细的说明。
现在我们了解了printf和scanf函数的基本用法,我介绍的这些用法是这两个函数的最基本的用法,但其实有了这些基本用法,也差不多够用了。有兴趣的同学,可以专门查查这俩函数的说明(在网上查)。有人问,你这个冒牌货是不是不会,所以就不讲了。确实,其他用法我也记不住,就我给你讲的上面的内容,过两天我也不敢保证我能记住。但是用的时候我可以查呀,没有必要记。最重要的,你会了其他用法,不能说明你水平就高,不会也不能说明你C语言水平就低,没啥意思的东西。其实很多东西我们都没有必要记,用的时候再查,但是一定要知道,他大概有哪些功能,这才是最重要的。脑容量是非常珍贵的,不要把啥垃圾都往里装。