C语言非常道
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.6.6 指针—指针转换

在C语言里,允许将一个指向变量类型的指针转换为指向另一种变量类型的指针,比如将一个指向int类型的指针转换为指向char类型的指针。

每当我们声明或者定义了某个函数时,就相当于创建了一种函数类型。参数类型不同、返回类型不同的函数属于不同的函数类型,进一步地,指向不同函数类型的指针属于不同的指针类型。

相应地,也可以将一个指向某种函数类型的指针转换为指向另一种函数类型的指针,当它再次转换回原来的类型后,和原先的指针相等。在下面的程序中,我们将一个指向某函数类型的指针转换为指向另一种函数类型的指针,然后再转换回来加以比较,比较之后再用转换回来的指针调用它所指向的那个函数。

              /*******************c0410.c******************/
              int max(int a, int b)
              {
                  return a >= b ? a : b;
              }

              int main(void)
              {
                  int res,(* pf)(int, int)= max;
                  void(* px)(void)=(void(*)(void))pf;

                  res =(int(*)(int, int))px == pf ? 1 : 0;
                  res =((int(*)(int, int))px)(1, 2);
              }

在main函数里,第一行声明了变量res和pf,变量res的类型是int,而变量pf的类型是指向函数的指针。变量pf的确切类型是指向“有两个int类型的参数,且返回类型是void的函数”的指针。变量pf的初始化器是函数指示符max,将自动转换为指针,且转换后的类型与pf的类型一致(参见函数指示符—指针转换)。

在第二行,我们又声明了另一个指针类型的变量px,它的确切类型是指向“参数类型和返回类型都是void的函数”的指针。来看它的初始化器,左值pf经左值转换,值的类型是int(*)(int, int),与变量px的类型不一致,不能直接用于初始化,还需要用转型运算符把它转换为变量px的类型,即void(*)(void)类型。

接下来的一行看起来很复杂,但这只不过是因为它里面包含了一个转型运算符(int(*)(int, int))的缘故。在表达式

              res =(int(*)(int, int))px == pf ? 1 : 0

里,转型运算符的优先级最高,等性运算符==次之;条件运算符?:又次之;赋值运算符=的优先级最低,所以这个表达式等价于

              res =((((int(*)(int, int))px)== pf)? 1 : 0)

说到底,这是把条件表达式的值赋给左值res,而条件表达式的值又取决于(int(*)(int, int))px和pf的比较结果。

等性运算符不但适用于整数,还适用于指针类型的操作数,可用于比较两个指针是否相同,即,是否指向同一个变量或者函数,或者是否都是空指针。变量px和pf都存储了函数max的地址,但它们的类型不同,按规定,只有指向同一种函数类型的指针才能放在一起比较。但是,如果类型不一致,它不会像对待整型操作数那样能够自动转换,指针类型的操作数必须手工转换为一致。不然的话,在翻译程序时,翻译器将愤愤不平地咕哝几句以示抗议。

为此,该表达式是把左值px经左值转换后得到的值强制转换为int(*)(int, int)类型,再与左值pf经左值转换后的值作等性比较。对于等性运算符!=和==来说,不管操作数的类型是什么,比较的结果都是int类型的0或者1。

最后,表达式res =((int(*)(int, int))px)(1, 2)是将变量px的值转换为int(*)(int, int)类型,并以这种类型调用它所指向的函数。由于函数调用运算符的优先级高于转型运算符,故还必须将转型表达式(int(*)(int, int))px用括号括起来,使之成为基本表达式。函数调用的结果(返回值)被赋给res。

实际上,表达式(int(*)(int, int))px的值与变量pf的值一样,都是指向函数max的指针,所以上述函数调用实际上等效于res = pf(1, 2)。

练习4.11

1.在上述程序里,第一次赋值和第二次赋值后,变量res的值各为多少?请上机验证。

2.若p和q都是指针类型的左值,则表达式p ! = q和p == q的(结果)类型是什么?若p的类型是指向char的指针而q的类型是指向int的指针,则程序翻译时会有警告信息吗?请上机实际验证。

4.6.6.1 变量地址的对齐

在前面的程序中,变量px的类型是void(*)(void)。尽管它的值实际上指向函数max,但你不能这样调用:

              px(1, 2)

而只能这样调用:

              px()

原因极其简单:在程序翻译期间,C实现要做类型检查,左值px的类型是指向“参数类型和返回类型都是void的函数”的指针,但你却传递了两个参数,这不合法。

然而,变量px的值实际上指向函数max,函数调用表达式px( )虽然合法,但却与函数max的声明不一致。按照规定,如果一个指向函数的指针同它实际指向的函数类型不一致,则用这个指针做函数调用时,程序的行为是未定义的。

相似地,如果将一个指向某种变量类型的指针转换为指向另一种变量类型的指针,用转换后的指针访问变量时,也会出各种问题。来看下面的程序片段:

              char x = 0;
              ++ *(int *)& x;

