学习 C 语言时,我们很容易把注意力放在公式上,却忽略数据的类型。例如,同样是计算两个整数的平均值,(a + b) / 2 和 (a + b) / 2.0 可能得到不同的结果。

原因是:数据类型不仅决定数据如何存储,还会影响表达式怎样计算。

本文介绍整型、浮点型、字符型,以及自动转换、赋值转换和强制类型转换。所有示例都是可以单独编译运行的完整程序。

示例采用 C11。涉及字符编码的运行结果以 ASCII 兼容环境为前提。类型占用的字节数可能随编译器和平台变化,文中的 sizeof 结果是本次验证环境的实际输出。

一、先认识变量与数据类型

变量可以理解为一个有名称的存储位置。声明变量时,需要告诉编译器变量叫什么、存放什么类型的数据。

int count = 10;
float price = 2.5f;
double average = 86.75;
char grade = 'A';

这四个变量分别用于存放整数、单精度浮点数、双精度浮点数和字符。

类型

主要用途

声明示例

int

表示计数、数量等整数

int count = 10;

short、long、long long

表示不同范围需求的整数

long total = 1000L;

float

表示单精度浮点数

float price = 2.5f;

double

表示双精度浮点数

double average = 86.75;

long double

表示精度至少不低于 double 的浮点数

long double x = 3.14L;

char

保存字符编码,也可参与整数运算

char grade = 'A';

变量需要先声明后使用。在声明时给初值,称为初始化;声明之后更新变量的值,称为赋值。

int score = 60;  // 初始化
score = 90;      // 赋值

注意,同一条声明中,每个变量都需要单独初始化。例如 int a = 0, b; 只初始化了 a。函数内部未初始化的普通局部变量 b,不能直接拿来读取或计算。

二、整型:表示没有小数部分的数据

1. 有符号整数与无符号整数

整型适合表示人数、次数、编号等数据。

  • 有符号整数能够表示正数、负数和零,例如 int、long。

  • 无符号整数不能表示负数,例如 unsigned int、unsigned long。

具体能够表示多大的数,取决于类型及其实现。不要简单认为所有机器上的 int、long 都具有相同大小。

2. 十进制、八进制和十六进制

同一个整数可以用不同进制的常量形式书写。

形式

书写规则

示例

十进制值

十进制

普通十进制写法

83

83

八进制

以 0 开头,数码为 0~7

0123

83

十六进制

以 0x 或 0X 开头

0x53

83

前导零会影响整数常量的含义。 010 表示八进制的 10,也就是十进制的 8。

示例 1:观察不同进制的整数

#include <stdio.h>

int main(void)
{
    int decimal = 83;
    int octal = 0123;
    int hexadecimal = 0x53;

    printf("decimal = %d\n", decimal);
    printf("octal = %d\n", octal);
    printf("hexadecimal = %d\n", hexadecimal);
    printf("010 = %d\n", 010);
    printf("83 in octal = %o\n", 83u);
    printf("83 in hexadecimal = %X\n", 83u);
    return 0;
}

运行结果:

decimal = 83
octal = 83
hexadecimal = 83
010 = 8
83 in octal = 123
83 in hexadecimal = 53

解释:

83、0123 和 0x53 的写法不同,但表示相同的数值。输出时,%d 按十进制显示,%o 按八进制显示,%X 按十六进制显示,并使用大写字母。后两种格式对应无符号整数,因此示例使用带 u 后缀的 83u。

常量的书写进制与输出进制是两件事:用八进制写入的整数,同样可以按十进制输出。

示例 2:查看类型占用的空间

#include <stdio.h>

int main(void)
{
    printf("sizeof(char) = %zu\n", sizeof(char));
    printf("sizeof(short) = %zu\n", sizeof(short));
    printf("sizeof(int) = %zu\n", sizeof(int));
    printf("sizeof(long) = %zu\n", sizeof(long));
    printf("sizeof(long long) = %zu\n", sizeof(long long));
    printf("sizeof(float) = %zu\n", sizeof(float));
    printf("sizeof(double) = %zu\n", sizeof(double));
    return 0;
}

