目录
1 自动类型转换(隐式转换)
1.1 运算过程中的自动类型转换
1.1.1 转换规则
1.1.2 转换方向
1.1.3 案例演示
1.2 赋值时的自动类型转换
1.2.1 案例演示
2 强制类型转换(显式转换)
2.1 介绍
2.2 转换格式
2.3 转换规则
2.4 案例演示
2.5 实际场景:整除除法希望结果是浮点数
3 数据溢出和模运算
3.1 数据溢出的类型
3.2 数据溢出的主要原因
3.3 数据溢出的后果
3.4 溢出:给 signed 赋一个超过其范围的值
3.5 模运算:给 unsigned 赋一个超过其范围的值
3.6 给 unsigned 赋一个负数值
3.7 浮点数的精度丢失
3.8 类型转换范围溢出-值截断
4 测试题
1 自动类型转换(隐式转换)
1.1 运算过程中的自动类型转换
不同类型的数据进行混合运算,会发生数据类型转换,窄类型会自动转为宽类型,这样不会造成精度损失。
1.1.1 转换规则
- 不同类型整数进行运算,窄类型整数自动转换为宽类型整数。
- 小于 int 类型的整数会被自动提升为 int 类型,以此类推,见1.1.2 转换方向。
- 当有符号整数与无符号整数进行运算时,有符号整数会被转换为无符号整数。
- 不同类型浮点数进行运算,精度小的类型自动转换为精度大的类型。
- 整数与浮点数进行运算,整数自动转换为浮点数。
1.1.2 转换方向
虽然 long long 类型的存储空间大小(8 字节)比 float 类型的存储空间(4 字节)要大,但是 long long 还是会转换到 float,应为 float 能表示的数据范围大!只需要记住:整数与浮点数进行运算,整数自动转换为浮点数即可。
1.1.3 案例演示
#include <stdio.h>
int main()
{
// 整型提升
// 在表达式中,小于 int 类型的整数(如 short, char)会被提升为 int 类型进行运算
char ch1 = 10;
short s1 = 10;
int n1 = 40000;
// ch1 和 s1 在参与加法运算时会被自动提升为 int 类型
printf("ch1 + n1 = %d,sizeof(ch1 + n1) = %zu\n", ch1 + n1, sizeof(ch1 + n1)); // 输出:ch1 + n1 = 40010,sizeof(ch1 + n1) = 4
printf("s1 + ch1 = %d,sizeof(s1 + ch1) = %zu\n", s1 + ch1, sizeof(s1 + ch1)); // 输出:s1 + ch1 = 20,sizeof(s1 + ch1) = 4
// 有符号整数自动转为无符号整数
// 当有符号整数与无符号整数进行运算时,有符号整数会被转换为无符号整数
// 注意:这可能会导致意外的结果,特别是当有符号整数为负时
int n2 = -100;
unsigned int n3 = 20;
// -100 作为有符号整数(1111 1111 1111 1111 1111 1111 1001 1100)转换为无符号整数时,会变成一个非常大的数:4294967196(补码原理)
// 然后与 n3 相加,结果是一个无符号整数
printf("n2 + n3 = %u,sizeof(n2 + n3) = %zu\n", n2 + n3, sizeof(n2 + n3)); // 输出:n2 + n3 = 4294967216,sizeof(n2 + n3) = 4
// 使用 %d 来输出
printf("n2 + n3 = %d\n", n2 + n3); // n2 + n3 = -80
// 不同类型的浮点数运算,精度低的转为精度高的
// 当 float 和 double 类型进行运算时,float 会被提升为 double类型进行运算
float f1 = 1.25f;
double d2 = 4.58667435;
// f1 在参与加法运算时会被自动提升为 double 类型,然后与 d2 相加
printf("f1 + d2 = %.10f,sizeof(f1 + d2) = %zu\n", f1 + d2, sizeof(f1 + d2)); // 输出:f1 + d2 = 5.8366743500,sizeof(f1 + d2) = 8
// 整型与浮点型运算,整型转为浮点型
// 当整型与浮点型进行运算时,整型会被提升为浮点型进行运算
int n4 = 10;
double d3 = 1.67;
// n4在参与加法运算时会被自动提升为double类型,然后与d3相加
printf("n4 + d3 = %f,sizeof(n4 + d3) = %zu\n", n4 + d3, sizeof(n4 + d3)); // 输出:n4 + d3 = 11.670000,sizeof(n4 + d3) = 8
long long ll = 3523235;
float f2 = 1.353543;
printf("ll + f2 = %f,sizeof(ll + f2) = %zu\n", ll + f2, sizeof(ll + f2)); // 输出:ll + f2 = 3523236.250000,sizeof(ll + f2) = 4
return 0;
}
1.2 赋值时的自动类型转换
在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
如果窄类型赋值给宽类型,不会造成精度损失;
如果宽类型赋值给窄类型,可能会发生数据丢失,特别是当宽类型包含小数部分或超出窄类型的范围时。在这种情况下,小数部分会被截断,只保留整数部分。如果宽类型的值超出了窄类型的范围,还可能发生溢出,导致不可预测的结果。
1.2.1 案例演示
#include <stdio.h>
int main()
{
// 赋值:窄类型赋值给宽类型
// 这里,int 类型(窄类型)的变量 a1 被赋值给 double 类型(宽类型)的变量 a2
// 不会发生数据丢失,因为 double 有足够的精度和范围来存储 int 的值
int a1 = 10;
double a2 = a1;
printf("%f\n", a2); // 输出:10.000000,因为 a1 的值被准确地转换成了 double 类型
// 赋值:宽类型赋值给窄类型
// 这里,double 类型(宽类型)的变量 b1 被赋值给 int 类型(窄类型)的变量 b2
// 这种情况可能会导致数据丢失,因为 int 可能没有足够的精度或范围来存储 double 的所有值
// 特别是当 double 包含小数部分或超出 int 的范围时
double b1 = 1.2;
int b2 = b1;
// 当 double 被赋值给 int 时,小数部分会被截断,只保留整数部分
printf("%d\n", b2); // 输出:1,因为 1.2 的小数部分被截断了
double b3 = 133.456; // 133 二进制低八位:1000 0101(-123的补码)
char b4 = b3;
printf("%d\n", b4); // 输出:-123,发生数据溢出
// 赋值:宽类型赋值给窄类型
int c1 = 10;
short c2 = c1;
printf("%d\n", c2); // 输出:10,没有变化
return 0;
}
总结:
1. 窄类型(如 int)赋值给宽类型(如 double)时,不会发生数据丢失,转换是安全的。
2. 宽类型(如 double)赋值给窄类型(如 int)时,可能会发生数据丢失,特别是当宽类型包含小数部分或超出窄类型的范围时。在这种情况下,小数部分会被截断,只保留整数部分。如果宽类型的值超出了窄类型的范围,还可能发生溢出,导致不可预测的结果。
2 强制类型转换(显式转换)
2.1 介绍
隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
2.2 转换格式
(类型名)变量、常量或表达式
2.3 转换规则
当浮点数被显式转换为整数时,小数部分会被丢弃,只保留整数部分。
在进行数学运算时,如果操作数中有浮点数,那么整个运算的结果也是浮点数。
2.4 案例演示
#include <stdio.h>
int main()
{
double d1 = 1.934;
double d2 = 4.2;
// 单独转换 d1 和 d2 为 int 类型,然后相加
// 这里,d1 被截断为 1(因为小数部分被丢弃),d2 被截断为4
// 然后这两个整数相加得到 5
int num1 = (int)d1 + (int)d2; // 结果是 5
// 先将 d1 和 d2 相加,然后将结果转换为 int 类型
// 这里,d1 + d2 = 6.134,然后将 6.134 截断为 6(因为小数部分被丢弃)
int num2 = (int)(d1 + d2); // 结果是 6
// 直接在表达式中进行浮点运算,然后将结果转换为 int 类型
// 这里,3.5 * 10 = 35.0(浮点数),6 * 1.5 = 9.0(浮点数)
// 然后 35.0 + 9.0 = 44.0,最后将 44.0 截断为 44
int num3 = (int)(3.5 * 10 + 6 * 1.5); // 结果是 44
// 打印结果
printf("num1=%d \n", num1); // 输出:num1=5
printf("num2=%d \n", num2); // 输出:num2=6
printf("num3=%d \n", num3); // 输出:num3=44
return 0;
}
2.5 实际场景:整除除法希望结果是浮点数
前面我们提到:在进行数学运算时,如果操作数中有浮点数,那么整个运算的结果也是浮点数。如果没有浮点数,全是整型数据,那么结果也会只会是整数,小数部分会被自动截断,即整数除法的结果是整数。
那有时候我们希望结果是一个小数形式的,比如求平均数的场景,那我们就得将操作数之一转换为浮点数,就可以使用强制类型转换。
#include <stdio.h>
int main()
{
int i = 5;
float j = i / 2; // 由于 i 和 2 都是整数,所以这里的除法运算是整数除法。
printf("%f\n", j); // 2.000000,整数除法,小数部分会被自动截断
/* 如果想看到小数部分,只需要将操作数之一转换为浮点数即可 */
// 方法 1:将整数变量 i 强制转换为浮点数 (float) i
float k = (float)i / 2; // 强制转换,将整数 i 转换为浮点数
printf("%f\n", k); // 2.500000
// 方法 2:将除数 2 明确指定为浮点数 2.0 改变除法运算的性质
// i 会被隐式地转换为浮点数(通常是 double,因为 2.0 是 double 类型的)
// 除法运算的结果是 double 类型的,因此在将结果赋值给 m 时,会发生从 double 到 float 的隐式类型转换
float m = i / 2.0; // 2.0 是一个浮点数字面量
printf("%f\n", m); // 2.500000
return 0;
}
3 数据溢出和模运算
C 语言中的数据溢出是一种常见且需要重视的编程错误,它发生在数据存储或计算过程中,由于超出了变量类型所能表示的范围而导致的。
3.1 数据溢出的类型
在 C 语言中,数据溢出主要可以分为以下几种类型:
整数溢出:当整数运算的结果或存储的数据超出了其类型所能表示的范围时,就会发生整数溢出。例如,对于 unsigned short 类型(取值范围为 0 到 65535),如果尝试将值设置为 65536,就会发生溢出。
浮点数溢出:虽然浮点数(如 float 和 double)在理论上可以表示非常大或非常小的数值,但由于其存储方式和精度的限制,当数值过大或过小,以至于无法用给定的精度表示时,就会发生精度丢失或溢出。然而,严格来说,浮点数通常不会因为数值过大而 “溢出” 到错误的值,而是会因为精度不足而丢失部分信息。
类型转换溢出:当将一种数据类型的值转换为另一种类型,且目标类型的表示范围小于原类型时,如果原值超出了目标类型的范围,就会发生溢出。例如,将 int 类型的值转换为 char 类型时,如果 int 类型的值超出了 char 类型的范围,就会发生溢出。但更准确地说,这是类型转换时的范围溢出或值截断。
3.2 数据溢出的主要原因
- 使用了不适当的数据类型:选择了无法容纳预期数据范围的数据类型。
- 未对输入数据进行有效验证:在接收用户输入或处理外部数据时,未进行范围检查。
- 进行了超出类型范围的运算:如整数加法、乘法等运算的结果超出了变量类型的表示范围。
3.3 数据溢出的后果
- 错误的计算结果:溢出后得到的数值往往是不正确的,可能导致程序逻辑错误。
- 数据损坏:溢出的数据可能覆盖或破坏内存中的其他数据,导致数据损坏。
- 程序崩溃:在某些情况下,数据溢出可能导致程序崩溃或异常终止。
- 安全漏洞:在安全关键的系统中,数据溢出可能被恶意利用,导致安全漏洞。
3.4 溢出:给 signed 赋一个超过其范围的值
当尝试给一个有符号数据类型(如 char、short、int 等)赋予一个超出其表示范围的值时,通常会发生溢出。溢出是当运算结果太大而无法存储在分配的空间内时发生的一种情况。对于有符号整数,溢出会导致结果回绕(wrap around)到该类型能表示的最小值或最大值。
溢出:运算结果超出变量能表示的范围。
回绕:溢出时,结果可能会从最大值 “回绕” 到最小值,或者从最小值 “回绕” 到最大值(取决于具体的溢出情况和数据类型)。
未定义行为:在某些情况下,溢出可能导致未定义行为(Undefined Behavior, UB),这意味着编译器可以自由地以任何方式处理这种情况,包括但不限于产生不可预测的结果、崩溃或执行恶意代码。然而,对于整数溢出,大多数现代编译器和硬件平台都会以可预测的方式(如回绕)处理它。
// 尝试给 signed char 赋一个超过其范围的值(这可能会导致溢出)
signed char sc = 128; // 在某些情况下,这可能会溢出到 -128(取决于二进制表示和编译器)
printf("Signed char after overflow: %d\n", sc); // -128
在 C 语言中,signed char 类型的取值范围是 -128 到 127。尝试将一个超过这个范围的值赋给 signed char 类型的变量时,就会发生溢出。
在上面例子中,首先字面量 128 默认是 int(32 位)数据类型,然后尝试将字面量 128 赋给 signed char(8 位),这里会发生一次隐式类型转换(int -> char),所以变量 sc 中存储的是字面量 128 二进制(0000 0000 0000 0000 0000 0000 1000 0000)的低八位:1000 0000,但是变量 sc 是有符号类型,所以:1000 0000 将表示一个负数(补码)。可以通过计算器求得数值:
也可以手动计算:我们知道 1111 1111 表示 -1 的补码,1000 0000 即表示 1111 1111 一直减一减到最后,即 8 位二进制能表示的负数最小值:-128(-2^7)。所以最终打印输出为:-128。
#include <stdio.h>
int main()
{
// short 能表示的范围:-32,768 (-2^15) 到 32,767 (2^15 - 1)
short sh= 32768; // 这个数超过能表示的范围了,会发生数据溢出
printf("sh=%hd\n", sh);
// 输出:sh=-32768
return 0;
}
在 C 语言中,(signed)short 类型的取值范围是 -32768 到 32767。尝试将一个超过这个范围的值赋给 short 型的变量时,就会发生溢出。
在上面例子中,首先字面量 32768 默认是 int(32 位)数据类型,然后尝试将字面量 32768 赋给 short(16 位),这里会发生一次隐式类型转换(int -> short),所以变量 sh 中存储的是字面量 32768 二进制(0000 0000 0000 0000 1000 0000 0000 0000)的低十六位:1000 0000 0000 0000,但是变量 sh 是有符号类型,所以:1000 0000 0000 0000 将表示一个负数(补码)。可以通过计算器求得数值:
也可以手动计算:我们知道 1111 1111 1111 1111 表示 -1 的补码,1000 0000 0000 0000 即表示 1111 1111 1111 1111 一直减一减到最后,即 16 位二进制能表示的负数最小值:-32768(-2^15)。所以最终打印输出为:-32768。
3.5 模运算:给 unsigned 赋一个超过其范围的值
当尝试给一个无符号数据类型(如 unsigned char、unsigned short 、unsigned int 等)赋予一个超出其表示范围的值时,实际上并不会发生“溢出”这个词通常所指的那种情况,因为无符号类型不区分正负。相反,这个值会被解释为该类型能表示的最大值加上的一个数,然后通过一个模运算(在这个上下文中,更准确地说是模该类型能表示的最大值加 1)来得到一个有效的无符号值。
模运算:在数学中,模运算(或取余运算)返回两个数相除后的余数。在无符号整数上下文中,当赋予一个超出范围的值时,实际上是通过模该类型能表示的最大值加 1 来得到一个有效的值。
无符号整数的 “溢出”:虽然不常使用“溢出”这个词来描述无符号整数的情况,但可以说这个值 “回绕” 到了该类型能表示的范围内的另一个值。不过,更准确的描述是通过模运算得到的。
无符号整数的表示:无符号整数以二进制补码的形式存储(尽管它们没有符号位),但解释这些位时不会考虑符号,而是直接将其视为一个非负整数。
// 尝试给 unsigned char 赋一个超过其范围的值(这会导致模运算)
unsigned char uc = 257; // 这将模 256(255+1),uc 的值将变为 1
printf("Unsigned char after overflow: %u\n", uc); // 1
在 C 语言中,unsigned char 类型的取值范围是 0 到 255。尝试将一个超过这个范围的值赋给 unsigned char 类型的变量时,就会发生模运算。变量 uc 实际存储的值是 257 除以 256(255+1) 的余数,为 1,所以变量 uc 的值为 1。
底层原理:在上面例子中,首先字面量 257 默认是 int(32 位)数据类型,然后尝试将字面量 257 赋给 unsigned char(8 位),这里会发生一次隐式类型转换(int ->char),所以变量 uc 中存储的是字面量 257 二进制(0000 0000 0000 0000 0000 0001 0000 0001)的低八位:000 0001,即为 1。
总结:
- 对于 signed 类型,超出范围的值会导致溢出,可能引发回绕到最小或最大值。
- 对于 unsigned 类型,超出范围的值会通过模运算得到一个有效的无符号值。
3.6 给 unsigned 赋一个负数值
当给一个 unsigned 类型赋值一个负数时,由于 unsigned 类型不表示负数,这个负数会被当作一个大的正数来处理。
#include <stdio.h>
int main()
{
unsigned short sh = -20; // 给一个无符号整数赋一个负数值,
printf("sh=%hu\n", sh);
// 输出:sh=65516
return 0;
}
在上面例子中,首先字面量 -20 默认是 int(32 位)数据类型,然后尝试将字面量 -20 赋给 unsigned short(16 位),这里会发生一次隐式类型转换(int -> unsigned short),所以变量 sh 中存储的是字面量 -20 二进制(1111 1111 1111 1111 1111 1111 1110 1100)的低十六位:1111 1111 1110 1100,但是变量 sh 是无符号类型,所以:1111 1111 1110 1100 将表示一个正数。可以通过计算器求得数值:
计算规律:当给一个 unsigned 类型的变量赋一个负数值时,这个负数值首先被转换为它的二进制补码表示,然后这个补码被直接解释为一个无符号数。这个无符号数与原始负数在数值上的关系是:它们之和等于该 unsigned 类型能表示的最大值加 1(在这个例子中是 65536)。所以这里无符号数 + 20(数值上的和,不管符号)应该等于 65536(2^16,65535+1),即无符号数为 65516。
#include <stdio.h>
int main()
{
// unsigned short 类型能表示的最大值为 65535 加 1 = 65536
unsigned short sh1 = -20; // 给一个无符号整数赋一个负数值,
printf("sh1=%hu\n", sh1);
// 输出:sh1=65516 <=> 65536-20
unsigned short sh2 = -30; // 给一个无符号整数赋一个负数值,
printf("sh2=%hu\n", sh2);
// 输出:sh=65506 <=> 65536-30
unsigned short sh3 = -40; // 给一个无符号整数赋一个负数值,
printf("sh3=%hu\n", sh3);
// 输出:sh=65496 <=> 65536-40
unsigned short sh4 = -32768; // 给一个无符号整数赋一个负数值,
printf("sh4=%hu\n", sh4);
// 输出:sh=32768 <=> 65536-32768
return 0;
}
3.7 浮点数的精度丢失
浮点数在 C 语言中使用 IEEE 754 标准来表示,这个标准允许浮点数表示非常大的数和非常小的数,但是有一定的精度限制。
当数值超出表示范围或精度极限时,浮点数会表现出特殊的行为,而不是像整数那样发生“溢出”。对于超出表示范围的数,浮点数会变成无穷大(Infinity)或无穷小(Underflow to zero),而对于接近但又不足以准确表示的数,浮点数会出现精度丢失。
#include <stdio.h>
int main()
{
float a = 1e30f; // 一个相对较大的浮点数
float b = a + 1.0f; // 尝试在 a 的基础上加 1
// 由于 a 的值非常大,浮点数 a 可能无法精确表示 a+1 的结果,因为精度有限
if (a == b)
{
printf("a 和 b 相等(由于精度丢失)\n"); // 会打印
}
else
{
printf("a 和 b 不相等(理论上应该如此,但可能由于精度问题显示相等)\n");
}
// 打印 a 和 b 的值以观察差异
printf("a = %e\n", a); // a = 1.000000e+30
printf("b = %e\n", b); // b = 1.000000e+30
// 为了更清楚地看到精度问题,我们可以尝试打印它们的差值
float diff = b - a;
printf("a 和 b 的差值 = %e\n", diff); // a 和 b 的差值 = 0.000000e+00
// 注意:由于浮点数的表示方式,差值可能非常小,接近于0
return 0;
}
在这个示例中,我们创建了一个相对较大的浮点数 a,并尝试在其基础上加 1 来创建 b。然而,由于 float 类型的精度限制,a 和 b 在内存中的表示可能非常接近,以至于在比较时它们可能被视为相等(尽管这取决于具体的编译器和浮点数实现)。
当你运行这段代码时,你可能会看到 a 和 b 的值打印出来非常接近,而且它们的差值非常小(接近于 0),这表明在尝试对非常大的浮点数进行微小修改时发生了精度丢失。
重要的是要理解,这并不是因为数值 “溢出” 到了错误的值,而是因为浮点数在表示极大数值时的精度有限。在极端情况下,如果数值超出了浮点数能表示的范围,那么它可能会变成 INF(正无穷大)或 -INF(负无穷大),或者在某些情况下变成 NaN(不是一个数)。但在本例中,我们只是在展示精度丢失的问题。
3.8 类型转换范围溢出-值截断
当将一个数据类型的值转换为另一个类型,并且目标类型的表示范围小于原类型时,如果原值超出了目标类型的范围,那么会发生一种 “溢出” 现象,但更准确地说,这是类型转换时的范围溢出或值截断。在 C/C++ 等语言中,这种转换通常是隐式的(除非显式地进行了类型转换),并且结果可能会导致数据丢失或意外的行为。
#include <stdio.h>
#include <limits.h>
int main()
{
int intValue = 256; // int 类型可以存储比 256 大得多的值
char charValue;
// 将 int 类型的值转换为 char 类型
charValue = (char)intValue;
// 输出原始的 int 值和转换后的 char 值
printf("Original int value: %d\n", intValue); // 256
printf("Converted char value: %d\n", charValue); // 0
// 输出 CHAR_MAX 和 CHAR_MIN 以显示 char 类型的范围
printf("CHAR_MIN: %d\n", CHAR_MIN); // -128
printf("CHAR_MAX: %d\n", CHAR_MAX); // 127
return 0;
}
在上面例子中,首先字面量 256 是 int(32 位)数据类型,然后尝试将其强转成 char(8 位),所以变量 charValue 中存储的是字面量 256 二进制(0000 0000 0000 0000 0000 0001 0000 0000)的低八位:0000 0000,即为0,所以输出打印 charValue 值为 0。
提示:不管是数据溢出还是模运算或是其他情况,只需要掌握底层的补码原理和数据类型位数即可!
4 测试题
1. 变量 int a 和 short b ,现在 a 和 b 进行运算 a + b, 哪个变量会发生数据类型转换,转换为什么类型 ?
【答案】变量 b 会发生数据类型转换,转换为 int 类型。
【解析】运算过程中,窄类型整数自动转换为宽类型整数。
2. 如果将一个字节宽度较大的类型转为字节宽度较小的类型,可能会造成什么问题 ?
【答案】精度损失、数据溢出
【解析】宽类型(如 double)赋值给窄类型(如 int)时,可能会发生数据丢失,特别是当宽类型包含小数部分或超出窄类型的范围时。在这种情况下,小数部分会被截断,只保留整数部分,导致精度损失。如果宽类型的值超出了窄类型的范围,还可能发生溢出,导致不可预测的结果。