1-07基本数据类型
一、概述
C语言的基本数据类型(也叫内置数据类型)在日常编程中被频繁使用,本章我们主要简单地介绍下 C 语言的基本数据类型。
基本数据类型主要包括:整数类型,浮点数类型和字符类型,其中字符类型也可以看作是整数类型中的一种。
所以你可以认为C语言的基本数据类型只有两类:
- 整数类型
- 浮点数类型
二、sizeof运算符
sizeof运算符用于确定特定数据类型或对象的大小(以字节为单位)。使用sizeof可以帮助确保代码的可移植性,因为在不同的编译器或体系结构上,数据类型的大小可能会有所不同。
它大体上有三种使用场景:
-
类型名:你可以直接查询一个数据类型的大小。
sizeof(int);
-
变量名:你可以查询一个已定义的变量的大小,而不必显式指出它的类型。
sizeof(a); // a是某个任意类型的变量
-
**表达式:**当对表达式使用sizeof时,用于测量表达式结果类型的大小。此时表达式的结果具体是什么并不会被计算。
sizeof(2 + 3.5);
sizeof组成的表达式的结果是一个size_t类型,这是一个类型的别名,它定义了一个**“和平台以及编译器相关的”**无符号整数类型。
下面代码示例,演示了sizeof的一些使用:
代码块 1. sizeof代码示例
int i, j;
sizeof(10);
sizeof(i);
sizeof(i + j);
sizeof i
在一个典型的 32-bit 和 64-bit 的机器上,上面三个表达式的值都为 4,因为int被设计为占用4个字节内存空间。
值的注意的是,我们经常在VS的监视窗口使用sizeof运算符。
三、整数类型
整数类型又可以分为两大类:
- 有符号整数。默认情况下,C 语言的整数类型都是有符号的,即在不加任何修饰符的情况下就是一个有符号整数,但C语言也允许程序员带上**"signed"关键字**明确表示该整数类型是有符号的(但一般没有必要,也不建议这么做)。
- 无符号整数。若要声明为无符号整数,则需要明确添加**"unsigned"关键字**。
C语言(C99)目前允许的整数类型有以下:
代码块 2. C99提供的整数类型
short (int)
unsigned short (int)
int
unsigned (int)
long (int)
unsigned long (int)
long long (int)
unsigned long long (int)
C 语言整数类型的取值范围可能根据机器的不同而不同,但是有两条所有编译器都必须遵循的原则:
- 首先,C 标准规定了整数类型的最小字节长度:short(2)、 int(2)、long(4)、long long(8)
- 其次,C 标准规定了各个整数类型的字节长度满足下面的关系:short <= int <= long <= long long
下表是64位处理器,在Linux平台机器上,整数类型的常见取值范围:
表 1. 64位Linux平台机器上整数类型的取值范围
类型 | 字节长度 | 最小值 | 最大值 |
---|---|---|---|
short | 2 | -32768 | 32767 |
unsigned short | 2 | 0 | 65535 |
int | 4 | -2 147 483 648 | 2 147 483 647 |
unsigned int | 4 | 0 | 4 294 967 295 |
long | 8 | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
unsigned long | 8 | 0 | 18 446 744 073 709 551 615 |
long long | 8 | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
unsigned long long | 8 | 0 | 18 446 744 073 709 551 615 |
注意:
无符号数意味着最高位也充当数值位,所以它没有负数,但是表示的正数范围就更大了。
1. 整数字面值
C 语言允许使用十进制 (Decimal),八进制 (Octal) 或者十六进制 (Hex) 来书写整数字面值。
- 十进制字面值包含数字 0~9,但是不能以 0 开头。如:15, 255, 32767。
- 八进制字面值包含数字 0~7,而且必须以 0 开头。如:017, 0377, 077777。
- 十六进制字面值包含数字 0~9 和字母 af(或AF),而且总以 0x (或0X)开头。如:0xf, 0xff, 0x7fF。
注意:整数字面值也是有类型的!
代码中最常见的十进制整数字面值,默认情况下是int类型的,如果它超出了 int 的表示范围,那么它的类型是 long 和 long long 中能表示该字面值的 “最小” 类型。
对八进制和十六进制整数字面值来说,整数的字面值首先尝试被视为 int
类型,如果超出 int
范围,依次尝试 unsigned int
、long
、unsigned long
、long long
和 unsigned long long
,直到找到可以容纳该字面值的最小类型。
所以在有必要的情况下,我们可以通过一系列的操作,使得整数字面值改变它的默认类型。比如:
如果要指明某个整数字面值为一个 long 类型,只需要在字面值最后面加字母 L (禁止加小写的L):
15L, 0377L, 0x7fffL
如果要指明整数字面值是 long long 类型,我们需要在后面加 LL:
15LL, 0377LL, 0x7fffLL
如果要指明整数常量是无符号的,我们需要在后面加字母 U (或 u):
15U, 0377U, 0x7fffU
L、LL还可以和 U 结合使用,如:
0xffffffffUL, 0x12345678ULL
当然,这些字母组合时,先后顺序是没有影响的。
注意:尽管可以在程序中使用各种整数字面值,但为了代码的可读性,建议仅在需要时使用八进制和十六进制,并明确注释其用途。
2. 读/写整数
当你在C语言中使用scanf和printf函数来读写整数类型数据时,需要明确整数的类型和相应的转换说明书写格式。
关于转换说明,大家可以自行查看文档格式化输入和输出回顾。
其中四个基本的整数转换说明符:
- d:有符号十进制整数
- u:无符号十进制整数
- o:无符号的八进制整数
- x(X):无符号的十六进制整数
再组合上长度修饰符:
- h:表示short类型
- l:表示long类型
- ll:表示long long类型
代码示例
声明一个无符号的int整数变量,就可以用以下代码进行读写:
代码块 3. 读写整数代码示例1
unsigned int n;
scanf("%u", &n); // 读无符号十进制整数
printf("%u\n", n); // 打印无符号十进制整数
scanf("%o", &n); // 读无符号八进制整数
printf("%o\n", n); // 打印无符号八进制整数
scanf("%x", &n); // 读无符号十六进制整数
printf("%x\n", n); // 打印无符号十六进制整数
如果想要读写short类型,只需要在d、u、o、x说明符前加长度修饰符h:
代码块 4. 读写整数代码示例2
short n;
scanf("%hd", &n); // 读十进制short整数
printf("%hd\n", n); // 打印十进制short整数
如果想要读写long类型,只需要在d、u、o、x说明符前加长度修饰符l(若是long long类型,则加ll):
代码块 5. 读写整数代码示例3
long n;
scanf("%ld", &n); // 读十进制long类型整数
printf("%ld\n", n); // 打印十进制long整数
- 当使用
scanf
读取数据时,确保使用&
运算符取得变量的地址。 - 调用printf函数打印数据时,不要忘记用换行符"\n"刷新缓冲区。
3. 整数类型能够执行的操作
对于整数类型能够执行的操作,我相信大家都再熟悉不过了。它可以进行以下操作:
- 算术运算,比如加减乘除和取余。
- 比较运算,比如等于(==)、大于小于等。
- 赋值运算,比如赋值(=)、扩展赋值(+=、-=等)。
- 位运算,左移右移等。
其中,和同为数值类型的浮点类型相比,位运算是整数类型比较独特的操作,需要记住。
4. 整数类型编码 (拓展)
整数类型分为两类:
- 无符号整数
- 将所有的二进制位都作为数值位,在内存中它的编码表示就是这个整数的二进制表示,非常简单。
- 取值范围就是[ 0, 2该整数类型位长-1],比如unsigned short一般占2个字节,位长是16,它的取值范围就是[0, 65535]
- 有符号整数,关于有符号整数在内存中的存储表示,可以直接参考文档补充_有符号整数。这部分内容属于《计算机组成原理》的一部分,建议每一位C程序员都要掌握。
有符号整数规则的总结
如果你实在懒得看文档,那么也请至少记住以下结论,会非常有用:
- 原码
- 其有效数字是该数绝对值的二进制表示。
- 最高位是符号位,如果是负数符号位是1,正数符号位是0。
- 有效数值和最高位之间如果有空隙,加0。
- 最高位是符号位,其它位都是数值位。
- 反码
- 正数的反码与原码相同。
- 负数的反码是对其原码逐位取反,但符号位除外。
- 补码
- 正数的补码与原码相同。
- 负数的补码是在其反码的基础上再加1。
计算机当中存储整数都是以补码的形式存储的,目的是为了统一加减法为加法,降低CPU设计的复杂性,提高性能。
补码有以下两个重要的特性(假设x是一个n位整数):
- x + (-x) = 1, 000…02进制补码(其中0一共有n个,高位的1溢出被舍掉) = 0
- x + (~x) = 111…12进制补码(其中1一共有n个) = -1
一道简单的思考题:
对于一个有符号整数10110101补,求它的相反数的二进制表示(求补码)。
四、浮点数类型
C 语言提供了三种浮点数类型,对应三种不同的浮点数格式:
- float
- double
- long double
这三种浮点型如何选择呢?
- 当对精度要求不高时 (比如只有一位小数的运算时),我们可以使用 float 类型。它最节省空间。
- 大多数情况下,我们都会使用 double 类型;
- 在极少数对精度要求非常高的情况下,才会使用 long double。
1. 浮点数的特点
C 语言标准并没有明确说明 float、 double、long double 类型究竟采用什么方式进行存储,不同的平台确实可以使用不同的方式存储浮点数。
但是现代计算机一般都采用IEEE 754标准作为浮点数的存储规范,也就是说 IEEE 754 是事实上的标准,C语言中的浮点数也不例外。
那么什么是IEEE754标准呢?
限于课程的进度,课堂上就无法给大家详细讲解了,请自行参考文档补充_IEEE754标准了解学习。IEEE754标准浮点数也是《计算机组成原理》的基础内容,你可以通过它稍微领略一下计算机组成原理的魅力。
当然,不管你是否熟知这个标准,我们都要给大家简要介绍一下IEEE754标准浮点数的特点:
-
浮点数本质上是二进制的科学计数法表示,所以浮点数可以用较小的存储空间,表示非常大和非常小的数。
很简单的道理,比如:1 * 10-100 这个数就非常小,1 * 10100这个数就非常大。
-
浮点数的精度是有限的。浮点数都只能保证在一个有限的有效数字内,浮点数是比较准确的。
下面的表格就描述了遵循 IEEE 标准的浮点数的特征:
表 2. IEEE 标准的浮点数特征表
类型 | 所占空间(一般情况) | 大致取值范围 | 精度 |
---|---|---|---|
float | 4个字节 | 大约 ±3.403E38 | 基本保证6位有效数字精确 |
double | 8个字节 | 大约 ±1.798E308 | 基本保证15位有效数字精确 |
注:
- 表格中给出的大致取值范围是非常不准确的,浮点数存在规格化和非规格化的区别,所以它们的取值范围是不能用连续区间表示的,但为了简单起见就直接这么写了。
- long double 类型没有显示在此表中,因为它的长度可能随机器的不同而变化,最常见的是 80 位和 128 位。它往往取值范围比
double
类型更广,精度也更高。但某些平台也存在long double等同于double的情况。
2. 浮点数的不精确是绝对的
注意:浮点数的不精确是绝对的!!!
有些同学看到浮点数有精度限制,比如float可以**“基本保证6位有效数字精确”**,那么是不是我只要在6个有效数字内使用float就能保证数据准确呢?
答案当然不是,浮点数的不精确是绝对的,即便是在有效数字内。
这主要是因为某些十进制小数,转换成浮点数用二进制存储,会出现无限循环的情况,此时有限位数的浮点数必然是无法精确表示无限循环小数的!
比如十进制小数0.1
转换成二进制表示:
十进制正小数(0.开头)转换成二进制,先用小数部分乘以2,取结果的整数部分(必然是1或者0):
- 然后小数部分继续乘2
- 直到小数部分为0,或者已经达到了最大的位数
- 最终的结果(0.开头)正序排列
0.1 * 2 = 0.2 —> 取整数部分0
0.2 * 2 = 0.4 —> 取整数部分0
0.4 * 2 = 0.8 —> 取整数部分0
0.8 * 2 = 1.6 —> 取整数部分1
0.6 * 2 = 1.2 —> 取整数部分1
0.2 * 2 = 0.4 —> 取整数部分0
0.4 * 2 = 0.8 —> 取整数部分0
0.8 * 2 = 1.6 —> 取整数部分1
…
所以十进制小数0.1转换成二进制小数是:0.000110011001100…(1100四位无限循环)
于是我们可以得出结论:
很多十进制小数转换成二进制时,会存在循环小数的情况。那么有限有效数字的浮点数就不能准确表示这个数字了,那些超出表示位数的数据就被截断丢失了,数据失真,精度丢失。这种小数并不少见,所以浮点数的不精确是绝对的。
总之:
浮点数执行的浮点数运算,只是在广泛的数字范围上较为精确而快速的近似运算,当你选择使用浮点数进行运算时,那就意味着数据准确对你而言已经不重要了,也不可能了。
所以不要尝试使用浮点数进行精确的小数运算,不过好在C语言也极少在需求精确小数运算的场景中使用。
3. 几道浮点数精度问题的练习题
读下列代码,思考它们的输出是什么以及原因。
代码块 6. 浮点数练习题1
// 定义不同类型的浮点数
float a = 0.1f;
double b = 0.1;
long double c = 0.1L;
// 打印这些值,但保留更多的位数
printf("float: %.20f\n", a);
printf("double: %.20lf\n", b);
printf("long double: %.20Lf\n", c);
代码块 7. 浮点数练习题2
float a = 0.1f;
double b = 0.1;
printf("%d\n", a == b);
float c = 0.1F;
// 中间有10个0
float d = 0.100000000001F;
printf("%d\n", c == d);
4. 浮点数字面值
在C语言中,浮点数字面值常量的书写方式比较灵活,有多种方式。例如,下面都是 57.0 的有效表示方式:
57.0
57.0e0
57E0
5.7e1
5.7e+1
.57e2
570.e-1
也就是说,在C语言中,浮点数的字面值常量必须包含:
- 小数点
- (或者)指数,即E或者e,表示乘以10的xx次方。
默认情况下,浮点数字面值常量都是 double 类型。如果想要改变类型:
- 如果需要表明以单精度方式存储,可以在末尾加字母 F 或 f。如
57.0F
。- 如果需要以 long double 方式存储,则在后面加 L 或 l(最好不要用小写L),如
57.0L
。
5. 读/写浮点数
在前面,我们已经知道可以使用转换说明符 %f 来读写 float 类型的数据。读写 double 和 long double 类型所需的说明符与 float 略有不同。参考下面的代码:
读写 double 类型的值时,需要在 f 前面添加字母 l。
代码块 8. 读写double类型浮点数
double d;
scanf("%lf", &d);
printf("%lf", d);
代码块 9. 读写long double类型浮点数
long double ld;
scanf("%Lf", &ld);
printf("%Lf", ld);
6. 浮点数类型能够执行的操作
浮点数类型能够执行的操作大体上和整数类型是一样的,只不过少了一个位运算。
五、字符类型
所谓字符,指的是像字母,汉字、标点等类似的符号。那么字符型,就是用来存储表示单个字符的数据类型。
很明显,计算机只能存储二进制数据,不可能也没能力直接存储一个符号(字符)。
计算机中存储字符,实际是通过存储一个整数值来映射对应的字符。所以编程语言中的字符类型普遍都可以作为一个整数类型对待。
比如:
现在我规定一个整数值97对应字符"a",那么计算机中存储整数97,在字符类型中就表示存储字符"a"。
于是我们就有了以下概念和结论:
- 一个用来映射对应字符的整数值,称之为该字符的**“编码值”**。
- 如果用一张表格来存储所有字符和编码值的映射关系,这就是编码表。
所以我们了解一门编程语言的字符类型,最重要的就是弄清楚它所基于的编码表。
1. C语言的字符类型
C语言的字符类型,和大多数编程语言的字符类型一致,都是用于存储单个字符。
在绝大多数机器平台下,char只占用1个字节的内存空间,和整数类型一样,它同样有无符号和有符号的区别:
- 无符号char类型(unsigned char),它可以表示存储编码值范围在[0, 255]内的字符。同时你可以把它当成此范围内的一个整数类型。
- 有符号char类型(signed char),它可以表示存储编码值范围在[-128, 127]内的字符。同时你可以把它当成此范围内的一个整数类型。
注意:
不同平台,直接写char作为类型的默认有无符号是不同的,所以为了跨平台考虑,严谨的C语言代码应该使用unsigned char
或signed char
来表示char类型。当然一般的代码是不用如此的,现代多数编译器平台的char类型默认是有符号的。
知道了上述概念后,那么C语言的char类型的编码集是什么呢?
很可惜,C标准也没有规定这一点,不同平台可以选择不同的编码表。但是作为世界上最早和使用最广泛的编码表——ASCII码表,C语言的char类型总是兼容它的。
ASCII码表中的字符需要1个字节来存储,但只使用7位,最高位事实上是空的,所以它一共可以存储128个字符,编码值在[0, 127]范围内。
关于ASCII码表,可以参考本小节的末尾附录: ASCII码表。
大体上我们记住以下几个特殊的字符,在ASCII码表中的编码值即可:
‘’ = 0 ----- 编码值为0的字符是一个绝对空字符
’ ’ = 32 ----- 空格字符编码值为32
‘0’ = 48 ----- 数字0的编码值是48
‘A’ = 65 ------ 字母A的编码值是65
‘a’ = 97 ------ 字母a的编码值是97
由于C语言自身char类型设计的局限性,尽量不要使用char类型去表示和操作非ASCII码表中的字符,这会带来很多麻烦。
2. 字符字面量
C语言中的字符字面量非常简单,在代码中用单引号’'引起来的字符就是一个字符字面值常量。比如:
代码块 10. 字符字面量-演示代码
char ch;
ch = 'a';
ch = 'A';
ch = '0';
ch = ' ';
除此之外,像转义序列比如’\n’也被视为字符字面量的一种,下面我们一起来学习一下C语言中的转义序列。
3. 转义序列
什么是转义序列?
- 转义序列,也叫转义字符,就是用反斜杠(\)加上一个特殊的字符(串)用来表示一个全新的字符。
- 因为这个全新的字符已经和原先字符(串)的意思大不相同,所以整体称之为转义序列。
转义序列有啥用呢?
- 实际上转义序列可以用来表示任何一个普通字符,但普通的字符直接书写给出即可,使用转义序列的意义不大。
- **转移序列最重要的用途是,表示那些无法用键盘直接输入的控制字符。**比如在ASCII码表中,那些换行,翻页等等字符。
下面表格罗列了C语言常见的一些转义序列及其含义:
表 3. C语言常见转义序列及其含义
作用 | 转义序列 | 作用 | 转义序列 |
---|---|---|---|
响铃 | \a | 垂直制表符 | \v |
退格 | \b | 反斜杠 | \ |
换页 | \f | 问号 | ? |
换行 | \n | 单引号 | ’ |
回车 | \r | 双引号 | " |
水平制表符 | \t |
大多数同学可能都会好奇一个问题:回车(‘\r’)和换行(‘\n’)不是一回事吗?
确实,它们确实不是一回事:
- 转义字符中的回车,含义来源于早期的打字机设备,它表示将打印头(光标)移动到行首,它和换行没什么关系。
- 转义字符中的换行表示结束某一行,转而从下一行的开头开始。
这里需要注意的一个细节就是,不同平台的换行符的不同,带来的文本编辑的跨平台问题。比如:
- Windows:文本编辑中使用"\r\n"作为换行符
- Linux/Mac平台:文本编辑中使用"\n"作为换行符
后续我们讲Linux还会给大家重点强调这一点。
除了上述转义序列,我们还可以使用以下两种方式,自由的表示不同编码值的字符:
- 八进制转义序列,即"\ooo",其中"ooo"是一个三位八进制数。
- "ooo"默认即是八进制数,不需要以0开头。
- 由于char类型的限制,‘\ooo’最大取’\377’,而不是’\777’。
- 也就是说,这种方式可以表示编码值在[0, 255]范围内的字符。
- 十六进制转义序列,即"\xhh…“,其中”\x"是固定形式,"hh…"是一个十六进制数,不限制长度,但常用的是两位十六进制数。
- "hh…"取两位时,也就是’\FF’,这种方式同样可以表示编码值在[0, 255]范围内的字符。
- 虽然理论上不限制长度,但一般不建议超出两位,因为它的结果是不可预测的。
参考下列代码示例:
代码块 11. 转义序列-演示代码
printf("\x61\n"); // 十六进制61也就是十进制97, 此转义序列表示字符a
printf("\141\n"); // 八进制141也就是十进制97, 此转义序列表示字符a
// 转义序列可以整体作为一个字符字面值
printf("%c\n", '\x61');
这种方式总体不常用,了解即可。
4. 字符类型能够执行的操作
C语言的char类型在绝大多数时候,可以直接作为整数类型进行处理。(当然,下面我们会讲不能作为整数处理的情况)
比如:
直接将char类型当成整数类型进行赋值和算术运算:
代码块 12. char类型操作-演示代码1
char ch;
int i;
i = 'a'; /* int i的取值是97 */
ch = 65; /* ch表示存储编码值为65的字符,也就是A */
ch = ch + 1; /* 编码值+1,此时ch表示字符B */
ch++; /* 编码值++,此时ch表示字符C */
// 遍历所有大写字母
for (ch = 'A'; ch <= 'Z'; ch++) {
// ....
}
代码块 13.
char c1 = 'A', c2 = 'B';
printf("%d\n", c2 > c1); // 结果是1,说明c2大于c1,也就是字符B大于A
这种设定,使得排在ASCII码表后面的字符就越大,因为编码值越大。这种排序的规则,被称为**“字符的字典顺序”**,这是一个非常有用的概念。(后面课程会用到)
当然诸如位运算这样整数可以进行的运算,char类型也可以执行。
最后,我们还是要注意:
虽然字符可以直接作为整数处理,但还是尽量在处理字符时使用char类型,而不是滥用char类型,以至于写出一些很奇葩的代码:
'a' * 'b' / 'c'; /* 这个表达式的结果是96,但明显过于奇葩,不要这么用 */
下面,我们将介绍char类型作为字符类型,可以进行的一些独特的操作。
1. 字符处理函数
现在我提出一个需求:
给你一个字符(任意字符),在确定它是小写字母的前提下,将它转换成对应大写字母。
怎么做呢?
利用char类型本质存储编码值的特点,你可能会想到下面的实现:
代码块 14. 手动实现将小写字母转换成大写-参考代码
char ch = 'a';
if (ch >= 'a' && ch <= 'z'){
// 是小写字母
ch = ch - 'a' + 'A';
}
这个实现是OK的,但实际上并没有必要。因为C语言标准库,提高了一系列字符处理函数,其中就包括此功能。直到调用标准库函数实现功能,不仅代码更简洁,可读性也会更好。
下面我们来看一下常用的字符处理函数。
首先,使用字符处理函数,需要包含头文件<ctype.h> 。
这些字符处理函数,又可以分为两大类:
- 检查字符类型函数
- 字符大小写转换函数
这些函数的声明如下:
代码块 15. 常用字符处理函数的声明
// 检查字符类型函数
int isalnum(int c); /* Is c alphanumeric? 检查字符c是否是一个字母或数字 */
int isalpha(int c); /* Is c alphabetic? 检查字符c是否是一个字母 */
int isblank(int c); /* 检查字符c是否是一个空白字符(只包括空格、制表)*/
int isspace(int c); /* 检查字符c是否是一个空白字符(包括空格、制表、换行、换页等)*/
int isdigit(int c); /* Is c a decimal digit? 检查字符c是否是一个十进制整数 */
int islower(int c); /* Is c a lower-case letter? 检查字符c是否是一个小写字母 */
int isupper(int c); /* Is c an upper-case letter? 检查字符c是否是一个大写字母 */
int ispunct(int c); /* Is c punctuation? 检查字符c是否是一个标点符号 */
int isxdigit(int c); /* Is c a hexadecimal digit? 检查字符c是否是一个十六进制数 */
// 字符大小写转换函数
int tolower(int c); // 如果字符 c 是大写字母,则转换为对应的小写字母;否则,返回原字符。
int toupper(int c); // 如果字符 c 是小写字母,则转换为对应的大写字母;否则,返回原字符。
这些函数都不要死记硬背,你只需记住C语言的标准库提供了这些功能就可以了。
到了实际需要用的时候,如果记不起具体的函数名和调用方式,也没有关系。只要你还大体上记得有这个函数能实现xx功能,就可以通过查询的方式很快找到并使用它。
这里推荐一个比较权威的,查询C语言标准语法、库的网站:C++ Reference
2. 读/写字符
和整数类型一样,你也可以选择使用 scanf 和 printf 读/写字符数据,使用的转换说明是"%c"。
例如以下代码:
代码块 16. scanf 和 printf 读/写字符数据-参考代码
char ch;
scanf("%c", &ch);
printf("%c", ch);
但需要注意的是:
scanf
函数在读字符时,不会跳过前面的空白字符。如果需要跳过前面的空白字符,则要在转换说明符 %c 前面加一个空格:
scanf(" %c", &ch);
scanf
格式串中的空格意味着"跳过零个或着多个空白字符"。
总体上而言,对char类型字符数据进行读/写,我们更推荐使用以下两个标准库函数。即getchar和putchar函数!
首先,这两个库函数的使用需要先包含头文件<stdio.h>,它们都是用于处理单个字符操作的函数。
putchar
函数用于将单个字符打印到显示器上,其函数的声明如下:
int putchar(int c);
其作用是向标准输出缓冲区(stdout)写入一个字符,并且直接返回该字符作为返回值,如果写入过程发生意外错误,该函数会返回EOF(End of File)。
这就意味着此字符会暂存缓冲区,直到缓冲区满、遇到换行符或者程序结束等场景时,缓冲区刷新,内容才会显示在屏幕上。
一个putchar函数调用的示例如下:
代码块 17. putchar函数调用-演示代码
char ch = 'A';
putchar(ch); // 将字符A写入到标准输出缓冲区中
putchar('\n'); // 换行,刷新缓冲区
此函数调用不需要任何参数,它会从**标准输入缓冲区(stdin)**中读取一个字符,并且把读取到的字符作为返回值返回。如果已经读取到流(数据源)的末尾或者发生意外错误,此函数会返回EOF。
一个getchar函数调用的示例如下:
代码块 18. getchar函数调用-演示代码
int ch; // 注意这里是int类型,以便可以存储EOF
printf("Enter a character: ");
ch = getchar(); // 读取一个字符
printf("You entered: ");
putchar(ch);
putchar('\n');
注意:
- getchar函数在读字符时,仍然不会自动跳过空白字符。
- putchar 和 getchar 函数是针对char类型字符处理专门优化的函数,它们的效率要高于 printf 和 scanf,处理char数据请优先使用它们。
一个重要的惯用法
在编程中,惯用法是指一种常用的编码模式或表达方式,类似汉语中的成语,是程序员前辈们的智慧结晶。惯用法往往代表了处理某种场景的最佳手段。
在C语言中,我们经常需要从流中读取数据,比如getchar函数就是从标准输入流(stdin, 缓冲区)中读取数据,为了确保将流中的数据读完,我们经常会使用以下惯用法:
代码块 19. 循环读取流中数据-惯用法
int ch; // 使用int来存储getchar的返回值,因为它可能返回EOF
int count = 0; // 统计总字符数
while ((ch = getchar()) != '\n') { // 读取到流中的换行符就结束循环
count++;
printf("第%d次读到的字符是%c\n", count, ch);
}
printf("输入的字符一共有%d个\n", count);
注意:
- EOF在大多数平台下实际上就等于-1,但它并不是任何字符,只是一个表示流已经读到末尾的标记、特殊值。所以不应该用char类型作为getchar()函数的返回值,一般我们习惯使用int类型作为函数返回值。
ch = getchar()
赋值运算符组成的赋值表达式是有值的,就是"="右边的表达式的取值,也就是getchar()函数的返回值。- 在读文件流时,只需要把"!="的条件从换行符改成EOF,就是一个通过循环将文件数据读取完的惯用法。(后面文件流会讲)
实际上,不仅是C语言,C++和Java也有几乎一模一样的惯用法,只不过是结束读取流的条件不同。
3. 课堂练习题
编写一个程序实现以下需求:
- 用户键盘录入一整行字符数据,包含数字和字母。
- 当用户按下回车键后结束数据录入。
- 请统计用户输入的一整行字符数据中,数字和字母分别有多少个。
这个题目就用到了我们上面提到的惯用法,也会用到两个上面提到的字符处理函数。
参考代码如下:
代码块 20. char类型课堂练习-参考代码
int ch;
int letters = 0; // 字母的数量
int nums = 0; // 数字的数量
printf("请输入一行字符,包含数字和字母,按回车结束:\n");
while ((ch = getchar()) != '\n') {
if (isalpha(ch)) { // 检查是否为字母
letters++;
}else if (isdigit(ch)) { // 检查是否为数字
nums++;
}
}
printf("字母的数量: %d\n", letters);
printf("数字的数量: %d\n", nums);
以上。
六、C语言的数据类型转换
C语言的不同类型之间是允许进行数据类型转换的,这些类型转换可以分为两大类:
- 隐式类型转换(自动类型转换):由编译器自动处理和完成类型转换,程序员无需进行任何额外操作,类型转换自动完成。
- 强制类型转换(显式类型转换):由C程序员通过强制类型转换的语法,手动实现类型的转换。
下面我们先讨论隐式类型转换,再讨论强制类型转换。
1. 隐式类型转换
在C语言代码中,隐式类型转换主要发生在以下场景中:
1.赋值运算中的隐式类型转换
当给变量赋值时,如果赋值表达式右边值的类型和左边变量的类型不匹配,则赋值表达式右边值的类型会被隐式转换为左边变量的类型。
参考下列代码:
代码块 21. 赋值运算中的隐式类型转换-参考代码
int a = 10.1; // 隐式类型转换,数据截断
char b = 97.9f; // 隐式类型转换,数据截断
double c = 0.1;
float d = c; // 隐式类型转换,数据精度丢失
printf("%d\n", a);
printf("%c\n", b);
printf("%.20lf\n", c);
printf("%.20f\n", d);
由于C语言在赋值过程中的这种隐式类型转换机制,所以在赋值时要避免表达式右值超出左边类型的取值范围,否则你将会得到一个你也搞不清楚取值是什么的变量。
代码块 22. 赋值运算中的隐式类型转换-参考代码2
char a;
int b;
float c;
a = 100000; // WRONG 超出char范围,隐式转换引发未定义行为
b = 1.0E20; // WRONG 超出int范围,隐式转换引发未定义行为
c = 1.0E100; // WRONG 超出float范围,隐式转换引发未定义行为
将浮点常量赋值给 float 类型变量时,一个好的习惯是在常量后面加字符 f。如:f = 3.14f
。如果没有后缀 f,那么字面值常量 3.14 是 double 类型,会引发隐式转换。
2.函数相关的隐式类型转换
在使用函数时,也有两个比较常见的隐式类型转换过程:
- 当函数调用时实参类型与形参类型不匹配,传递的实参会被隐式转换为形参类型。
- 在使用return表示返回值时,如果return后面的值类型和返回值类型不匹配,也会进行隐式类型转换。
参考下列代码:
代码块 23. 函数相关的隐式类型转换-参考代码
void fun(int a) {
printf("函数调用传入的参数a是:%d\n", a);
}
float fun2(void) {
return 0.1; // 隐式类型转换,精度丢失。double --> float
}
int main(void) {
fun(10.5); // 函数的形参类型是int,传入浮点数发生隐式类型转换,导致数据截断
printf("%.20f\n", fun2());
return 0;
}
实际上,最常见的还是第三种情况:
当不同类型的参数共同组成一个表达式时,为确保表达式最终结果的类型是唯一的,编译器会自动进行隐式类型转换。
这种隐式类型转换又可以细分为两种情况:
- 整数提升
- 常用算术转换
下面分别讲一下这两种情况。
1. 转换等级(重要)
为了更好的描述转换的规则,C99语言标准给C语言的基本数值类型都规定了一个转换等级,它从高到低依次为:
- long double
- double
- float
- long long、unsigned long long
- long、unsigned long
- int、unsigned
- short、unsigned short
- char、signed char、unsigned char
实际上这个等级,就是按照表示范围和精度从大到小将所有的数值类型排序。
2. 整数提升
**当表达式中仅有int以及int以下等级的类型参与时,表达式的结果一律是int(或者unsigned int)。**比如:
- char + char = int
- short + short = int
- char + short = int
- …
这是因为在C语言的设计中,转换等级小于int的类型参与表达式运算时,先将它们提升到int(或unsigned int),这种语法设计就叫做"整数提升"。
这么设计的目的也很简单:
因为short和char在参与算术运算时,它们的取值范围都太小了,如果char + char = char
成立,那么会经常发生溢出,而溢出往往导致数据失真。
3. 常用算术转换
当多个不同数据类型的操作数共同组成一个算术运算表达式时,表达式的最终结果遵循以下规则:
- 如果操作数中有一个是
long double
类型,其余操作数都会被隐式转换为long double
。 - 否则,如果有一个是
double
类型,其余操作数都会被隐式转换为double
。 - 否则,如果有一个是
float
类型,其余操作数都会被隐式转换为float
。 - 否则,如果有一个是
long long
类型,其余操作数都会被隐式转换为long long
。 - 否则,如果有一个是
long
类型,其余操作数都会被隐式转换为long
。 - 如果以上类型在表达式中都没有出现,则遵循"整数提升"原则。也就是说结果是int或unsigned int。
这个规则看起来很复杂,其实一句话就总结了:
表达式的最终结果类型是,表达式中取值范围和精度最大的那个操作数的类型。
这种语法设计就叫做"常用算术转换"。
4. 注意事项(重要)
在讲上述表达式相关的隐式类型转换时,我们刻意避开了无符号的整数类型。
实际上C语言有以下明确的语法规定:
同一转换等级的有符号整数和无符号整数一起参与运算时,有符号整数会转换成对应的无符号整数。
也就是说:
unsigned + int = unsigned
unsigned long + long = unsigned long
…
但无符号数在C语言其实是非常麻烦和坑爹的,我们来看两个例子:
无符号整数会带来转换等级的复杂性
你认为:
unsigned + long 结果是什么类型啊?
你可能觉得long类型等级更高,结果应该是long类型。但实际上在VS的MSVC编译器下这个结果类型是unsigned long。
这是因为在VS的MSVC编译器平台下,int和long类型的大小是一致的,都是4个字节。
因此unsigned long被视为转换等级的最高级别,所以结果就是unsigned long。
这说明由于无符号整数的参与,转换等级变得需要考虑平台和编译器等额外因素了,这无疑增加了编程的复杂性。
当然,这个例子最多导致一些类型的误判,不算太坑。但下面的例子就是一个纯粹的**“陷阱”**了。
无符号数参与的类型转换将导致直觉上的逻辑错误
请看以下代码:
代码块 24. 无符号类型转换引发直接逻辑错误-演示代码
int a = -10;
unsigned b = 100;
if (b > a){
printf("b > a is true.\n");
}else{
printf("b > a is false.\n");
}
printf("%u\n", (a + b));
请问程序的两行输出都是什么呢?
答:
- b > a is false.
- 90
这是因为**(为了方便描述,设32位无符号数系统的"模"为N,N = 32位无符号数的最大值 + 1, 即N = 232)**:
-
当表达式中同时存在有符号数和无符号数时,有符号数会被隐式转换为无符号数。在这个例子中,
a
是一个有符号整数值为 -10,这个数的补码形式是11111111 11111111 11111111 11110110
。当将这个补码解释为无符号整数时,它代表的是一个非常大的正数,具体来说,是N - 10( 即2^32 - 10,实际上这个数是最大值 - 9)。注:N - 10是怎么来的呢?
N是模且系统中没有负数,于是-10就可以转换成模相关的一个加法从而去掉负号,-10在这个系统中就等价于 +(N - 10),于是-10在此系统中变成了一个正数。
-
明白上面代码的原因后,那么a + b = 90,应该也是非常容易理解的了:
- a转换成无符号数结果是N - 10,那么再加上100,结果就是N - 10 + 100 = N + 90
- 但N是系统的模,任何达到或超出模的数值,都会从0开始计算。
- 所以N + 90,结果就是90
总之,由于无符号数的类型转换,给程序员带来了极大的麻烦,这是C语言的一个常见"设计陷阱"。
针对无符号数的类型转换,我们给出以下建议:
- 在一般的应用级C程序开发中,无符号整数是用不到的,往往只有在底层开发中才会使用它。
- 鉴于无符号整数带来的一系列复杂性和坑爹的陷阱,请大家不要在日常代码中使用无符号数。
- 假如你有一天能接触到底层开发,有使用无符号整数的需求,也建议不要混合使用无符号整数和有符号整数。
2. 强制类型转换
C语言的隐式类型转换强大且灵活,在正确使用的情况下是非常方便的。但为了更加主动的控制类型转换,在很多时候,我们还需要进行手动的强制类型转换。
强制类型转换的语法非常简单,如下:
(type_name) expression // type代表想要强转成的类型
你只需要把需要进行强转的表达式(或值)前面加上"(type_name)"即可。
在C语言中,"(type_name)"被视为一元运算符,而一元运算符的优先级是高于二元运算符的。(这意味着强转在一个表达式中往往会优先运算,这是一个非常重要的设计)
搞清楚强转的语法后,下面只要搞清楚它有啥用,或者说在什么场景下会用到它,就算是学会这个语法了。
强转在以下场景中比较常用:
-
在发生隐式类型转换的地方使用强制类型转换语法,显式地表明将要发生类型转换。如下列代码:
代码块 25. 强制类型转换使用场景-演示1
int a; float (int)b = a; // a会隐式类型转换成int,但这里用强转显式标注转换
之所以这么做,目的是为了显式标注,提高代码的可读性。(而且隐式转换是有可能出现错误了,显式标注出来可以让程序员更容易注意到代码中的问题)
-
提升表达式结果的精度。一个非常典型的例子就是:一个表达式因所有操作数都是整数,结果会得到一个截断数据的整数,如两个整数相除。在这种情况下就可以使用强转来提升表达式结果的精度,如下列代码:
代码块 26. 强制类型转换使用场景-演示2
float result, result2; int a = 10; int b = 4; result = a / b; result2 = (float)a / b; // (float)是一元运算符优先级高于/除号
小数取整。这个比较简单,参考代码如下:
代码块 27. 强制类型转换使用场景-演示3
float a = 7.1f; int num = (int)a; // 当然这里本身就会隐式类型转换,但强转标注出来会具有更好的可读性
-
计算浮点数的小数部分。参考代码如下:
代码块 28. 强制类型转换使用场景-演示4
float f = 3.14159, fractional_part; fractional_part = f - (int)f;
-
强转类型转换还经常用于避免数据溢出。比如下列代码:
代码块 29.
// 此表达式右值的结果在int范围内不会溢出,所以这里隐式转换就可以了 long long millisPerDay = 24 * 60 * 60 * 1000; // 此表达式右值的结果超出int范围溢出,这里如果只隐式转换就会数据失真,所以需要强转 long long nanosPerDay = (long long)24 * 60 * 60 * 1000 * 1000 * 1000;
**C语言的强制类型转换语法非常灵活,理论上允许程序员将变量从一种类型转换为任意的另一种类型。**但这种灵活的语法应当谨慎使用,多多思考,避免无意义或不安全的强转导致数据错误或者未定义行为。
七、给类型定义别名
给类型定义别名,是C语言中极其常用和重要的语法。我们可以使用typedef
关键字用于为现有的数据类型创建新的名称,即类型别名。
它的语法格式如下所示**(定义别名语句一般和预处理指令放在一起,放在文件的开头)**:
typedef 现有类型的名字 别名;
现在解释一下这个语法:
typedef
是一个关键字,读作"type define",表示定义类型别名。- 现有类型可以是C语言的基本数据类型,也可以是复合类型如结构体等。
- 关于别名的命名规范,首先C语言没有强制的规范去约束别名的命名风格,但基于编程实践,给出以下几点要求:
- 别名应该是在原有类型名的基础上,更明确更清晰的表明类型作用的,应该具有好的可读性。“只有起错的名字,没有起错的外号。”
- 为了区分类型名和别名,建议别名和类型名采用不同的命名风格。比如类型名采用"下划线式命名风格",那么别名可以采用"驼峰命名风格"。
- 在C语言标准库中,常用"_t"作为后缀结尾表明此类型名是一个别名。常见的如:
size_t
、int32_t
等。我们也可以模仿采用这种风格,当然关于命名,一切以公司的要求为最高要求。
给某个类型起别名后,就可以在后续的代码中使用这个别名代替原有的类型名,在具体使用时,别名和原类型名完全等价。如下列代码:
代码块 30. 给类型定义别名-演示代码
typedef int E;
// 等价于int作为返回值
E test(void) {}
int main(void) {
E a = 10; // 等价于 int a = 10;
return 0;
}
以上。
为什么要给类型定义别名/定义别名的好处是什么
搞清楚定义别名的语法是非常容易的,对于这个语法的学习,我们重点应该是理解:为什么要给类型定义别名/定义别名的好处是什么?
其实不外乎以下三个优点:
- 提升代码的可读性。这个很容易理解,不多赘述。原类型名往往是一个通用的称呼,而别名是此场景下的一个精准描述。
- 提升代码的扩展性。这一点在后续数据结构阶段会体现的很明显,在后续课程我们将展开讲解这部分内容。
- 提升代码的跨平台性移植性。类型别名的语法最重要的用途就是增强代码的跨平台移植性,下面将详细讲一个作用。
类型别名如何提升跨平台性移植性?
我们都知道,C语言由于编译器、平台之间的差异,经常会出现同一个类型,但存储方式不同的情况。比如:
- int类型在绝大多数现代桌面平台下,占用4个字节32位内存空间。大多数应用级C程序员接触的int类型,也是4个字节的int类型。
- 但是在某些小型机器或者嵌入式平台下,int类型可能就会变成占用2个字节16位内存空间的整数类型。(因为要节省内存空间)
于是代码在跨平台移植时,就会出现以下问题:
int a = 100000;
这句代码在32位存储int时没有问题,但如果int变为16位存储,就会出现数据溢出失真的问题。
那如何避免这种情况呢?
只需要在移植代码后,使用一个32位的整数类型来存储这个a就可以了。那么具体可以用以下两种方式实现:
- 直接把a声明为更大的整数类型(比如long),这样大家都能装得下了,移植时就避免了溢出情况。
- 为每一个平台选择最合适的类型。原平台继续用int,新平台使用更大的,比如long类型。
很显然,方式一会带来空间的浪费,性能下降,方式2是更好的选择。
那么如何实现方式二呢?
很简单,使用类型别名。
我们可以在原平台上,将int类型定义别名为BigInteger:
typedef int BigInteger;
在移植后,不需要改变任何其它代码,只需要改变这个别名定义中的int为一个合适的类型即可,比如:
typedef long BigInteger;
这样就可以在几乎不用修改任何额外代码的前提下,将整个代码移植到了新平台。这里也同时体现了定义别名,增强了代码的可扩展性。
在C语言标准库中,有非常多这样为了兼顾平台移植性而被定义的类型别名,比如:
-
**size_t类型。**此类型在任何平台下,都代表一个无符号的整数类型。在大多数情况下,它被设计为和平台位数一致的存储大小,比如32位平台下,它就是一个32位的无符号整数。size_t非常有用常用,它广泛用于表示那些在逻辑上不会为负数的概念,如:
- 数组的长度
- 字符串的长度
- 某个数据结构中的元素、结点的数量
- 内存大小,比如sizeof运算符的结果类型就是size_t
- …
size_t非常常见常用,要记住这个类型别名。
-
int8_t
,int16_t
,int32_t
,int64_t
: 无论任何平台下,分别表示确切的8、16、32、64位有符号整数。 -
uint8_t
,uint16_t
,uint32_t
,uint64_t
: 无论任何平台下,分别表示确切的8、16、32、64位无符号整数。 -
…
总之,为类型定义别名非常有用,请大家要重视这个语法。以上。
八、C语言中的布尔值表示
在数据类型章节的最后,我们一起来看一下C语言中的布尔值表示,它是代码中十分常用的概念。
首先,C语言的基本数据类型中并没有提供类似Java/C++的布尔类型来表示true或者false。
所以,为了表示布尔值C语言规定:
- 任何非零值都被视为true(真)
- 任何零值都被视为false(假)
注:
整数类型和浮点类型都是数值类型,它们的零值和非零值很容易理解,但C语言还有一个比较特殊的指针类型。在指针类型中:
- 零值代表空指针,即NULL
- 非零值代表指针指向一片内存区域,即非空针。
这种无布尔类型的设计,在早期为C程序员带来了很大的麻烦,使得程序员经常能写出一些可读性差的丑陋代码。为了改变这一局面,从C99开始,C语言支持了独立的布尔类型_Bool
,但它不属于基本数据类型。
为了让程序员更容易地使用布尔类型,C99还提供了一个头文件<stdbool.h>
。当你包含这个头文件时,你可以使用以下标识符更好的来使用布尔类型:
- bool,实际上就是类型
_Bool
的别名。建议使用别名来使用该布尔类型,而不是_Bool
。 - true,表示真。实际上就是整数值1。
- flase,表示假。实际上就是整数值0。
以上。
关于在C语言中使用布尔值的建议
首先,我们强烈不建议大家直接把一个整数、指针类型变量作为布尔值在代码中直接使用。比如
代码块 31. 用整数值直接代表布尔值使用-演示代码
int a = 0;
if (!a) { // 等价于 a == 0
printf("a is 0.\n");
}
这样的做法虽然使得代码简洁,但牺牲了很多可读性,使得代码不够直观明确的表达含义,在现代C编程中是比较得不偿失的。(如果把a
改成一个指针类型,这个代码将更加丑陋)
所以应该写成以下格式:
if (a == 0)
除此之外,我们还建议,当你需要把布尔值作为函数的返回值和形参时,也尽量不要直接用int类型,而是包含头文件使用类型别名bool。
比如:
代码块 32. 使用bool布尔类型作为函数返回值和形参类型-演示代码
#include <stdbool.h>
bool fun(void) {
// ...
return true;
}
总之,在现代的C编程中,我们更追求更好的可读性和明确性,尤其是当确定代码会在C99标准及以后的编译器平台上运行时,使用布尔类型bool是一个好习惯。
九、附录: ASCII码表
ASCII(American Standard Code for Information Interchange)美国信息交换标准代码),是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。
表格中的每一个十进制ASCII编码值,都映射一个字符。ASCII表是最基本的字符编码表,现在常用的编码表大多是兼容ASCII表的。
表 4. ASCII表参考
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | 90 | Z | 122 | z | |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
不要去尝试记忆ASCII码表,没有任何意义,需要用的时候查表即可。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!