本次验证环境的运行结果:

sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 8
sizeof(long long) = 8
sizeof(float) = 4
sizeof(double) = 8

解释:

sizeof 得到对象或类型占用的字节数,其结果类型是 size_t,使用 %zu 输出。

C 语言保证 sizeof(char) 为 1,但不能据此推断所有平台的 long 都是 4 字节或 8 字节。实际编程中,应通过 sizeof 查询大小;需要整数类型的范围时,可使用 <limits.h> 中的 INT_MIN、INT_MAX 等宏。

三、浮点型:表示带小数的数据

C 语言常用的浮点型有 float 和 double。double 通常比 float 能保存更多有效数字。

浮点常量可以采用小数形式,也可以采用指数形式:

3.14       // 小数形式
.5         // 等于 0.5
6.0        // 浮点常量
3.2e2      // 3.2 × 10²,即 320.0
1.23E-2    // 1.23 × 10⁻²,即 0.0123

后缀决定浮点常量的类型:

写法

类型

3.14

double

3.14f 或 3.14F

float

3.14L 或 3.14l

long double

注意,12L 是 long 整数常量,12.0L 才是 long double 浮点常量。不能只看后缀,还要看常量本身的写法。

示例 3:计算圆的面积

#include <stdio.h>

int main(void)
{
    double radius = 3.5;
    double pi = 3.14159;
    double area = pi * radius * radius;

    printf("radius = %.1f\n", radius);
    printf("area = %.4f\n", area);
    printf("3.2e2 = %.1f\n", 3.2e2);
    printf("1.23E-2 = %.4f\n", 1.23E-2);
    return 0;
}

运行结果:

radius = 3.5
area = 38.4845
3.2e2 = 320.0
1.23E-2 = 0.0123

解释:

radius、pi 和 area 都是 double,计算过程采用浮点运算。%.1f 表示输出小数点后 1 位,%.4f 表示输出小数点后 4 位。

格式中的精度控制的是显示效果,不会修改变量中保存的值。浮点数的精度有限,很多十进制小数无法被二进制浮点数精确表示,因此也不能把“显示了很多位”理解为“这些位都准确”。

在 printf 中,float 参数会先提升为 double,所以二者都可以用 %f 输出。long double 应使用 %Lf。

四、字符型:字符背后也有数值

char 属于整数类型。它可以用来保存字符编码,因此字符既可以显示为符号,也可以参与算术运算。

在 ASCII 字符集中:

字符

编码值

'A'

65

'a'

97

'0'

48

'6'

54

' '

32

字符 '6' 和整数 6 不同。前者表示数字字符的编码,后者表示数值 6。

示例 4:输出字符、编码和后一个字符

#include <stdio.h>

int main(void)
{
    char upper = 'A';
    char lower = 'g';
    char digit = '6';
    char space = ' ';

    printf("%c has code %d\n", upper, upper);
    printf("next of %c is %c\n", upper, upper + 1);
    printf("next of %c is %c\n", lower, lower + 1);
    printf("%c has code %d\n", digit, digit);
    printf("digit value = %d\n", digit - '0');
    printf("space has code %d\n", space);
    return 0;
}

在 ASCII 兼容环境下,运行结果为:

A has code 65
next of A is B
next of g is h
6 has code 54
digit value = 6
space has code 32

解释:

%c 把参数显示为字符,%d 把参数显示为整数。对于这里的字符,参与运算或传给 printf 时会提升为 int。

ASCII 中的大写字母和小写字母分别连续排列,所以 'A' + 1 对应 'B','g' + 1 对应 'h'。这并不意味着所有字符都能按这种方法寻找“下一个字符”。

数字字符连续排列,因此当 digit 确实是 '0'~'9' 时,digit - '0' 可以得到对应的整数值。

