《C语言程序设计现代方法》note-4 基本类型 强制类型转换 类型定义

文章目录

助记提要

  1. 6种整数类型;
  2. 如何确定整数类型的范围;
  3. 编译器如何确定整型常量的范围;
  4. 整数溢出时的行为;
  5. 读写整数的转换说明符;
  6. 浮点数的范围;
  7. 数字转义序列;
  8. getchar的惯用法;
  9. 需要类型转换的原因;
  10. 会发生隐式转换发生的4个情况;
  11. 常规算数转换和赋值转换的规则;
  12. 强制类型转换的应用;
  13. typedef的优点;
  14. sizeof的2个注意事项;

7章 基本类型

7.1 整数类型

有符号整数和无符号整数

最左边带符号位的是有符号整数。正数或0的符号位是0;负数的符号位是1。

不带符号位的整数是无符号整数,最左边的位也是数值的一部分。无符号整数主要用于系统编程和底层与机器相关的应用。

C语言的整数默认都是有符号的。如果要告诉编译器变量没有符号位,需要把它声明为unsigned类型。

整数类型的说明符

除了int外,C语言还提供了长整型和短整型,用来存储大数或存小数节省空间。

为了使构造的整数类型满足需要,可以指明变量是long类型或short类型,signed类型或unsigned类型。也能把说明符组合起来,产生以下6种不同的类型:

C 复制代码
short int
unsigned short int
int 
unsigned int
long int
unsigned long int

其他组合都是这6种类型的同义词。

由于整数默认都是有符号的,所以signed可以省略。

C语言允许省略int来缩写整数类型的定义。unsigned short int可以写为unsigned short

说明符的先后顺序不影响类型声明。
为了满足对超大型整数的需求和适应64位的处理器,C99提供了long long intunsigned long long int两种类型。

整数类型的范围

C标准要求每个整型类型都要覆盖一个确定的取值范围。

其次,short int类型不能比int类型长,long int类型不能比int类型短。

在不同的机器上,通常的取值范围如下:

16位机

类型 最小值 最大值
short int int -2^15^ 2^15^-1
unsigned short int unsigned int 0 2^16-1
long int -2^31^ 2^31^-1
unsigned long int 0 2^32^-1
32位机
类型 最小值 最大值
:---- :----: :----:
short int -2^15^ 2^15^-1
unsigned short int 0 2^16-1
long int int -2^31^ 2^31^-1
unsigned long int unsigned int 0 2^32^-1
64位机
类型 最小值 最大值
:---- :----: :----:
short int -2^15^ 2^15^-1
unsigned short int 0 2^16-1
int -2^31^ 2^31^-1
unsigned int 0 2^32^-1
long int -2^63^ 2^63^-1
unsigned long int 0 2^64^-1

这些范围不是C标准强制的,不同的编译器也可能不同。

C标准库有<limits.h>头,其中定义了每种整数类型的最大值和最小值的宏,可以用它来确定某个实现中整数类型的范围。

C99的long long int类型要求至少是64位宽。long long int范围通常是-2^63^到2^63^-1,而unsigned logn long int是0到2^64^-1。

整型常量

C语言允许用十进制、八进制和十六进制的形式书写整型常量。

  • 十进制常量包含0~9的数字,但是不能以0开头。
  • 八进制常量包含0~7的数字,必须以0开头。
  • 十六进制常量包含0~9的数字和a~f的字母,总是以0x开头。字母大写或小写都可以。

不同进制只是数的书写方式,所有的数都是二进制的形式存储的。

八进制和十六进制更适用于底层程序的编写。

编译器如何判断常量的类型

十进制的常量类型一般为int,如果int不够,就用long int类型。long int也不够,则用unsigned long int做最后的尝试。

而对八进制和十六进制,编译器会一次尝试intunsigned intlong intunsigned long int类型,直到找到能表示该常量的类型。

C99对于没有后缀的十进制常量,会使用intlong intlong long int中可表示该值的最小类型。对于八进制和十六进制常量,类型判定顺序为intunsigned intlong intunsigned long intlong long intunsigned long long int

在常量后面加上字母Ll,可以强制编译器把常量做为长整数处理。在常量后加Uu,可以指定无符号常量。LU可以结合使用。

C99中以LLll结尾的整型常量是long long int类型。

整数溢出

算数运算时,结果可能因为太大而无法表示。

对两个int进行算数运算时,结果必须仍然可以用int表示,否则就会发生溢出。

整数溢出时的行为和整数的负号有关:

  • 有符号整数溢出的行为是未定义的。可能只是运算出错,也可能程序崩溃。
  • 无符号整数溢出时,结果是正确值对2^n^取模。n是存储结果的位数。
