2.2 数据类型的本质
2.2.1 sizeof()的重要作用
问题描述:
曾经有这样一个小笑话,说的是一个人晚下班回家,一个民警迎面巡逻而来。突然对这个人大喊:站住!民警:int类型占几个字节?这个人:4个。民警:你可以走了。这个人感到很诧异。问道:为什么问这样的问题?民警:深夜还在街上走,幸苦又寒酸的样子,不是小偷就是程序员……
这个笑话挺有意思的,但是却出了一点小误会。那么这里的一个老生常谈的问题就是int是占用了4个字节吗?
这个问题答案是否定的,世界上int有时占用的4个字节,有时占用的是2个字节。
这个值很多人都争论过,到底和什么有关?编译器,操作系统,中央处理器,好像都可以扯上一点关系。
从以前的4位计算机发展到现在的32位计算机,甚至64计算机,操作系统也是一直都在变化。为了适应这种不断的变化,C语言并没有硬性的规定int字节的长度,但是这样也导致了不同系统上,程序的可移植性大大的降低。在编写程序的时候,如果要考虑移植性,就尽量要避免直接使用int类型。
这样绕了一圈,我们虽然知道了为什么int字节没有一个标准,但是还是有这样一个问题,我怎么知道我现在正在用的编译器中int的长度呢?
这里有一个绝对正确的选择,那就是最好使用sizeof来确定。在我学习C语言的很长一段时间里,一直都认为sizeof是一个函数,因为看起来实在很难和+,-,*,/这些运算符归到一类中去。但是sizeof的确是一个运算符,使用方法是sizeof(t),可以返回t占用的字节数。
实例分析:使用sizeof可以解决我们之前的那个关于int占几个字节的问题了。
Sizeof的第一种用法就是用来计算某一种数据类型占用的字节数。
语法结构是sizeof(Type)
比如:
#include <stdio.h>
int main(void){
printf("%d\n", sizeof(int));
}
这个程序就可以输出你的计算机里面int占用的字节数。
一般的数据类型分为两种,一种是C标准明确规定了字节数的类型,如最为常见的char,unsigned char,signed char占用的字节数都是1。另一种是C标准并没有明确标明的字节数的类型,它们的长度和编程环境有关。比如,int,signed int,short int,unsigned short,long int,unsigned long,float,double等等类型。同时指针的字节长度也是属于第二种类型的,一般为4个字节。
sizeof的第二种用法是用来计算变量和常量占用的字节数。需要注意的是当采用这种用法的时候,sizeof后面无论有没有括号都是正确的,只是大多数程序员都习惯了要加上括号。
#include <stdio.h>
int main(void){
printf("The sizeof is \'a\' %d\n", sizeof('a'));
printf("The sizeof is \"a\" %d\n", sizeof("a"));
printf("The sizeof is \' \' %d\n", sizeof(" "));
printf("The sizeof is \" \" %d\n", sizeof(' '));
return 0;
}这样执行的结果输出为
The sizeof is 1
The sizeof is 2
The sizeof is 2
The sizeof is 1
这里需要注意的是两点,一是双引号包含的常量长度要比看到的多1,这是因为字符串的末尾都有一个看不见的\0,表示字符串的结束,这样做的结果就是使得字符串的占用的字节数多了一个。另外一点需要注意的是,空格也是一个字符,也需要占用一个字节。
但是sizeof是在编译阶段运行,因此不能sizeof来计算函数或者未知存储大小的数据类型的大小。
另外由于sizeof计算的最小单位是字节,因此也不能用来计算位字段类型。
同样不能用来计算void的长度。
同样一个数据占用的字节数也经常会出现在一些企业的笔试题目中,而其中出现最多的往往却是高级数据类型的字节数问题。
比如下面的这个结构体:
struct person1{
int student_number;
int ID_card;
};
占用了几个字节呢,(在设定该编译器的int为4个字节的前提下)该结构体中包含了两个int,所以一个person1类型占用的字节是8个。
深入剖析:
当然,很少会有企业会好心的出上面的题目,至少他们会把题目改成下面这样
struct person2{
char Name;
int ID_card;
};
上面的这个结构体person2占用了几个字节呢?一个int应该是4个,一个char应该是1个,答案好像一下就跃然纸面,5个!
如果真的这么回答的话,我们就中了出题者的陷阱了,或者说根本没有理会到出题的考点。
这道题表面考察的数据类型的长度,实际上也间接考察了计算体系结构方面的内容。
我们以16位机为例,通常cpu是有16位的地址总线,那么一次可以读出16位数据,一个字节是8位,那么一次就可以读出2个字节,正好是一个int型数据。0,1地址为一个数据,2,3是一个数据。Cpu会把内存单元划分为奇数单元和偶数单元,因为是16位机,所以没必要把单位设为最小的字节,假设cpu为偶单元寻址。设计算机数数的时候,是0,2,4,6,8这样。
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
如果这里面的数据都是2位的话,那么我们读数据的时候读2,3就可以了。
但是如果有一个数据只占用了一个字节2的话,那么接下来的另一个类型占用了3,4的话,那么计算机还是只能先读取2,3这两个地址,然后在读取4,5这个地址,最后交换到我们的变量。从而占用了两个总线周期。
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
为了不出现上面的情况,我们就尽量让所有的数据都对齐,如果对不起的话,就采用空白补充。说白了这种方法就是我们抗战时期采用的“以空间换时间”的方法。
总结
到底采用什么样的对齐方式其实是有编译器决定的。不同的编译器采用了不同的对齐方式,我们所做的只需要知道对齐的原因,以及经常使用到的编辑器的对齐方式就可以了。关于sizeof运算的优先级别以及复杂类型的sizeof运算,我们会在以后的章节中给出说明。
扩展:
由于我们在编写程序的时候,经常会处理不同类型的数据,比如可能需要计算一个家庭一年的收入支出,也可能需要计算地球围太阳公转的周长,显然这两者计算需要的数据相差极大,如果分配给相同的内存,那就很不实际了。这样的话,作为使用者(程序员)实际上又多了一个工作,需要每次在申请仓位的时候,还需要预先估计使用的仓位数量,然后再提出申请。这就额外增加了程序员的负担。
为了解决这个问题,编译器和程序员也做出了一个规定,不需要程序员来指明需要使用的字节数量,只需要程序员说明使用的数据类型就行了,这个约定中规定了我们经常使用的所有数据类型:
int,Char,Float,Double
以及它们和Signed,Unsigned和long,short的组合。
(这里需要注意一点,很多学过C++语言的人,会经常性的将C++中才出现的布尔型也认为是C的基本类型)。
对于不同的数据类型必须至少具有以下两点区别中的一个
1长度不同。
2存储的方式不同。
在不同的C编译器上,同一数据类型的可能是不同的,因为C标准没有做出规定。
有时我们可能会忘记标明类型,那么系统会默认为是int类型,因为C标准中指出了这点。所以我们经常有人看到程序的这种写法:
Main(void){
……
}
这其实与
int Main(void){
……
}
是一样的。
不同的数据类型必须至少具有以下两点区别中的一个
1长度不同。
2存储的方式不同。
这里介绍一个typedef,有些类似#define的用法,但是两者在不同的阶段进行处理的,#define是在预编译阶段,typedef是在编译阶段。这样做的好处就是,假如我们是在一个int为2个字节的编译环境下完成的程序,但是现在需要在另一个int为4个字节的编译环境下执行,在这个编译环境中,short int为2个字节。如果直接移植的话,需要将所有的int都找到,然后替换为short,这样做工作量很大,而且容易出现错误。
如果在之前的程序中我们定义过了
typedef int int16;
并且在所有需要定义为int的地方都是采用int16的方式。
那么在移植以后,需要做的事只是将原来语句中的int换成short就可以了。
typedef short int16;
防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。通常我们可以采用如下的定义:
typedef unsigned char boolean; /* Boolean value type. */
typedef unsigned long int uint32; /* Unsigned 32 bit value */
typedef unsigned short uint16; /* Unsigned 16 bit value */
typedef unsigned char uint8; /* Unsigned 8 bit value */
typedef signed long int int32; /* Signed 32 bit value */
typedef signed short int16; /* Signed 16 bit value */
typedef signed char int8; /* Signed 8 bit value */