单引号、双引号与空字符

这些写法很容易混淆:

写法

含义

'A'

字符常量

"A"

字符串,包含 'A' 和结束标记 '\0'

' '

空格字符

'\0'

值为 0 的空字符

""

空字符串,仍包含结束标记 '\0'

''

非法写法,不能表示空字符

字符串不是单个 char。例如 char ch = 'A'; 合法,但不能用 char ch = "A"; 来保存一个字符。

常见转义序列包括 \n(换行)、\t(水平制表)、\\(反斜杠)、\'(单引号)和 \"(双引号)。例如 printf("Hello\n"); 会在输出 Hello 后换行。

五、自动类型转换:混合运算怎样进行

不同类型的数据可以出现在同一个表达式中。编译器会按照规则转换操作数,再执行计算。

对于初学者,先掌握下面几种常见情况:

  1. char、short 参与算术运算时,通常先提升为 int。

  2. int 与 double 运算时,整数转换为 double。

  3. float 与 double 运算时,float 转换为 double。

这不是所有规则的完整列表。有符号和无符号整数混合时,还涉及类型等级及可表示范围,不能仅用“低精度变成高精度”概括。

示例 5:为什么平均值会少了小数

#include <stdio.h>

int main(void)
{
    int a = 5;
    int b = 20;
    double average1 = (a + b) / 2;
    double average2 = (a + b) / 2.0;

    printf("average1 = %.2f\n", average1);
    printf("average2 = %.2f\n", average2);
    printf("3 + 3.5 = %.1f\n", 3 + 3.5);
    return 0;
}

运行结果:

average1 = 12.00
average2 = 12.50
3 + 3.5 = 6.5

解释:

对于 average1,计算顺序是:

  1. a + b 得到整数 25。

  2. 25 / 2 的两个操作数都是整数,执行整数除法,得到 12。

  3. 把 12 转换为 double,赋给 average1,得到 12.0。

对于 average2,除数 2.0 是 double,所以整数 25 先转换为 double,再进行浮点除法,得到 12.5。

把计算结果存入浮点变量,并不会让之前的整数除法自动变成浮点除法。 必须在除法发生之前,让相关操作数具有浮点类型。

本例中的整数很小。如果 a、b 很大,a + b 还可能先发生整数溢出。需要考虑这种情况时,可在加法前转换,例如 ((double)a + b) / 2.0,同时留意浮点数的精度限制。

六、赋值转换:以目标变量的类型为准

赋值时,如果右侧表达式的类型与左侧变量不同,会发生类型转换。

示例 6:浮点数赋给整数会怎样

#include <stdio.h>

int main(void)
{
    double x = 5;
    int positive = 3.9;
    int negative = -3.9;
    int code = 'A';

    printf("x = %.1f\n", x);
    printf("positive = %d\n", positive);
    printf("negative = %d\n", negative);
    printf("code = %d\n", code);
    return 0;
}

运行结果:

x = 5.0
positive = 3
negative = -3
code = 65

解释:

  • double x = 5; 把整数 5 转换为浮点值 5.0。

  • int positive = 3.9; 舍去小数部分,得到 3。

  • int negative = -3.9; 同样舍去小数部分,得到 -3。

  • 在 ASCII 兼容环境下,'A' 的编码值为 65。

浮点数转整数时采用向零截断,不是四舍五入,也不是一律向下取整。以上结论以截断后的值能够由目标整数类型表示为前提;不能依靠超范围转换得到有效结果。

赋值转换不会改变变量的声明类型。positive 声明为 int,即使右侧写着小数,它仍然是整数变量。编译器可能对这种会丢失小数部分的转换给出警告。

七、强制类型转换:转换的位置很重要

强制类型转换的常见写法是:

(类型名) 表达式

例如 (double)a 表示把 a 的值转换为 double,供当前表达式使用。它不会把变量 a 本身永久改成 double。

示例 7:先转换与后转换的区别