读/写整数
  • %d只适用于int类型。
  • 无符号整数,使用%u%o%x分别表示十进制、八进制、十六进制。
  • 短整数在d、o、u、x前面加上h
  • 长整数在d、o、u、x前面加上l
  • 长长整数(C99)在d、o、u、x前面加上ll
C 复制代码
unsigned int u;
scanf("%u", u);     // 读入十进制
printf("%u", u);    // 显示十进制
scanf("%o", u);     // 读入八进制
printf("%o", u);    // 显示八进制
scanf("%x", u);     // 读入十六进制
printf("%x", u);    // 显示十六进制

short s;
scanf("%hd", s);    // 读入短整型
printf("%hd", s);   // 显示短整型

long l;
scanf("%ld", l);    // 读入长整型
printf("%ld", l);   // 显示长整型

long long ll;
scanf("%lld", ll);  // 读入长长整型
printf("%lld", ll); // 显示长长整型

%x%o只适合用于显示无符号整数或正数。因为printf会假设被转换的数值是无符号的,如果符号位为1,会显示一个预期外的大数。

显示负数的八进制或十六进制的情况很少。需要显示时可以自己输出一个负号。

7.2 浮点类型

C语言的浮点数格式有三种:float,单精度浮点数;double,双精度浮点数;long double,扩展精度浮点数。

浮点数的范围

C标准没有规定floatdoublelong double类型的精度。大多数计算机遵循IEEE标准。

IEEE标准提供了单精度和双精度的格式标准。单精度32位,双精度64位。

数值义科学计数法存储,每个数由符号指数小数组成。指数部分表示数值的可能大小,小数部分的位数表示精度。

单精度数,指数占8位,小数占23位。单精度数可以表示的最大值是由指数决定的,指数范围-126到127(-127和128有特殊用途),指数最大是2^127^;32位小数位都是1时底数最大,最大底数约等于2。所以单精度数可表示的最大值为 2*2^127^≈3.402*10^38^。

浮点常量

浮点常量必须包含小数点或指数。如果有指数,需要在指数值前面加字母eE

默认情况下,浮点常量以double的形式存储,在需要时可以自动转为float

为了让编译器以float格式存储,可以在常量的末尾加上字母Ff;要以long double格式存储,在末尾加Ll

C语言默认倾向于使用精度更高的double类型。用float类型是为了节省空间和时间。

读/写浮点数
  • float类型
    %e%f%g用于读写单精度浮点数。

  • double类型
    scanf函数读取double类型时,在前面加小写字母l
    printfefg既可以写float也可以写double。C99允许printf函数使用%le%lf%lg,但是字母l不起作用。

    scanf和printf函数有可变长度的参数列表。调用可变长度的参数列表时,编译器会把float参数自动转成double类型。这导致printf无法区分float和double,所以%f在printf中既可以表示float又可以表示double。而scanf是读取的值要通过指针传给变量,%f%lf必须区别开,才能正确存储对应的字节数。

    C 复制代码
    double d;
    scanf("%lf", &d);
    printf("%f", d);
  • long double类型

    读写long double类型时,在前面加大写字母L

    C 复制代码
    long double ld;
    scanf("%Lf", &ld);
    printf("%Lf", ld);

十六进制浮点常量以0x0X开头,且必须包含指数(跟在字母p或P后面)。指数以十进制表示,可以为负,代表2的幂。如:0x1.Bp3表示1.6875 × 2^3^ = 13.5

十六进制浮点常量主要用于指定精度要求较高的浮点常量。十进制数在转为二进制时可能受到舍入误差的影响,而十六进制有精确的二进制表示。

7.3 字符类型

char字符类型的值依据机器的字符集来定。

目前最常用的字符集是美国信息交换标准码(ASCII)字符集。它使用7位代码来表示128个字符。

char类型的变量可以用任意的单个字符赋值。赋值时,字符常量需要使用单引号括起来,不能用双引号。

字符被当做整数来操作

C语言把字符当做小整数进行处理。

计算中出现字符时,C语言只是使用它对应的整数值。可以像比较数字那样对字符进行比较。

以数字的方式处理字符的坏处是,编译器无法发现处理过程中的编程错误。程序在使用不同字符集的机器上也可能会不兼容。
字符类型的符号

C标准没有规定char类型的数据是有符号还是无符号型。有些编译器把它们当做有符号型来处理,有些编译器当做无符号型处理。

为了保证可移植性,不要假设char类型默认是signedunsigned。在需要作区别的时候,使用signed charunsigned char

声明为char的变量存储的字符如果只有7位长度,不需要考虑其符号。如果有8位长度,许多编译器会把它当做有符号的字符来处理。后面这个变量只作为字符使用就不会有问题,但是如果需要转换为整数,就可能会有问题(结果为负数)。

转义序列

一些特殊字符无法用键盘输入,或不可见。使用转义序列可以让程序处理字符集中的每一个字符。

转义序列有两种:字符转义序列和数字转义序列

  • 字符转义序列
