C语言类型的本质(一):前言

在一切的开始,内存只是一片荒芜,后修真者编译天地,便有了今天的锦绣山河

一块没有使用的内存就像是一片荒凉的大地,为了方便管理,人们进行区域划分,便有了良田千顷,房屋万座,几乎在每一门编程语言中都有类型这个概念,每一种类型都有特定的大小,内存在被使用时就被不同的类型分割成了大大小小的不同的块。C语言中基本数据类型有int、char等,在基本数据类型的基础上又可以任意组合生成结构体类型。很多人在第一次接触编程时,往往很难理解类型是一个怎样的概念,我个人觉得称之为类型不是很合适,我更喜欢称之为格式,本文会从格式的角度出发,重新解读数据类型的概念。

在不同的平台上同一种类型也会有不同的大小,所以抛开平台谈类型无异于耍流氓,本文使用的环境为windows 64位。

编程语言中的各种类型本质上是为了区分不同的数据,所以格式的定义第一步就是要如何区分数据。数据在内存上最直观的属性就是空间占用大小,int类型空间占用为4个字节,char类型空间占用为1个字节,所以它们不是同一种类型。但是仅仅以空间占用大小区分数据还是太过模糊了,float类型空间占用为也为4个字节,那int和float就是同一种格式吗?显然这是不合逻辑的。

所有的数据在内存中都表示为一串二进制码,为了能得到正确的结果,在数据存储时,会按照标准约定进行存储。例如:int为整型数据,float为浮点型数据,IEEE标准规定int整型数据的最高位表示正负,剩余位表示存储值,而float浮点类型虽然最高位也表示正负,但是剩余位还要再分为底数部分和指数部分(碍于篇幅具体如何存储本文不在赘述,感兴趣者可自行查阅),可以将这种约定理解成存储格式,相应的,读取时也会按照约定的读取格式进行读取。存储格式和读取格式就像钥匙和锁的关系,是一一对应的,以A格式存储的数据只能通过A读取格式得到正确的结果,使用其它种读取格式只能得到没有意义的数据。

通过上面的分析可以发现,虽然int和float有相同的空间占用,但是它们拥有不同的存储格式和读取格式,所以它们不是同一种类型,至此格式的基本定义确定如下:

格式有三个属性,空间占用描述了在内存中需要的空间大小,存储格式描述了以何种约定存储数据,读取格式描述了以何种约定读取数据。

为方便表示,可以设想编译器中存储着如下结构:

code c
{
    空间占用,
    存储格式,
    读取格式,
}

例如int格式在编译器中可以描述为:

code c
{
    空间占用: 4,
    存储格式: int存储约定(如果是正数,最高位赋值为0,否则赋值为1,然后将数据的补码存入剩余部分),
    读取格式: int读取约定(根据最高位判断数据正负,再根据剩余部分的补码求出原码),
}

夜阑卧听风吹雨,铁马冰河入梦来

你是否还记得初识C语言时,与它征战,与它厮杀,现在想想看是不是仍为自己踏入这个世界而感到骄傲呢?

先看一个很复杂的表达式:

code c
int x = 5;

简单来说这个表达式就是开辟了一块4个字节的空间,里面储存了数字5,通过访问x可以得到这块空间存储的数字,现在我们尝试用格式的眼光重新认识曾经熟悉的C语言。
编译器首先进行语法分词,得到了["int", "x" , "=", "5"]

  1. 处理int
    编译器找到int格式的定义,根据int的空间占用分配了分配4个字节的空间。
code c
{
    空间占用: 4,
    存储格式: int存储约定(如果是正数,最高位赋值为0,否则赋值为1,然后将数据的补码存入剩余部分),
    读取格式: int读取约定(根据最高位判断数据正负,再根据剩余部分的补码求出原码),
}
  1. 处理x
    变量继承了格式的所有属性,并绑定了分配的内存地址,x就像是这样:
