一、类型的含义

1) 什么是类型

一个计算机程序在运行时,其用到的任何变量(variable)、对象(object)、函数(function)等等(以下统称对象)都要占据一定字节数的内存。 那么,一个对象占据的字节数(简称对象的大小)由什么来决定呢? 答案是:它的类型(type)

一个对象的类型决定了以下几件事:

  1. 对象的大小
  2. 对象的内存布局
  3. 对象可以参与的运算

2) 类型的分类

C/C++的类型一般分为两大类。

  1. 内建(built-in)类型 语言本身提供的类型,例如:charintdoublebool等等。

  2. 用户自定义类型 由内建类型、已有用户自定义类型复合而成的类型,例如:指针类型、数组类型、结构/联合/类类型、枚举类型等等。

除此之外,通常有如下说法:

  • 内建类型、指针类型、枚举类型等合称为简单类型,其它的称为复合类型
  • 字符型、所有整型、浮点型、布尔型、枚举型合称为数值类型
  • 字符型、所有整型、布尔型合称为整数类型,因为它们的内存布局都采用整数的布局模式。

枚举类型的内存布局也与整数一样。但这并不能说明枚举类型和整型等价,尤其是在C++中。

二、对象(类型)的大小

1) 简单类型

简单类型对象的大小(字节数)根据语言的标准由编译器实现而定。常用简单类型的字节数如下表所示:

类型 32位 64位
char 1 1
int 4 4
long 4 8
T* 4 8

实际上,我们编写的程序基本上没有特别精度要求。因此,可以不必去死记硬背类型的大小,只要大概了解类型的表示范围即可。例如:

  • int:∓21亿,10个10进制数字
  • long:32位系统中同int;64位系统中是19个10进制数字
  • float:∓1038,6个10进制数字
  • double:∓10308,15个10进制数字

一般地,表示范围大的类型称为长类型,反之则是短类型

虽然float类型的表示范围相对较大,但因其内存布局的原因,可能导致计算精度不能保证。例如:

#include <stdio.h>

int main() {
    int si = 0;
    long sl = 0;
    float sf = 0;
    double sd = 0;

    for (int i = 0; i < 5794; ++i) {
        si += i;
        sl += i;
        sf += i;
        sd += i;
    }

    printf("si=%d\nsl=%ld\nsf=%f\nsd=%lf\n", si, sl, sf, sd);

    return 0;
}

结果是:

si=16782321
sl=16782321
sf=16782320.000000
sd=16782321.000000

可以看到,累加到一个不大的数5794就使float结果开始有精度损失了!

感谢李冠男先生,他提出的问题给了本程序灵感。

2) 复合类型

  1. 数组类型:基类型大小 x 所有维度长度的积

  2. 结构/类类型:≥ 所有成员的大小

    因计算机的特性,成员之间可能被填充一些无用字节,所以最好用sizeof()运算符求大小。

  3. 联合类型:最大成员的大小

三、对象的内存布局

1) 简单类型

  1. 整数类型/指针类型

    内存布局是平的,即除了符号位(如果有的话),其余的每一位都是数值的有机组成部分。

  2. 浮点类型

    内存布局是结构化的,即内存是分段的,并有一些位有特殊含义。

    请参阅IEEE浮点格式。这个格式能够解释前面float数据精度损失的原因。

2) 复合类型

  1. 结构/类类型

    内存布局是结构化的,一般是按成员的声明顺序分配的。成员的内存布局由其(基)类型决定。

  2. 数组类型

    连续存储。

四、对象参与的混合运算

从原则上来讲,如果有数值运算:

a @ b

那么对象ab的类型应该是一样的,以保证结果类型的确定性。

如果不一样,例如a是短类型,b是长类型,那么编译器一般会采用类型提升方式,默认将短类型转换为长类型以保证计算的正确性。这种现象常被称为隐式类型转换

一个特别的场合是复制。短类型向长类型复制可能面临精度损失的问题。例如:

int a = 1023456789;
float f = a; //可能有精度损失。一般会有编译警告。

除了赋值,形参实参结合也可能是复制。

指针的运算比较特殊,这里不再赘述。请参阅文章”指针和数组“。

复合类型(尤其是C++的类类型)的运算比较复杂,这里就不展开讨论了。