字符转义序列 说明
\a 警报符
\b 回退符
\f 换页符
\n 换行符
\r 回车符
\t 水平制表符
\v 垂直制表符
\\ 反斜杠
? 问号
' 单引号
" 双引号

字符转义序列很易使用,但是只包含了最常用的字符。有些无法打印的字符和ASCII码以外的字符没有被包含在内。

而数字转义序列可以表示任何字符。

  • 数字转义序列

字符的八进制和十六进制值可以用来书写转义序列。

使用八进制做转义序列时,由字符\和跟在后面的最多含3位数字的八进制数组成。如\33\033

十六进制转义序列由\x和跟在后面的十六进制数组成。

C 复制代码
int main() {
    printf("%c\n", 'a');
    printf("%d\n", 'a');
    printf("\141\n");
    printf("\x61\n");
    return 0;
}
a
97
a
a

八进制和十六进制转义序列表示的数必须为无符号数,所以八进制值最大为377。标准C对十六进制数的位数没有限制,在字符长度为8位时,十六进制值最大为FF。

大小写转换

使用if语句可以把小写字母转换为大写字母:

C 复制代码
if ('a' <= ch && ch <= 'z')
	ch = ch - 'a' + 'A';

也可以使用C语言的库函数toupper

C 复制代码
#include <ctype.h>

ch = toupper(ch);
scanfprintf读/写字符

使用转换说明符%c对单个字符读写:

C 复制代码
char ch;
scanf("%c", &ch);
printf("%c", ch);

scanf在读取数值时,会自动跳过空白符,但是读取字符时不会。

为了强制它读入字符前跳过空白字符,可以在转换说明的前面加一个空格:

C 复制代码
scanf(" %c", &ch);

scanf不跳过空白符的特性可以用来检查是否是输入行的结尾。

getcharputchar读写字符

getchar读入一个字符并返回。putchar输出单个字符。
getcharputchar的执行速度比scanfprintf要快。因为不需要考虑数据的不同格式,而且getcharputchar是做为宏实现的。
getchar在读取时也不会跳过空白符。
scanf函数的返回值一般是1。而getchar函数返回的是读取到的字符。由此getchar能很方便的应用在需要对字符做条件判断的地方。

C 复制代码
// scanf实现
do {
    scanf("%c", &ch);
} while (ch != '\n');

// 使用getchar
while ((ch=getchar()) != '\n')
    ;

getchar常用的两种场景:

C 复制代码
// 跳过剩余的行
while (getchar() != '\n')
    ;
// 跳过空格。循环结束时,ch的值为遇到的第一个非空字符
while ((ch=getchar()) != ' ')
    ;

注意 混用scanfgetchar时,由于scanf会把没有消耗的任意字符留下,getchar取到的字符可能不是希望的结果:

C 复制代码
printf("Enter an integer: ");
scanf("%d", &i);
printf("Enter a command: ");
// command取到换行符
command = getchar();

7.4 类型转换

类型转换的必要性

C语言允许在表达式中混合使用基本类型。而计算机执行算术运算时,要求操作数有相同的位数,和相同的存储方式。

C编译器会自动生成一些指令,把操作数转换为合适的类型,这样表达式才能被硬件计算。

这种编译器自动处理的转换称为隐式转换 。而由程序员使用强制运算符执行的是显式转换

发生隐式转换的情况
  1. 算术表达式或逻辑表达式中的操作数类型不同(常规算术转换);
  2. 赋值表达式右侧表达式的类型和左侧变量的类型不匹配;
  3. 函数调用的实参类型和形参类型不匹配;
  4. return语句中表达式类型和函数返回值类型不匹配。
常规算术转换

常规算术转换会把两个操作数转换为可以安全地适用于两个数值的存储位数最小的类型。

通常是把存储位数较少类型的操作数转换为另一个操作数的类型。这种操作称为提升

常规算术转换可以分成两种情况:

  1. 任一操作数是浮点类型
    float->double->long double的顺序,对存储位数小的操作数做提升。
  2. 两个操作数都不是浮点类型
    先把两个操作数进行整值提升(确保没有字符型或短整型操作数),然后按int->unsigned int->llong int->unsigned long int的顺序提升存储位数小的操作数。
    如果出现long intunsigned int类型长度相同的情况,两个操作数会转换为unsigned long int类型。

注意 有符号操作数和无符号操作数混用时,有符号操作数会转换为无符号的值。转换过程会加上或者减去n+1的倍数。n是无符号整数可表示的最大值。实际编程时要避免使用无符号整数,更不要和有符号整数混用。

C 复制代码
int main() {
    // 混合比较有符号整数和无符号整数
    int a = -1;
    unsigned int b = 5;
    printf("%d\n", (a<b));	// 输出结果为0,不符合预期
    return 0;
}