#include <stdio.h>

int main(void)
{
    int a = 5;
    int b = 2;

    double result1 = (double)a / b;
    double result2 = (double)(a / b);
    double result3 = a / (double)b;

    printf("result1 = %.1f\n", result1);
    printf("result2 = %.1f\n", result2);
    printf("result3 = %.1f\n", result3);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

运行结果:

result1 = 2.5
result2 = 2.0
result3 = 2.5
a = 5, b = 2

解释:

表达式

计算过程

结果

(double)a / b

先把 a 转为浮点值,再做浮点除法

2.5

(double)(a / b)

先做整数除法得到 2,再转为浮点值

2.0

a / (double)b

先把 b 转为浮点值,再做浮点除法

2.5

对于这里的两个整数,要得到实数商,只需在除法前把其中一个操作数转换为浮点型。

(float)a / b 也体现相同原理。这里选用 double,以便使用通常更高的计算精度。无论选哪一种,转换都不能恢复已经在整数除法中丢失的小数部分。

八、综合示例:计算两个整数的和、差、积、商

下面把变量声明、整数运算、强制转换和格式化输出放进一个程序。

示例 8:四则运算

#include <stdio.h>

int main(void)
{
    int a = 9;
    int b = 4;
    int sum = a + b;
    int difference = a - b;
    int product = a * b;

    printf("%d + %d = %d\n", a, b, sum);
    printf("%d - %d = %d\n", a, b, difference);
    printf("%d * %d = %d\n", a, b, product);

    if (b != 0) {
        double quotient = (double)a / b;
        printf("%d / %d = %.2f\n", a, b, quotient);
    } else {
        printf("Division by zero is not allowed.\n");
    }

    return 0;
}

运行结果:

9 + 4 = 13
9 - 4 = 5
9 * 4 = 36
9 / 4 = 2.25

解释:

本例中,和、差、积都可以用 int 保存,商使用 double 保存。(double)a / b 在除法前转换操作数,因此能够得到 2.25。

if (b != 0) 用来检查除数是否为零。这里出现的 != 表示“不等于”。若将 b 改为 0,程序会输出提示,而不会执行除法。

这组数据较小,不会发生整数溢出。如果把程序用于更大范围的数据,还需要检查加法、减法和乘法的结果是否超出类型范围。

九、常见问题速查

常见误解

正确理解

long 一定占 4 字节

类型大小与实现有关,可用 sizeof 查询

3.14 是 float 常量

默认是 double,3.14f 才是 float

3.14L 是 double 常量

它是 long double 常量

'' 可以表示空字符

'' 非法,空字符写成 '\0'

字符 '6' 就是整数 6

在 ASCII 中,字符 '6' 的编码是 54

结果用 double 保存,就一定有小数

还要看前面的表达式是否执行整数除法

(double)(a / b) 能保留整数商的小数

转换发生得太晚,应在除法前转换操作数

浮点数转整数会四舍五入

在可表示范围内,转换舍去小数部分,向零截断

强制转换会改变原变量的类型

它改变当前表达式中使用的值的类型,不改变原变量

还要区分 printf 和 scanf 的格式规则:printf 输出 float、double 都可使用 %f;如果用 scanf 读取,float * 对应 %f,double * 对应 %lf。不要直接照搬输出函数的格式到输入函数中。

十、课后练习

先判断下面各表达式的结果,再写程序验证:

9 / 4
9 / 4.0
(double)9 / 4
(double)(9 / 4)
(int)3.9
(int)-3.9
'8' - '0'

参考答案依次为:2、2.25、2.25、2.0、3、-3、8。

还可以修改综合示例,把 a 和 b 分别改成 5 和 2,再把 b 改成 0,观察正常计算与除零检查的区别。

遇到计算结果与预期不一致时,依次检查三个位置:操作数的类型、运算发生前的转换、结果赋值时的转换。把这三个阶段分清楚,整数除法和类型转换就容易理解了。