在一切的开始,内存只是一片荒芜,后修真者编译天地,便有了今天的锦绣山河
一块没有使用的内存就像是一片荒凉的大地,为了方便管理,人们进行区域划分,便有了良田千顷,房屋万座,几乎在每一门编程语言中都有类型这个概念,每一种类型都有特定的大小,内存在被使用时就被不同的类型分割成了大大小小的不同的块。C语言中基本数据类型有int、char等,在基本数据类型的基础上又可以任意组合生成结构体类型。很多人在第一次接触编程时,往往很难理解类型是一个怎样的概念,我个人觉得称之为类型不是很合适,我更喜欢称之为格式,本文会从格式的角度出发,重新解读数据类型的概念。
在不同的平台上同一种类型也会有不同的大小,所以抛开平台谈类型无异于耍流氓,本文使用的环境为windows 64位。
编程语言中的各种类型本质上是为了区分不同的数据,所以格式的定义第一步就是要如何区分数据。数据在内存上最直观的属性就是空间占用大小,int类型空间占用为4个字节,char类型空间占用为1个字节,所以它们不是同一种类型。但是仅仅以空间占用大小区分数据还是太过模糊了,float类型空间占用为也为4个字节,那int和float就是同一种格式吗?显然这是不合逻辑的。
所有的数据在内存中都表示为一串二进制码,为了能得到正确的结果,在数据存储时,会按照标准约定进行存储。例如:int为整型数据,float为浮点型数据,IEEE标准规定int整型数据的最高位表示正负,剩余位表示存储值,而float浮点类型虽然最高位也表示正负,但是剩余位还要再分为底数部分和指数部分(碍于篇幅具体如何存储本文不在赘述,感兴趣者可自行查阅),可以将这种约定理解成存储格式,相应的,读取时也会按照约定的读取格式进行读取。存储格式和读取格式就像钥匙和锁的关系,是一一对应的,以A格式存储的数据只能通过A读取格式得到正确的结果,使用其它种读取格式只能得到没有意义的数据。
通过上面的分析可以发现,虽然int和float有相同的空间占用,但是它们拥有不同的存储格式和读取格式,所以它们不是同一种类型,至此格式的基本定义确定如下:
格式有三个属性,空间占用描述了在内存中需要的空间大小,存储格式描述了以何种约定存储数据,读取格式描述了以何种约定读取数据。
为方便表示,可以设想编译器中存储着如下结构:
{ 空间占用, 存储格式, 读取格式, }
例如int格式在编译器中可以描述为:
{ 空间占用: 4, 存储格式: int存储约定(如果是正数,最高位赋值为0,否则赋值为1,然后将数据的补码存入剩余部分), 读取格式: int读取约定(根据最高位判断数据正负,再根据剩余部分的补码求出原码), }
夜阑卧听风吹雨,铁马冰河入梦来
你是否还记得初识C语言时,与它征战,与它厮杀,现在想想看是不是仍为自己踏入这个世界而感到骄傲呢?
先看一个很复杂的表达式:
int x = 5;
简单来说这个表达式就是开辟了一块4个字节的空间,里面储存了数字5,通过访问x可以得到这块空间存储的数字,现在我们尝试用格式的眼光重新认识曾经熟悉的C语言。
编译器首先进行语法分词,得到了["int", "x" , "=", "5"]
,
- 处理
int
编译器找到int
格式的定义,根据int的空间占用分配了分配4个字节的空间。
{ 空间占用: 4, 存储格式: int存储约定(如果是正数,最高位赋值为0,否则赋值为1,然后将数据的补码存入剩余部分), 读取格式: int读取约定(根据最高位判断数据正负,再根据剩余部分的补码求出原码), }
- 处理
x
变量继承了格式的所有属性,并绑定了分配的内存地址,x就像是这样:
x = { 空间占用: int->空间占用, // 4 存储格式: int->存储格式, 读取格式: int->读取格式, 存储空间指针: 指向第一步分配好的空间的首地址, }
- 处理
=
和5
等号操作符可以直接改变等号左值内存地址中的数据,此处等号会调用x->存储格式
将数字5存入x->存储空间指针
指向的内存,这样就完成了对内存的初始化。读取x
时,编译器会调用x->读取格式
去读取x->存储空间指针
指向的内存得到数字5。
仔细对比变量x和int格式的结构会发现变量x中只是多一了一个存储空间指针
属性,类似于继承关系
,截止到目前,格式的概念好像并没有解决什么问题,反而让事情变得复杂起来。公子莫急,下面我会用几个实际问题带你认识格式的魅力。
为了便于理解读取器和存储器,看一个简单的例子,这个例子自定义了一个分子格式:
// 自定义结构 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语言的基本变量也会有这样那样的存储器、读取器,只不过那些不需要用户再去手动写。
为了行文方便,后续会伪造一些编译器的概念,纯属为了便于理解,与实际编译过程不相干,如有雷同,算我学到了。感兴趣的可以看一看编译原理和计算机组成原理,真的很浪漫。