5.1.3 认识sizeof和乘性运算符
如果一个数组的声明中仅有初始化器而未指定数组的元素数量,则数组的元素数量由初始化器决定。具体来说,在所有被初始化的元素中,总有一个下标最大,数组的元素数量由这个元素的下标来确定。
如下面的声明所示,对于数组a,初始化器1和2分别用于初始化下标为0和1的元素,初始化器[199] = 8和[99] = 7分别用于初始化下标为199和99的元素。其中最大的下标是199,所以数组a有200个元素。
/*************c0504.c************/ int main(void) { int a [] = {1, 2, [199] = 8, [99] = 7}, b [] = {1, 2, 3};
unsigned int siza = 0, sizb = 0, numa = 0, numb = 0; siza = sizeof a; //S1 sizb = sizeof b; //S2 numa = sizeof a / sizeof(int); //S3 numb = sizeof b / sizeof(int); //S4 }
对于数组b,初始化器1、2和3分别用于初始化下标为0、1和2的元素,最大的下标是2,所以该数组有3个元素。
现在的问题是,数组a和数组b到底有多大,占用多大的内存空间?它们的元素数量真的如上所说吗?为了加以验证,接下来,我们声明了4个变量。siza和sizb分别用于保存数组a和数组b的大小,以字节计;numa和numb分别用于保存数组a和数组b的元素个数。
为了获得一个变量或者类型的大小,C语言从发明之初就引入了sizeof运算符。和我们曾经学过的_Alignof一样,它看起来像是函数,但并不是函数。sizeof不单单是C语言里的运算符,也是关键字,Sizeof、SizeOf等都是错误的拼写。
sizeof运算符只需要一个右操作数,可以是表达式,也可以是用圆括号“( )”括住的类型名:
sizeof表达式 sizeof(类型名)
由运算符sizeof和它的操作数组成的表达式称为尺寸表达式,或者叫sizeof表达式。显然,sizeof 3、sizeof 3ULL、sizeof(char)、sizeof(int)、sizeof(int*)和sizeof(int(*)(void))都是合法的尺寸表达式。
运算符sizeof的结果是其操作数的大小,以字节计,结果的类型是一个无符号整数类型,具体是哪种无符号整数类型,由C实现自行决定,但必须足够大,以保证能够容纳所有类型的大小。
如果sizeof运算符的操作数是一个类型名,则它返回类型的大小——类型可用于声明变量,它决定了变量的大小,这个大小可视为类型的大小。因此,表达式sizeof(int*)返回“指向int的指针”类型的大小;表达式sizeof(int(*)(void))返回“指向函数的指针”类型的大小;表达式sizeof(char)和sizeof(int)分别返回char类型和int类型的大小。
如果sizeof运算符的操作数是一个表达式,则它仅抽取表达式的类型。这里,3是整型常量表达式,其类型为int,故表达式sizeof 3的结果是int类型的大小;3ULL也是整型常量表达式,其类型为unsigned long long int,故表达式sizeof 3ULL的结果是unsigned long long int类型的大小。
如果运算符sizeof的操作数是一个代表变量的表达式,即,左值,则不执行左值转换而直接抽取左值的类型,并返回该类型的大小。在语句S1中,表达式sizeof a的结果是变量a的大小,以字节计。尽管a是个左值,但并不执行左值转换。那么,sizeof a的结果是多少呢?变量a是一个数组,而数组的大小取决于元素的数量和每个元素的大小,或者说是元素的数量乘以每个元素的大小。然而,sizeof并不是通过访问数组a来得到这个总大小的,相反,它仅仅是依靠a的类型来计算的。
类似地,语句S2则用于取得变量b的大小;语句S3的作用是计算数组a的元素个数,它的做法是用数组的大小除以每个元素的大小。在这里,运算符sizeof的优先级最高,运算符/次之,运算符=的优先级最低。
运算符/属于乘性运算符。乘性运算符都是二元运算符,也就是需要一左一右两个操作数,它们包括*、/和%。二元*运算符的结果是两个操作数的乘积;运算符/的结果是其左操作数除以右操作数的商,如果两个操作数都是整数,则运算符/的结果是一个舍弃了小数部分的整数;运算符%的两个操作数只能是整数类型,其结果也是一个整数,而且是其左操作数除以右操作数之后所得到的余数。
也就是说,表达式15 * 6的结果是90;表达式15 / 6的结果是2;表达式15 % 6的结果是3。
再来看语句S3,表达式sizeof a得到数组a的大小,以字节计,它是所有元素大小的总和。因为元素的类型是int,故每个元素的大小就是int类型的大小,所以我们是用表达式sizeof(int)来得到每个元素的大小的。最后,这两者相除,就是元素的数量,我们将它赋给变量numa。同样地,语句S4也用这种方法来计算数组b的元素数量。
要想知道数组a、b的大小和元素的数量,最可靠的办法就是在调试器里观察变量siza、sizb、numa和numb的值。将断点设置在组成函数体的右花括号“}”所在的那一行,然后运行程序并用p命令打印变量的值:
(gdb)p {siza, sizb, numa, numb} $1 = {800, 12, 3, 200} (gdb)p sizeof(int) $2 = 4 (gdb)
因为变量siza、sizb、numa和numb的类型都相同,可以用集合的形式打印。p命令不限于打印变量的值,而是可以打印任何表达式的值,所以最后一个调试命令打印了int类型的大小,在我的机器上,每个int类型的变量占据4个字节的内存空间。
在带有花括号的初始化器中,被花括号包围的部分称为初始化器列表。在初始化器列表的末尾可以多添加一个逗号“,”。例如:
int a [5] = {1, 2, 3, 4, 5, };
这个多余的逗号是无害的,引入它的动机据说可能和源代码的版本控制有关。在大公司和大项目中,团队开发是常见的事。通常情况下,一个大的软件开发项目会进行分解,并交由不同的人负责完成。然而在实际的工作中难免会有一些交叉的部分,比如两个人都要使用同一个源文件。
团队协作,软件的版本控制非常重要。所有软件的源代码都存放在数据库里,当程序员要求修改某个源文件时,他要先执行一个检出操作,从数据库里调出该文件的最新版本。之后,他可能会修改这个文件,删除一些东西,或者添加一些代码。如果他认为很满意,就需要执行一个检入操作,这将在数据库里生成一个该文件的最新版本,但是以前的版本不受影响。这样,即使第二天他发现前一天的修改非常愚蠢,也可以很容易回退到历史版本。
如果多个程序员的工作都涉及同一个源文件,则他们将产生冲突。安全起见,如果一个程序员在检出时,文件被锁定且不允许其他程序员修改这个文件,直到该程序员检入这个文件并解除锁定。
当然,版本控制系统也可以被设定为允许多个程序员同时修改同一个源文件。在这种情况下,如何协调冲突是非常重要的。假定某个源文件当前的修订版本是Rev5,其中有这样一个数组声明(注意,程序员都是爱美的人,很注意代码的排版和缩进效果):
int b [] = { 1, 2, 3, 4, 5, 6 };
真是凑巧,程序员A和程序员B都检出该文件并进行了表5-1所示的修改。程序员A先做了检入操作,检入后,版本控制系统将生成最新版本Rev6。
表5-1 程序员A和程序员B对同一文件的不同修改
于是,当程序员B检入的时候,版本控制系统将报告当前文件已经过期,而且他的修改与新版本的某些内容冲突。此时,程序员B可以查看程序员A都做了哪些修改,并打电话或者发电子邮件与他进行沟通。
经过沟通,程序员B意识到程序员A的修改很有道理,而他自己添加的那3个数也很有必要。怎么办呢?他将选择用程序员A的修改更新第2行的内容,并使自己新加的那一行也有效。此时,在他即将检入的文件中,数组的最终声明是:
int b [] = { 1, 2, 3, 40, 50, 60 7, 8, 9, };
注意,第2行的末尾没有逗号,因为程序员B认可程序员A对第2行的修改,而程序员A的这一行本来就没有逗号。在这种情况下,程序员B不得不手动为第2行添加一个逗号并使此修改有效。最终,程序员B检入合并后的内容,并更新到Rev7。
显然,如果在原始版本Rev5中的第2行本来就有一个逗号,像添加逗号这种额外的操作就可以省略。