在这里,变量x的类型是char,只占用1个字节的存储空间。紧接着在第二行,一个指向变量x的指针被转换为指向int的指针,然后递增它所指向的变量。

表达式& x的类型是指向char的指针;表达式(int *)& x的类型是指向int的指针;表达式*(int *)& x的结果是一个int类型的左值;运算符++递增这个左值所代表的变量(的存储值)。

这里的重点在于被递增的变量是int类型的。在我的机器上,一个int类型的变量占用4个字节的存储空间。但是实际上,有3个字节并不属于它。

这就尴尬了,你侵犯了别人的领土,那个地方可能属于另一个变量,这样的话你就破坏了另一个变量的值,如果那里恰巧保存的是银行账目,这就更让人头大了;那个地方也许并不对应任何变量,变量必须先分配再使用,访问一个没有分配的存储空间等于拿着空头支票去市场上买东西,当然会被拒绝,拒绝的结果就是程序可能崩溃。

变量的大小只是一个方面的问题,另一个问题是内存地址的对齐。学过计算机原理的同学都知道,处理器读写内存储器时,要先通过地址总线发送一个地址到内存储器。然而,内存储器是按字节组织的,字节是最小的可寻址单元,但它也可以每次读写2个字节或者4个字节甚至16个字节的数据。

这就是说,一个地址可用于访问1个字节单元,也可用于访问2个连续的字节单元,或者4个、8个连续的字节单元。这种灵活性是有代价的,受硬件布线的限制,这将要求特定类型(长度)的变量只能位于特定的地址,这称为对齐。

对齐用一个整数值来描述,它必须是2N,且N是非负数。比如在我的机器上,int类型的变量原则上只能位于0x00000004、0x00000008、0x0000000C等地址上,都是一些能够被4(22)整除的地址,故它的对齐是4。

在任何机器上,char类型的变量可位于任何地址上,因为它只有一个字节,而字节是内存储器支持的最小可寻址单元。能够将所有地址整除的只有数字1(20),故对于char类型的变量来说,其对齐始终为1。

来看上面的例子,变量x的类型是char,可位于任何内存地址上;但是,左值*(int*)& x的类型是int,代表一个int类型的变量。在我的机器上,它要求这个变量对齐于能够被4整除的地址上。

先不说将char类型的变量当成int类型的变量来访问是否合法,就说地址,如果变量x的地址是0x00000003,那么,它并不符合int类型所要求的对齐,这个地址不能被4整除。

有些处理器是强制要求对齐的,比如Motorola 68K处理器,在这种计算机上,非对齐的访问将产生一个总线错误。Intel x86处理器也建议使用对齐的访问,但同时它并不限制你非得这么做。如果你使用非对齐的访问,它也能工作,只不过要迂回一些。

原则上,我们并不需要考虑变量的对齐问题。你编程时的任务是声明变量,不需要关心它在哪里。在程序运行时,自然会按照它的类型把它安排在符合要求的地址上。然而,如果是通过指针访问变量,而且指针所指向的类型与它所指向的变量不符,这就是必须要考虑的问题了。

4.6.6.2 认识_Alignof运算符

一旦了解到变量在内存中的位置需要对齐到特定的地址上,你难免想知道特定类型的对齐值是多少,或者它应当位于哪些地址上。

这个问题不难解决,从C11(ISO/IEC 9899:2011)开始,C语言引入了一个新的运算符_Alignof,它用于返回指定类型的对齐值,其语法形式为:

            _Alignof类型名

注意,圆括号内只能是类型名,而不能是表达式(常量、左值或者变量的名字)。我们所认识的运算符都是一些非字母的符号,比如+、=、++、>,等等,但这个运算符却完全是由字母组成,很像一个函数。看起来很荒谬,但这就是C语言。

注意,_Alignof不单单是C语言里的运算符,也是关键字。运算符_Alignof的结果类型是一种无符号整数类型,可能是unsigned int,也可能是unsigned long long int,也可能是别的,但具体是哪种整数类型取决于具体的C实现。

在下面的例子中,我们分别获取char、指向char的指针,以及指向函数的指针这三种类型的对齐值。

              /************c0411.c**********/
              int main(void)
              {
                  unsigned long long x, y, z;
                  x = _Alignof(char);
                  y = _Alignof(char *);
                  z = _Alignof(int(*)(void));
              }

不管在哪种计算机系统上,char类型的对齐值始终为1。然而,char *和int(*)(void)类型的对齐值可以随计算机系统而异。还有,尽管指针可以指向函数,但它本身并不是函数,任何指针类型都是变量类型。也就是说,任何指针类型都可用于声明变量,任何指针类型的值都可以保存在变量中。

练习4.12

如果某类型的对齐值是8,它应当位于哪些地址上( )。

A. 0x00000000

B.0x00000008

C.0x0000000F

D.0x00000010