C99中增加了一些新的类型,转换规则也有变化。它先规定了整数类型的转换等级:

  1. (最高)long long intunsigned long long int
  2. long intunsigned long int
  3. intunsigned int
  4. short intunsigned short int
  5. charsigned charunsigned char
  6. (最低)_Bool

C99在任一操作数是浮点类型时,规则同上。在两个操作数都不是浮点类型时,依次执行以下判定:

  1. 先把转换等级低于intunsigned int的类型转为intunsigned int,如果这时两个操作数类型相同,就结束转换;
  2. 如果都是有符号型,或都是无符号型,把转换等级低的操作数转换为等级高的操作数类型;
  3. 如果无符号操作数等级高于或等于有符号操作数,转为无符号操作数的类型;
  4. 如果有符号操作数的类型可以表示无符号操作数类型的所有值,转为有符号操作数的类型;
  5. 其他情况,都转换为与有符号操作数的类型对应的无符号类型。
赋值时的转换

赋值时会把右边表达式转换为左边变量的类型。

这种规则出问题的情况:

  1. 浮点数赋值给整型变量时,小数部分丢失;
  2. 过大的值赋值给存储位数较小的类型变量时,会得到无意义的结果。
强制类型转换

强制类型转换的表达式

(将转换的类型名) 表达式

C语言把(类型名)视为一元运算符,其优先级高于二元运算符。

强制类型转换的应用:

C 复制代码
// 计算浮点型值的小数部分
float f, frac_part;
frac_part = f - (int) f;
C 复制代码
// 避免溢出
long i1, i2;
int j = 100000;
// 由于两个int相乘,结果还是int类型,因此可能会溢出
// 溢出后,把错误的值转为long
i1 = j * j;
// 先把j转为long再计算可避免溢出
i2 = (long) j * j;

7.5 类型定义

前面使用宏定义布尔类型

C 复制代码
#define BOOL int

可以使用类型定义语句实现布尔类型:

C 复制代码
typedef int Bool;

Bool是新类型的名字。首字母不是必须大写。

typedef定义的类型会加入到编译器识别的类型名列表中。

类型定义的好处
  • 能使程序更易于理解,后续需要修改类型也更方便。

    C 复制代码
    // 可以在这里修改Dollars的类型
    typedef float Dollars;
    // 易于理解
    Dollars cash_in, cash_out;
  • 提高易移植性。

    不同机器上的类型取值范围可能是不同的。

    可以使用typedef定义新的类型,避免使用int或其他可能因机器而变化的类型声明。这样在需要更改更大的数据类型时很方便,但是要修改转换说明(如%d改为%ld)依然很麻烦。

    C语言库使用typedef为可能因实现而不同的类型创建类型名,这些类型名一般以_t结尾。

    C99的<stdint.h>头中使用typedef定义占用特定位数的整数类型名。如int32_t是占用32位的有符号整型。

  • 比宏定义更好用

    数组和指针不能定义为宏,但是类型定义可以。

    函数体内typedef定义的名称在函数体外是不识别的。而宏的名称会在任何出现的地方被替换。

7.6 sizeof运算符

用于获取存储指定类型的值需要的内存空间

sizeof(类型名)

返回值是一个无符号整数,表示存储某个类型的字节数。
sizeof也可以用于常量、变量和表达式。
sizeof用于表达式时不需要加圆括号,但是它做为一元运算符,优先级比二元运算符高,使用时为了方便理解,最好加上圆括号。

C 复制代码
// 不需要加圆括号也可以
// 先获取i的存储位数,然后再把结果与j相加
sizeof i + j;
// 最好加上圆括号,便于理解
sizeof(i + j);

注意 sizeof表达式的值是size_t类型,由实现决定的类型。

在C89中最好在转换前把表达式的值转换为一种已知类型,并在显示时使用对应的转换说明符。

C99中的printf可以直接显示size_t的值,不发生强制转换,方法是在转换说明中使用字母z

C 复制代码
printf("%zu\n", sizeof(int));

C99引入了变长数组,编译器不能用sizeof确定变长数组的大小。

相关推荐
昇腾CANN28 分钟前
Ascend C算子性能优化实用技巧05——API使用优化
c语言·开发语言·性能优化
沐泽Mu38 分钟前
嵌入式学习-C嘎嘎-Day03
c语言·开发语言·c++·学习
凭君语未可1 小时前
讲解C语言形参与实参
c语言
爬菜2 小时前
编译原理(手绘)
c语言
是阿建吖!3 小时前
【优选算法】双指针
c语言·c++·算法
原来是猿3 小时前
类和对象(上)
c语言·开发语言·数据结构·c++·算法
OTWOL4 小时前
C语言中操作符详解(中)
c语言·开发语言·c++
ahadee5 小时前
蓝桥杯每日真题 - 第13天
c语言·vscode·算法·蓝桥杯