目录
前排提示,此篇文章较长~
正文开始
一、数据类型介绍
程序离不开数据。把数字、字母和文字输入计算机,就是希望它利用这些数据完成某些任务。
使用整型类型来描述整数,使用字符类型来描述字符,使用浮点型类型来描述小数。
所谓“类型”,就是相似的数据所拥有的共同特征,编译器只有知道了数据的类型,才知道怎么操作 数据。
下面是C语言提供的各种数据类型:
为了便于C语言的理解与学习,以上并不是C语言数据的全部类型,剩余类型将在后续部分中指出
1.字符型
1. char //取值范围: 0~127 通常为8位
2. signed char //有符号的char 取值范围: -127~127 通常为8位
3. unsigned char //无符号的char 取值范围: 0~255 通常为8位
字符型,顾名思义,用于存储字符(英文字母,特殊符号等)。计算机使用数字编码来处理字符,即用特定的整数表示特定的字符(例如最为常见的ASCLL码表),因此字符型本质上是整型。
/*ASCLL码表*\
其中,整数98代表小写字母b,所以存储字母b实际上存储的是整数98(其他字符以此类推~)
2.整型
C语言把不含小数点和指数的数作为整数。例如21,32,-14,等整数,在C语言中都属于整型;而例如22.0和2.2E1则不是。
整型包括字符型、短整型、整型、长整型,它们都分为有符号(signed)和无符号(unsigned)
//短整型
1. short int //取值范围: -32768~32767 通常为16位
2. unsigned short int //取值范围: 0~65535 通常为16位
//整型
1. int //取值范围: -2147483647~2147483647 通常为32位
2. unsigned int //取值范围: 0~4294967295 通常为32位
//长整型
1. long int 或 long //取值范围: -2147483647~2147483647(32位环境下) 至少为32位,64位环境下通常为64位
2. unsigned long int //取值范围: 0~4294967295(32位环境下) 至少为32位,64位环境下通常为64位
//更长的整型(C99标准引入)
1. long long int //至少为64位
2. unsigned long long int 或 unsigned long long //至少为64位
那么为什么整型会有这么多种类呢?
由上我们可以知道,int类型的最大数为32767,unsigned类型的最大数为65535,假如要存入100000这个常量呢?显然,int 和 unsigned int是无法存入100000这类比较大的数的,所以为了数据的存入,就引入了long 和 unsigned long。随着时代发展,存储数据的需求也越来越大,所以在C99标准中引入了long long 和 unsigned long long。
如果一个数超出了int类型的取值范围,且在long类型的取值范围内,则这个数用long来定义,而一个数如果在int类型和long类型的取值范围内,为了节省空间,将这个数用int来定义。我们将以上过程称为数据类型的转换。既然有数据类型的转换,那么这些类型一定会有精度等级高低之分:
unsignd long long | 高 |
long long | |
unsigned long | |
long | |
unsigned int | |
int | 低 |
short 和 char类型在类型转换时将自动转换为int类型或unsigned int(依计算机的自然字长而定)。由低等级类型转换为高等级类型称为升级,由高等级类型转换为低等级类型称为降级。在C语言处理数据的过程中,要依据不同情况选择不同的数据类型。如果在一些特定情况下要用高等级数据类型来存储一个较小的数,可以在其末尾加上不同的后缀。例如,将3存入long long类型中写为3LL(或3ll),若存入无符号类型,则加上字母U(或u)(顺序可在LL前也可在LL后),依此类推~
3.浮点型
1. flaot //单精度浮点数 取值范围10e-38~10e38 通常为32位
2. double//双精度浮点数 取值范围10e-308~10e308 通常为64位
3. long double //至少为64位
为了存储更多的数据,只有整数是远远不够的,因此C语言引入了浮点型来存储小数和指数。如22.0和2.2E1
数字 | 指数记数法 |
1000000000 | 1.0e9或1.0E9 |
123000 | 1.23e5或1.23E5 |
322.56 | 3.2256e2或3.2256E2 |
0.000056 | 5.6e-6或5.6E-6 |
/*记数法示例*\
由以上我们可以知道,float、double、long double的取值范围比整型的取值范围更大,因此浮点型的精度等级比整型高,在数据类型转换中,我们可以由整型升级为浮点型,也可以由浮点型降级为整型。浮点型等级由高到低依次为long double、double、float。接下来为整型。
4.布尔类型(_Bool)
C语言原来并没有为布尔值单独设置一个类型,而是使用0表示为假,非0为真(这在后续判断条件中经常使用)。因此在C99标准中引入了布尔类型来专门表示真假。
要使用布尔类型,需要包含 头文件<stdbool.h> 其取值为True或False
5.各种数据类型的长度
每⼀种数据类型都有自己的长度,使用不同的数据类型,能够创建出长度不同的变量,变量长度的不同,存储的数据范围就有所差异。
在计算数据类型的长度前,先介绍一下C语言中一个重要的关键字:sizeof.
sizeof 是C语言中的一个运算符,用于获取任何数据类型或对象在内存中的大小,单位是字节(Byte)。sizeof的操作数可以是类型,也可以是变量或者表达式。sizeof 的操作数如果不是类型,是表达式的时候,可以省略掉后边的括号的。sizeof 后边的表达式是不真实参与运算的,根据表达式的类型来得出大小。
• sizeof(类型)
• sizeof 表达式
sizeof的计算结果是size_t类型的。
sizeof 运算符的返回值,C语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定 sizeof 到底返回什么类型。不同的系统中,返回值的类型有可能是 unsigned int ,也有可能是 unsigned long ,甚至是 unsigned long long ,对应的 printf() 占位符分别是 %u 、%lu 和 %llu 。这样不利于程序的可移植性。C语言提供了⼀个解决方法,创造了⼀个类型别名 size_t ,用来统⼀表示 sizeof 的返回值类型。对应当前系统的 sizeof 的返回值类型,可能是 unsigned int ,也可能是 unsigned long long 。
了解 sizeof 后,我们可以通过sizeof来计算数据类型所占空间的大小:
#include <stdio.h>
#include <stdbool.h>
int main()
{
printf("char类型的大小: %zd\n", sizeof(char));
printf("_Bool类型的大小: %zd\n", sizeof(_Bool));
printf("short类型的大小: %zd\n", sizeof(short));
printf("int类型的大小: %zd\n", sizeof(int));
printf("long类型的大小: %zd\n", sizeof(long));
printf("long long类型的大小: %zd\n", sizeof(long long));
printf("float类型的大小: %zd\n", sizeof(float));
printf("double类型的大小: %zd\n", sizeof(double));
printf("long double类型的大小:%zd\n", sizeof(long double));
return 0;
}
结果如下(VS2022 64位环境):
sizeof中表达式不计算:sizeof在代码进行编译的时候,就根据表达式的类型确定了,类型的常用,而表达式的执行却要在程序运行期间才能执行,在编译期间已经将sizeof处理掉了,所以在运行期间就不会执行表达式了。
#include <stdio.h>
int main()
{
short s = 2;
int b = 10;
printf("%d\n", sizeof(s = b+1));
printf("s = %d\n", s);
return 0;
}
结果:
6.数据类型的取值范围
其实每一种数据类型有自己的取值范围,也就是存储的数值的最大值和最小值的区间,有了丰富的类型,我们就可以在适当的场景下去选择适合的类型。如果要查看当前系统上不同数据类型的极限值:
limits.h 头文件中说明了整型类型的取值范围。
float.h 头文件中说明浮点型类型的取值范围。
为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。
limits.h 头文件中的常量:
• SCHAR_MIN SCHAR_MAX : signed char 的最小值和最大值
• SHRT_MIN SHRT_MAX : short 的最小值和最大值
• INT_MIN INT_MAX : int 的最小值和最大值
• LONG_MIN LONG_MAX : long 的最小值和最大值
• LLONG_MIN LLONG_MAX : long long 的最小值和最大值
• UCHAR_MAX : unsigned char 的最大值
• USHRT_MAX : unsigned short 的最大值
• UINT_MAX : unsigned int 的最大值
• ULONG_MAX : unsigned long 的最大值
• ULLONG_MAX : unsigned long long 的最大值
float.h 头文件中的常量:
• FLT_MANT_DIG : float 的尾数位数
• FLT_DIG : float 的最少有效数字位数(十进制)
• FLT_MIN_10_EXP : 带全部有效数字的 float 的最小负指数(以10为底)
• FLT_MAX_10_EXP : float 的最大正指数(以10为底)
• FLT_MIN : 保留全部精度的 float 最小正数
• FLT_MAX : float 的最大正数
• FLT_EPSILON : 1.00和比1.00大的最小 float 值之间的差值
以上为C语言中基本数据类型的介绍,了解完C语言中的数据类型,接下来我们来探讨如何使用它们(创建变量).
二、变量
在程序的指导下,计算机可以做很多事情。要完成这些事情,程序需要使用数据,即承载信息的数字和字符。有些数据类型在程序使用之前就已经预先设定好了,在整个程序的的运行过程中没有变化,这些称为常量。其他数据类型在程序运行期间可能会被改变或被赋值,这些称为变量。
那么该如何创建变量呢?
1.创建变量
data_type name;
//data_type为数据类型
//name为变量名
例如:
1. int age;//整型变量
2. char ch;//字符变量
3. double weight;//浮点型变量
变量在创建的时候给一个初始值,叫做初始化。初始化可以直接在创建变量时完成,只需在变量名后面加上赋值运算符( = )和待赋给变量的值即可。
1. int age = 18;
2. char ch = 'w';
3. double weight = 50.0;
4. unsigned int height = 100;
若变量不进行初始化,内存将会自动进行随机分配,此时,变量的数值将会是随机值,这对此后程序的执行会有很大的影响,因此,初始化是非常必要的。
2.变量的分类
• 全局变量 : 在大括号外部定义的变量就是全局变量。全局变量的使用范围更广,在整个程序中可以直接调用。
• 局部变量 : 在大括号内部定义的变量就是局部变量。局部变量的使用范围比较局限,只能在自己所在的局部范围内使用。
#include <stdio.h>
int global = 2023;//全局变量
int main()
{
int local = 2018;//定义在main函数中的局部变量
printf("%d\n", local);
printf("%d\n", global);
return 0;
}
但如果全局变量和局部变量的名称一样呢?
#include <stdio.h>
int n = 1000;
int main()
{
int n = 10;
printf("%d\n", n);//打印的结果是多少呢?
return 0;
}
结果:
由此我们可以得出:当全局变量和局部变量的名称相同时,局部变量优先使用。
三、算术操作符:+、-、*、/、%
在写代码时候,⼀定会涉及到计算。 C语言中为了方便运算,提供了⼀系列操作符,其中有⼀组操作符叫:算术操作符。分别是: + - * / % ,这些操作符都是双目操作符。
1. + 和 -
运算符 + 和 - 分别用来完成加法和减法运算。+ 和 - 都是有2个操作数的,位于操作符两端的就是它们的操作数,这种操作符也叫双目操作符。
#include <stdio.h>
int main()
{
int x = 4 + 22;
int y = 61 - 23;
printf("%d\n", x);//输出 26
printf("%d\n", y);//输出 38
return 0;
}
其中操作数可以是像 4 22 这类的数,也可以是像 x y 这样的变量名(前提是变量要初始化)
2. *
运算符 * 用来完成乘法运算。它和+、-一样是有2个操作数的,所以它也是双目操作符。
#include <stdio.h>
int main()
{
int num = 5;
printf("%d\n", num * num);//输出 25
return 0;
}
3. /
运算符 / 用来完成除法运算。它同样是双目操作符。
除号的两端如果是整数,执行的是整数除法,得到的结果也是整数。
#include <stdio.h>
int main()
{
float x = 6 / 4;
int y = 6 / 4;
printf("%f\n", x);//输出 1.000000
printf("%d\n", y);//输出 1
return 0;
}
上面示例中,尽管变量 x 的类型是 float ,但是 6 / 4 得到的结果是 1.0 而不是 1.5 。原因就在于 C 语言里面的整数除法是整除,只会返回整数部分,直接丢弃小数部分(所以也就不会有数学上所谓的四舍五入)。这一过程叫做截断
如果希望得到浮点数的结果,两个运算数必须至少有⼀个浮点数,这时 C 语言就会进行浮点数除法。实际上,计算机不能真正地将浮点数和整数相除,编译器会把两个运算对象转换为相同的类型。
#include <stdio.h>
int main()
{
float x = 6.0 / 4;//也可以写成 6 / 4.0 或 6.0 / 4.0
printf("%f\n", x);//输出 1.500000
return 0;
}
但如果是负数的整除运算呢?例如对于3.8而言,在整除运算时会直接进行截断,将3.8处理为3。那如果为-3.8呢?在C99标准之前,舍入过程采用小于或等于浮点数的最大整数,即会将-3.8转换为-4。但在C99及C11标准中将这一方法舍弃了,定义了一种新的方法——趋零截断。即将-3.8的小数部分直接进行截断,将-3.8转换为-3。这种方法也会在强制类型转换(将在后续讲解)中体现:
结果:
4. %
运算符 % 用来完成求模运算。也是双目操作符。求模运算符只能用于整数运算,不能用于浮点数。求模运算符常用于控制程序流。
#include <stdio.h>
int main()
{
int x = 13 % 5;
printf("%d\n", x);//输出 3
return 0;
}
负数求模的规则是:结果的正负号由第一个运算数的正负号决定。
#include <stdio.h>
int main()
{
printf("%d\n", 11 % -5);//输出 1
printf("%d\n",-11 % -5);//输出 -1
printf("%d\n",-11 % 5);//输出 -1
return 0;
}
除了%操作符,其余的操作符都是既适用于浮点类型,又适用于整数类型。
以上为C语言中的四则运算,了解以上的操作符,就已经可以进行C语言中的大部分运算了。
四、赋值操作符:= 和复合赋值
在数学的运算中,= 意味着相等。但在C语言中,= 是一个赋值运算符。例如下列第2行代码就是赋值表达式,即将 180 这个数赋给变量 height 。赋值操作符 = 是⼀个随时可以给变量赋值的操作符。
1. int height;
2. height = 180;
赋值操作符也可以连续赋值,如:
int a = 3;
int b = 5;
int c = 0;
c = b = a+3;//连续赋值,从右向左依次赋值的。
C语言虽然支持这种连续赋值,但是写出的代码不容易理解,建议还是拆开来写,这样方便观察代码的执行细节。因此,建议改成以下写法:
int a = 3;
int b = 5;
int c = 0;
b = a+3;
c = b;
这样写,在调试时,每⼀次赋值的细节都是可以很方便的观察的。
再来思考下面代码:
#include <stdio.h>
int main()
{
int i = 5;
i = i + 1;
printf("%d\n", i);//输出 6
return 0;
}
i = i + 1这行代码对数学而言是不符合逻辑的,如果给一个有限的数加上1,它绝不可能“等于”原来的数。但是在C语言中,这种代码是完全正确可行的。该语句的意思是: 找出变量 i 的值,把该值加1,然后再把新的值赋给变量 i。因此,如果要是将上面代码运行的话,结果将是6。但是这样写代码似乎还不够简洁大方,因此C语言还给出以下写法:
int i = 5;
i+=1;
i-=2;
C语言中提供了复合赋值符,方便我们编写代码,这些赋值符有:
1. += -=
2. *= /= %=
//下面的操作符将在后期讲解
3. >>= <<=
4. &= |= ^=
但在C语言中,类似这样的语句没有意义,编译器会直接报错,因此要避免这样的写法:
5 = i;
以上是C语言中大部分的双目操作符,接下来介绍C语言中的单目操作符。
五、单目操作符:++、--、+、-
前面介绍的操作符都是双目操作符,有2个操作数的。C语言中还有一些操作符只有一个操作数,被称为单目操作符。 ++、--、+(正)、-(负) 就是单目操作符。
1. ++ 和 --
++ 是一种自增操作符,分为前置 ++ 和后置 ++ ,-- 是一种自减操作符,也分为前置 ++ 和后置 --
//前置++
int a = 10;
int b = ++a;//++的操作数是a,是放在a的前⾯的,就是前置++
printf("a=%d b=%d\n",a , b);
计算口诀:先+1,后使用;
a原来是10,先+1,后a变成了11,再使用就是赋值给b,b得到的也是11,所以计算技术后,a和b都 是11,相当于这样的代码:
int a = 10;
a = a+1;
b = a;
printf("a=%d b=%d\n",a , b);
//后置++
int a = 10;
int b = a++;//++的操作数是a,是放在a的后面的,就是后置++
printf("a=%d b=%d\n",a , b);
计算口诀:先使用,后+1;
a原来是10,先使用,就是先赋值给b,b得到了10,然后再+1,然后a变成了11,所以直接结束后a是 11,b是10,相当于这样的代码:
int a = 10;
int b = a;
a = a+1;
printf("a=%d b=%d\n",a , b);
如果你听懂了前置 ++ 和后置 ++ ,那前置 -- 和后置 -- 也是同理的,只是把加1,换成了减1。
//前置--
int a = 10;
int b = --a;//--的操作数是a,是放在a的前⾯的,就是前置--
printf("a=%d b=%d\n",a , b);//输出的结果是:9 9
计算口诀:先-1,后使用;
//后置--
int a = 10;
int b = a--;//--的操作数是a,是放在a的后⾯的,就是后置--
printf("a=%d b=%d\n",a , b);//输出的结果是:9 10
计算口诀:先使用,后-1;
有了自增运算符和自减运算符,在以后编写代码的时候将会更加简洁方便,机器语言代码效率也会更高。
2. + 和 -
这里的+是正号,-是负号,都是单目操作符。运算符 + 对正负值没有影响,是⼀个完全可以省略的运算符,写了也不会报错。
int a = +10;
等价于 int a = 10;
运算符 - 用来改变一个值的正负号,负数的前面加上 - 就会得到正数,正数的前面加上 - 会得到负 数。
int a = 10;
int b = -a;
int c = -10;
printf("b=%d c=%d\n", b, c);//这里的b和c都是-10
int a = -10;
int b = -a;
printf("b=%d\n", b); //这里的b是10
在前面已经提到过强制类型转换,接下来详细介绍一下~
六、类型转换
1.强制类型转换
在操作符中还有⼀种特殊的操作符是强制类型转换,语法形式很简单,形式如下:
(数据类型)
请看代码:
结果:
以上代码是在VS2022 64位环境所输出的结果,在编译时貌似不会出错,但是在解读代码的时候可能会出现一些误解:在创建变量 a 时,定义 a 的类型为 int ,但是在初始化变量 a 时却为 a 赋值了浮点数3.14 假使我们没有进行任何编译和输出而单看
int a = 3.14;
这行代码,在不考虑编译器的情况下,输出的会是3还是3.14呢?从上面的代码结果来看,VS2022在编译时自动地将3.14的小数部分截断,因而输出结果显示为3。但为了避免这种不必要的争议以及良好的编写代码的习惯,我们通常会在3.14的前面加上(int),此时,浮点类型就会被降级为整型,这样就可以将3.14的小数部分进行截断,将3.14转化为3。
既然强制类型转换可以帮助我们很好地将一个数据类型转换为另一个数据类型,那么在编写程序时,我们可以运用强制类型转换写出更为灵活的代码。
我们在之前就已经提到过数据类型的精度等级:
数据类型 | 精度等级(由下往上) |
long double | 高 |
double | |
float | |
unsigned long long | |
long long | |
unsigned long | |
long | |
unsigned int | |
int | 低 |
/*数据类型的精度比较*\
表中之所以没有出现unsigned / signed 的 char 和 short ,是因为编译器在编译时会将 char 和 short 自动升级为int,这种情况被称为整型提升。如果有必要会升级为unsigned int (前提是 short 和 int 的大小相同,unsigned short 就会比 int 大。这种情况下 unsigned short 就会被升级为unsigned int) 。作为函数参数传递时,char 和 short 同样会直接被转换成 int ,而 float 则会被转换为 double 。
俗话说,强扭的瓜不甜,我们使用强制类型转换都是万不得已的时候使用,如果不需要强制类型转化就能实现代码,这样自然更好的。
2.隐式类型转换
在C语言中,隐式类型转换(也称为类型提升或自动类型转换)是指在表达式求值过程中,编译器自动将一个类型的值转换为另一种类型,而无需程序员显式地进行类型转换。这种转换通常发生在不同数据类型的操作数参与运算时。隐式类型转换的规则取决于参与运算的数据类型以及运算符的类型。隐式类型转换包括:整型提升、浮点提升、算术转换、指针转换、函数参数。
整型提升:当一个小的整数类型(如 char 或 short)与一个较大的整数类型(如 int)
一起参与运算时,小的整数类型会被提升为较大的整数类型。例如:
char ch = 'A';
int i = ch;//ch被隐式转换为 int 类型
再例如下列代码,b和c的值被提升为 int 类型,然后执行加法运算。加法运算得出来的结果将会被截短,然后再存储于a中:
char a,b,c;
...
a = b + c;
浮点提升:当整数与浮点数一起参与运算时,整数会被提升为浮点数。例如:
int j = 10;
float i =j;//j被隐式转换为 float 类型
算术转换:在算术运算中,如果操作数的类型不同,较小的类型会被提升到较大的类型。例如:
double d = 5.0;
int i = 3;
double result = d + i;//i被隐式转换为 double 类型
指针转换:在指针运算中,不同类型的指针之间也会发生隐式转换。例如,将 void* 类型的指针赋值给其他类型的指针。
函数参数:调用函数时,如果实参的类型与形参的类型不匹配,实参会发生隐式类型转换以匹配形参的类型。
需要注意的是,尽管隐式类型转换在很多情况下是方便的,但是它可能会导致精度丢失或意外的行为,特别是在涉及浮点数和整数转换时,所以要谨慎在求值过程中让编译器自动转换数据类型!因此,在编写代码时,了解隐式类型转换的规则并在必要时使用显式类型转换(即强制类型转换)是很重要的,强制类型转换和隐式类型转换搭配使用才能使代码的效率变得更高。
七、操作符的优先级和使用顺序(部分)
这里只介绍本文章所涉及的操作符的优先级和使用部分,剩余操作符将在后续文章中补充!
操作符 | 描述 | 优先级(由上往下) |
++ | 后置++ | 高 |
-- | 后置-- | |
+ | 单目,表示正值 | |
- | 单目,表示负值 | |
++ | 前置++ | |
-- | 前置-- | |
sizeof | 取其长度,以字节表示 | |
(类型) | 类型转换 | |
* | 乘法 | |
/ | 除法 | |
% | 整数取余 | |
+ | 加法 | |
- | 剑法 | |
= | 赋值 | |
+= | 以...加 | |
-= | 以...减 | |
*= | 以...乘 | |
/= | 以...除 | |
%= | 以...取模 | 低 |
/*操作符的优先级(部分)*\
如果表达式中的操作符超过一个,是什么决定这些操作符的执行顺序呢?C的每个操作符都具有优先级,用于确定它和表达式中其余操作符之间的关系。但仅凭优先级还不能确定求值的顺序。求值规则如下:
两个相邻操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,则执行顺序由它们的结合性决定。除此之外编译器可以自由决定使用何种顺序对表达式进行求值,只要不违背逗号、&&、||、和三目操作符所施加的限制即可。
有时C的优先级会失去作用,例如:
a * b + c * d + e * f
在数学上,乘法的优先级比加法要高,因此在这个表达式中我们先求得a * b、c * d、e * f的值,再将三者的值相加得出最后的结果。按C的思维,如果仅由优先级决定这个表达式的求值顺序,那么所有的乘法运算将再所有加法运算之前进行。事实上,只要保证每个乘法运算在它相邻的加法运算之前执行即可。在数学上我们习惯由左向右依次计算,但乘除法的优先级并列,在顺序上并无严格的先后之分,所以不妨将这个思维移用到C中来,那么这个表达式的计算顺序可以是:
a * b;
c * d;
a * b + c * d;
e * f;
a * b + c * d + e * f;
也可以是:
c * d;
e * f;
a * b;
a * b + c * d;
a * b + c * d + e * f;
加法运算的结合性要求两个加法运算按照先左后右的顺序执行,但它对表达式剩余部分的执行顺序并未加以限制。尤其是这里并没有任何规则要求所有的乘法运算首先进行,也没有规则规定这几个乘法运算之间谁先执行。优先级规则在这里都起不到作用,优先级只对相邻操作符的执行顺序起作用。
另外严禁类似以下表达式语句出现:
c + --c;
i++ + i + ++i;
这类表达式语句大多都没有实际意义,在程序中表现出这样的代码只会带来不必要的争议和错误。
八、scanf() 和 printf()
printf函数和scanf函数能让我们与程序进行交流,它们是输出/输入函数。虽然两者的作用不同,但是它们的工作原理几乎相同,都使用格式字符串和参数列表。
1.printf()函数
printf()函数的作用是将参数文本输出到屏幕。它名字里面的 f 代表 format (格式化),表示可以定制输出文本的格式。
例如
#include <stdio.h>
int main()
{
printf("Hello,bit");
return 0;
}
上面命令会在屏幕上输出一行文字“Hello bit”。 printf() 不会在行尾自动添加换行符,运行结束后,光标就停留在输出结束的地方,不会自动换行。如果要让光标移到下一行的开头,可以在输出文本的结尾,添加⼀个换行符 \n 。
#include <stdio.h>
int main()
{
printf("Hello bit\n");
return 0;
}
printf函数是在标准库的头文件stdio.h定义的。使用这个函数前,必须在源码文件头部引入这个头文件。
2.printf中的转换说明(占位符)
使用printf函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印字符时使用%c,等等。这些符号被称为转换说明,也可以被称为占位符,它们指定了如何把数据转换成可显示的形式。输出文本里面可以使用多个占位符。下表是一些占位符和各自对应的类型输出。
占位符 | 输出 |
%a / %A | 浮点数、十六进制数和p计数法(C99/C11) |
%c | 单个字符 |
%d / %i | 有符号十进制整数 |
%e / %E | 浮点数,e记数法(指数记数法) |
%f | 浮点数,十进制记数法 |
%g / %G | 根据值的不同,自动选择%f或%e(或%E)。后者用于指数小于-4或者大于等于精度时 |
%o | 无符号八进制整数 |
%p | 指针 |
%s | 字符串 |
%u | 无符号十进制整数 |
%x / %X | 无符号十六进制整数,使用十六进制数 0f (0F) |
%% | 打印一个百分号 |
%Lf | long double类型的浮点数 |
%n | 已输出的字符串数量。该占位符本⾝不输出,只将值存储在指定变量之中。 |
%zd | size_t类型 |
/*占位符*\
printf的参数与占位符是一一对应关系,如果有 n 个占位符, printf的参数就应该有 n + 1 个。如果参数个数少于对应的占位符, printf可能会输出内存中的任意值。
3.printf的转换说明修饰符
标记 | 含义 |
- | 待打印项左对齐。即从字段的左侧开始打印该项 |
+ | 有符号值若为正,则在值前面显示加号;若为负,则在前面显示减号 |
空格 | 有符号值若为正,则在值前面显示前导空格(不显示任何符号);若为负,则在值前面显示减号并覆盖空格 |
# | 把结果转换为另一种形式。如果是%o格式,则以0开始;如果是%x或%X格式,则以0x或0X开始;对于所有的浮点格式,#保证了即使后面没有任何数字,也打印一个小数点字符。对于%g或%G格式,可以防止结果后面的0被删除 |
0 | 对于数值格式,用前导0代替空格填充字段宽度。对于整数格式,若出现-标记或指定精度,则忽略该标记 |
/*printf()中的标记*\
修饰符 | 含义 |
标记 | (见上表) |
数字 | 最小字符宽度。如果该字段不能容纳待打印的数字或字符串,系统会使用更宽的字段 输出的值默认是右对齐,即输出内容前面会有空格。 |
.数字 | 精度。对于%e、%E和%f转换,表示小数点右边数字的位数。 对于%g和%G转换,表示有效数字最大位数。 对于%s转换,表示待打印字符的最大数量。 对于整形转换,表示待打印数字的最小位数。 若有必要,使用前导0来达到这个位数。即0.数字。 只使用 . 表示其后跟随一个0,所以%.f和%0.f相同。 |
h | 和整型转换说明一起使用,表示 short 或 unsigned short 类型的值。 |
hh | 和整型转换说明一起使用,表示 signed char 或 unsigned char 类型的值。 |
j | 和整型转换说明一起使用,表示 intmax_t 或 uintmax_t 类型的值。 |
l | 和整型转换说明一起使用,表示 long 或 unsigned long 类型的值。 |
ll | 和整型转换说明一起使用,表示 long long 或 unsigned long long 类型的值。 |
L | 和浮点转换说明一起使用,表示 long double 类型的值。 |
t | 和整型转换说明一起使用,表示 ptrdiff_t 类型的值。ptrdiff_t 是两个指针差值的类型。 |
z | 和整型转换说明一起使用,表示 size_t 类型的值。 |
/*printf()中的转换说明修饰符*\
4.scanf函数
C语言库包含了多个输入函数,其中scanf函数是最通用的一个,它可以读取不同格式的数据。
scanf函数用于读取用户的键盘输入。程序运行到这个语句时,会停下来,等待用户从键盘输入。 用户输入数据、按下回车键后,scanf就会处理用户的输入,将其存入变量。它的原型定义在头文件<stdio.h>。scanf的语法与printf类似,也使用格式字符串和参数列表。scanf中的格式字符串表明字符输入流的目标数据类型。两个函数的主要区别在参数列表中。printf使用变量、常量和表达式,而scanf使用指向变量的指针。
scanf("%d", &i);
它的第一个参数是一个格式字符串,里面会放置占位符(与printf的占位符基本一致),告诉编译器如何解读用户的输入,需要提取的数据是什么类型。因为C语言的数据都是有类型的,scanf必须提前知道用户输入的数据类型,才能处理数据。它的其余参数就是存放用户输入的变量,格式字符串里面有多少个占位符,就有多少个变量。 上面示例中,scanf的第一个参数%d,表示用户输入的应该是⼀个整数。%d就是⼀个占位符,%是占位符的标志,d表示整数。第二个参数&i表示,将用户从键盘输入的整数存入变量i。(注意:变量前面必须加上&运算符(指针变量除外),因为scanf传递的不是值,而是地址,即将变量i的地址指向用户输入的值。如果这里的变量是指针变量(比如字符串变量),那就不用加&运算符)
下列代码是是一次将键盘输入多个变量的例子
scanf("%d%f%c", &a,&b,&c);
第一个要输入的是整型,第二个要输入的是浮点型,第三个要输入的是字符型。要注意的是,输入的顺序不能改变,对应的占位符输入对应的数据类型,若随意改变数据,可能会造成程序出错。
scanf处理数值占位符时,会自动过滤空白字符:包括空格、制表符、换行符等。所以,在我们输入的数据之间,有⼀个或多个空格不影响scanf解读数据。另外,使用回车键,将输入分成几行,也不影响解读。
#include <stdio.h>
int main()
{
int x;
float y;
//用户输入" -13.45e12# 0"
scanf("%d", &x);
printf("%d\n", x);
scanf("%f", &y);
printf("%f\n", y);
return 0;
}
结果
上面示例中,scanf读取用户输入时,%d占位符会忽略起首的空格,从 - 处开始获取数据,读取到 -13停下来,因为后面的 . 不属于整数的有效字符。这就是说,占位符%d会读到-13。第二次调用scanf时,就会从上一次停止解读的地方,继续往下读取。这一次读取的首字符是 . 由于对应的占位符是%f,会读取到 .45e12,在前面我们提到过,这是采用e计数法的浮点数格式。后面的#不属于浮点数的有效字符,所以会停在这里。
有了上面这个示例,我们来为scanf读取输入作个总结。
假设scanf()根据一个%d读取一个整数。scanf()每次只读取一个字符,跳过所有的空白字符,直至遇到第一个非空白字符才开始读取。因为要读取整数,所以scanf()要至少识别一个数字符号或者符号。若识别到一个数字或符号,便将其保存下来,接着往后读取直至读取到非数字字符,那么这时scanf()就认为我们从键盘输入的这组数据已经到了终点,然后会将读取到的非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。最后scanf()计算已读取数字(可能会带有符号)相应的数值,并将值放入指定变量中。倘若scanf()放置占位符的部分中有其他符号如 - , 等在输入时也应在对应位置输入对应符号,否则scanf()在读取时会出现错误。
5.scanf()的返回值
scanf的返回值是一个整数,表示成功读取变量的个数。如果没有读取任何项或者匹配失败,则返回0。如果在成功读取任何数据之前,发生了读取错误或者遇到读取到文件结尾,则返回常量EOF。EOF是头文件<stdio.h>中定义的特殊值,通常默认为-1。
如下代码
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
float f = 0.0f;
int r = scanf("%d %d %f", &a, &b, &f);
printf("a=%d b=%d f=%f\n", a, b, f);
printf("r = %d\n", r);
return 0;
}
结果
我们从键盘分别输入1 2 3.14三个数,随后输出了a b f的值,最后输出了r的数值,而r的数值正是我们输入有效数据的次数。
如果输入2个数后,按 ctrl+z ,提前结束输入:
我们只输入了1 2两个数,并没有输入f的值,而r的值还是我们输入有效数据的次数。
同理,若不输入值,直接按三次 crtl+z,则:
我们没有输入数据,输入的次数就为0,scanf就直接读取到文件结尾,返回EOF(即-1)。
6.scanf中的转换说明(占位符)
占位符 | 含义 |
%c | 把输入解释成字符 |
%d | 把输入解释成有符号十进制整数 |
%e(E) %f(F) %g(G) %a(A) | 把输入解释成浮点数 |
%i | 把输入解释成有符号十进制整数 |
%o | 把输入解释成有符号八进制整数 |
%p | 把输入解释成指针 |
%s | 把输入解释成字符串。从第一个非空白字符开始到下一个空白字符之前的所有字符都是输入 |
%u | 把输入解释成无符号十进制整数 |
%x(X) | 把输入解释成有符号十六进制整数 |
%[ ] | 在⽅括号中指定一组匹配的字符(比如 %[0-9] ),遇到不在集合之中的字符,匹配将会 停⽌ |
/*scanf()中的转换说明*\
上面所有占位符中,除了%c以外,都会自动忽略起首的空白字符。%c不忽略空白字符,总是返回当前第一个字符,无论该字符是否为空格。若要强制跳过字符前的空白字符,可写成scanf(" %c", &ch) ,即%c前加上一个空格,表示跳过零个或多个空白字符。
下面要特别说明一下%s这个占位符。%s不会包含空白字符,所以无法用来读取多个单词,除非多个%s一起使用。这也意味着,scanf()不适合读取可能包含空格的字符串,比如书名或歌曲名。另外,scanf()遇到%s占位符,会在字符串变量末尾存储⼀个空字符\0。
scanf()将字符串读入字符数组时,不会检测字符串是否超过了数组长度。所以在储存字符串时, 很可能会超过数组的边界,导致预想不到的结果。为了防止这种情况,使用%s占位符时,应该指定读入字符串的最大长度,即写成%[m]s,其中m是一个整数,表示读取字符串的最大长度,后 面的字符将被丢弃。
7.scanf的转换说明修饰符
标记 | 含义 |
* | 抑制赋值。 |
数字 | 最大字段宽度。输入达到最大字符宽度处或第一次遇到空白字符时停止。 |
hh | 把整数作为 signed char 或 unsigned char 类型读取。 |
ll | 把整数作为 long long 或 unsigned long long 类型读取。 |
h、l或L | "%hd" 和 "%hi" 表明把对应的值存储为 short 类型 "%ho"、"%hx" 和 "%hu" 表明把对应的值存储为 unsigned short 类型 "%ld" 和 "%li" 表明把对应的值存储为 long 类型 "%lo"、"%lx" 和 "%lu" 表明把对应的值存储为 unsigned long 类型 "%le" 、"%lf" 和 "%lg" 表明把对应的值存储为 double 类型 在e、f和g前面使用L而不是l,表明把对应的值存储为 long double 类型。 若没有修饰符,d、i、o和x表明对应的值被存储为 int 类型,f和g表明把对应的值存储为 float 类型 |
j | 在整型转换说明后面时,表明使用 intmax_t 或 uintmax_t 类型 |
z | 在整型转换说明后面时,表明使用 sizeof 的返回类型 size_t |
t | 在整型转换说明后面时,表明使用表示两个指针差值的类型 |
/*scanf()中的转换说明修饰符*\
这里的赋值忽略符需要强调一下,有时我们输入的数据可能不符合scanf()预定的格式。例如
#include <stdio.h>
int main()
{
int year = 0;
int month = 0;
int day = 0;
scanf("%d-%d-%d", &year, &month, &day);
printf("%d %d %d\n", year, month, day);
return 0;
}
如果我们输入2020-01-01,就会正确解读出年月日。问题是我们有可能会输入其他格式,比如2020/01/01,这种情况下,scanf()解析数据就会失败。所以为了避免这种情况,scanf()提供了⼀个赋值忽略符 * 。只要把 * 加在任何占位符的百分号后面,该占位符就不会返回值,解析后将被丢弃。
#include <stdio.h>
int main()
{
int year = 0;
int month = 0;
int day = 0;
scanf("%d%*c%d%*c%d", &year, &month, &day);
return 0;
}
上面示例中,%*c就是在占位符的百分号后面,加入了赋值忽略符 * ,表示这个占位符没有对应的 变量,解读后不必返回。
以上就是本章的全部内容~由于是本人第一次写博客,有不足之处还恳请在评论区批评指正~