code c
x  = {
    空间占用: int->空间占用,   // 4
    存储格式: int->存储格式,
    读取格式: int->读取格式,
    存储空间指针: 指向第一步分配好的空间的首地址,
}
  1. 处理=5
    等号操作符可以直接改变等号左值内存地址中的数据,此处等号会调用x->存储格式将数字5存入x->存储空间指针指向的内存,这样就完成了对内存的初始化。读取x时,编译器会调用x->读取格式去读取x->存储空间指针指向的内存得到数字5。

仔细对比变量x和int格式的结构会发现变量x中只是多一了一个存储空间指针属性,类似于继承关系,截止到目前,格式的概念好像并没有解决什么问题,反而让事情变得复杂起来。公子莫急,下面我会用几个实际问题带你认识格式的魅力。

为了便于理解读取器和存储器,看一个简单的例子,这个例子自定义了一个分子格式:

code c
// 自定义结构
typedef struct _Fraction {
    int numerator;
    int denominator;
} * Fraction;

// int/int类型的存储器
void setFractionInt(int numerator, int denominator, Fraction fraction) {
    if (fraction == NULL) {
        return;
    }

    int remainder = 0;
    fraction->numerator = numerator;
    fraction->denominator = denominator;
    
    do {
        remainder = numerator % denominator;
        numerator = denominator;
        denominator = remainder;
    } while (remainder != 0);
    
    fraction->numerator /= numerator;
    fraction->denominator /= numerator;
}

// double类型的存储器
void setFractionDouble(double number, Fraction fraction) {
    if (fraction == NULL) {
        return;
    }
    // 只保留小数点后三位
    setFractionInt(number * 1000, 1000, fraction);
}

// 字符串类型的存储器
void setFractionStr(char * str, Fraction fraction) {
    if (str == NULL) {
        return;
    }

    int numerator,  denominator;
    sscanf(str, "%d/%d", &numerator, &denominator);
    setFractionInt(numerator, denominator, fraction);
}

// double类型读取器
double getFraction(Fraction fraction) {
    if (fraction == NULL) {
        return 0;
    }
    return (double)fraction->numerator / (double)fraction->denominator;
}

// 字符串类型读取器
char * getFractionStr(Fraction fraction, char * result) {
    if (fraction == NULL ||  result == NULL) {
        return 0;
    }
    sprintf(result, "%d/%d", fraction->numerator, fraction->denominator);
    return result;
}

int main() {
    char result[10];
    
    Fraction x = (Fraction)malloc(sizeof(struct _Fraction));
    setFractionStr("30/6", x);
    printf("x: %s = %f\n", getFractionStr(x, result), getFraction(x));

    Fraction y = (Fraction)malloc(sizeof(struct _Fraction));
    setFractionStr("1/2", y);
    printf("y: %s = %f\n", getFractionStr(y, result), getFraction(y));

    Fraction z = (Fraction)malloc(sizeof(struct _Fraction));
    setFractionDouble(getFraction(x) * getFraction(y), z);
    printf("z: %s = %f\n", getFractionStr(z, result), getFraction(z));
    
    // 这里卖个关子,可以思考一下,后文会讲到
    int arr[] = { 100, 350 };
    printf("arr: %s = %f\n", getFractionStr(arr, result), getFraction(arr));
    
    free(x);
    free(y);
    free(z);
    return 0;
}

输出结果:

x: 5/1 = 5.000000
y: 1/2 = 0.500000
z: 5/2 = 2.500000
arr: 100/350 = 0.285714

上面的例子中给Fraction格式定义了三种存储器,两种读取器,这是从代码层面模拟的。C语言的基本变量也会有这样那样的存储器、读取器,只不过那些不需要用户再去手动写。

为了行文方便,后续会伪造一些编译器的概念,纯属为了便于理解,与实际编译过程不相干,如有雷同,算我学到了。感兴趣的可以看一看编译原理和计算机组成原理,真的很浪漫。