免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: MMMIX
打印 上一主题 下一主题

[C] 拨开迷雾见真知--C语言中指针的使用(获奖名单已公布-2014-4-21) [复制链接]

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
81 [报告]
发表于 2014-04-11 19:34 |只看该作者
本帖最后由 Ager 于 2014-04-11 21:16 编辑

(此,接续自上一楼

现在,或许有初学者觉得,通过仔细理解上面的阐述,自己已然可以对指针有深刻的掌握,于是,他们变得跃跃欲试,开始着手构造一个自指指针了。他们可能会编写如下的代码片段:
  1. int * p;
  2. p = &p;
复制代码
任何一个负责任的编译器,对此都会发出这样的抱怨:
  1. incompatible pointer type
复制代码
真是出师未捷!编译器其实已经否定了这段程序的正确性 —— 数据对象在类型上,出了问题!

所以我说,不要以为了解了以上篇幅中的关于指针的解说,就能真正掌握指针,乃至构造出一个合法的自指指针!—— 还差得远呢!

我们在前述内容中,已经解说了关于数据对象的四层结构,其中从下往上数的第二层,是Adverbial层。在关于“jptr = iptr“的例子中,概念图里为它们所标出的Adverbial值是”Tfoo *“。现在的问题是:Tfoo类型具体是什么?我们如何构造一个C标准允许的Tfoo类型,使得该类型上可以有一个自指指针?

请注意:对于任何类型T,都可以构造关于它的指针类型,即“指向T的指针”。 (参见:Harbison III, S.P., C: A Reference Manual, 5.3。)

现在,我们要构造一个指向自己的指针,它必然是一个“指向T(某种类型)的指针”。既然上面说了 —— “对于任何类型T,都可以构造指针类型”,那么,必然存在一种类型(T),令一种指针可以指向它,而这个指针是指向它自己的,除非:指向自己的指针在C中是不合法的,但是,这么一来,“对于任何类型T,都可以构造指针类型”这句话,好像就是错的,取而代之的“正确“做法是:把“能被指向自己的指针所指向的类型“排除在”任何类型“之外。其实,更严格地说,上面那句话在数理形式上的表达,应该是这样:

∀合法类型的T, ∃ T *。

现在的问题是:如果对于指向自己的指针来说,被它指向的(即它自己)的类型是合法的T,那么,这种类型(T)是什么样的类型?而指向T的指针,又将是什么样的类型呢?是 T * 类型吗?

如果指向T类型的指针是T * 类型,并且这个指针(T * 类型)是指向它自己的,那么,T * 类型就应该是T ** 类型的 —— 因为:指向S类型的指针是S * 类型的(我们用字母S代换了前面的T *)。同理,从另一个方向看,T类型能够被T * 类型所指向,又因为这里的T *类型是指向它自己的,所以,T *类型所指向的T类型就是它自己的类型,这样的话,T类型就应该是T * 类型的。

如果你还没有头昏的话,可以得到这么一个结论:如果指向自己的指针所指向的之类型是T,那么,T类型就是T * 类型,就是T **类型,而这三者是一致的类型。

但是,这样是可能的吗?对于C语言,这意味着什么?

暂时抛开“纯粹”的形上逻辑,让我们谈谈编程语言对指针的概念实现。首先,回到关于设置指针的宗旨,或指针的定义 —— 指针,作为一种数据类型,定义了一类数据对象,它们的Enslaving值是其他数据对象的Meta值。而构造指针类型的单个数据对象,在编程语言中,基本上有两种方式:

第一,指针仅能引用一种类型的数据对象。在形式上,指针数据对象的Adverbial值被限制为它所要引用的数据对象的Adverbial值加上“*”。Ada、Pascal、C都采用了这种方式,并且语言实现采用了静态类型检查。

第二,指针可以引用任何类型的数据对象。请注意,这里虽然出现了“任何”这个观念,但它并表明是静态上的“任何”,实际上,它是一种动态上的“任何”,即:允许指针在程序的运行时指向多种类型的数据对象(于是就似乎可以让T*在运行时中跃迁为T**,再跃迁为T***等)。这种方式的典型例子是Smalltalk。相关的类型检查在程序运行时维持,并且,数据对象在程序运行时仍具有类型描述符范畴。

所以,对于C语言来说,如果T和T *和T **是一致的类型,就必须保证它们是静态上的一致,即,在运行时前、编译期内,它们就必须被类型检查机构判定为合法。

回到上面的初学者代码 —— 看到上述抱怨信息,我们第一个想到的,就是使用一个名叫“类型转换“的操作符(即赋值操作符的右操作数是一个类型转换表达式)。于是有:
  1. int * p;
  2. p = (int *)&p;
复制代码
即:把 &p 这个int ** 类型的转换成了int * 类型的。不过,既然是使用了类型转换 —— 即不使用类型转换就不合法,就严格地意味着:int ** 类型与int * 类型不是一致的类型。

其实还有:int * 类型与int 类型也不是一致的类型 —— 如果一致的话,最先一个例子就应该合法,即:“p = &p“(假若)能够合法,在于左边的p是int *类型且右边的p是int类型,即int *类型与int 类型一致。也可以反过来说,如果int * 类型与int 类型一致,该赋值表达式就是合法的。

这样看来,int**类型、int*类型、int类型,都不是我们期望的那种“T”类型。之于其他算术类型,情况亦是如此。

在C中,是否存在一个“通用指针”的类型,使得它可以被转换为任何类型的指针而不违反标准?—— 当然是有的,并且C标准提供给用户的这种工具,正是叫做“通用指针”,有时也叫做“泛型指针”,即void * 类型。

之前我们曾经说过,一个数据对象的Adverbial值,由实现维护,并且可以让实现藉此决定,该如何将以机器内部样式存在的Enslaving值解释或表示出来。若一个数据对象被实现所维护的Adverbial值形如“T *”,那么这个数据对象就是指针。对于指针,实现之所以关心它的Adverbial值,是因为实现要为将来的解引用操作和加法减法对指针的操作做好预备 —— 作为解引用的结果,一个数据对象作为合法的左值,或在它成为一个右值的时候,即需要操作它的Enslaving值的时候,实现必须已经明确该用何种类型(方式)来写入(构成机器内部序列)或解读(构成符合程序逻辑的或符合用户感官预期的序列)。而作为加法减法的操作数,一个指针的Enslaving值,将被加上或减去多少(Meta值域的元量),或者将得到多大的“指针差”(ptrdiff_t类型上的数量),都是实现藉由该指针的Adverbial值来决定的。

我们不难理解,对于具有Tbar * 类型属性的指针(意思是Adverbial值为Tbar *的指针,而不是指向Tbar * 类型数据对象的指针),合法的解引用操作结果,一定是其Adverbial值为Tbar的数据对象,而合法的加法减法操作结果,一定与所操作的指针的Adverbial值有着洽适的关联。那么,对于具有void * 类型属性的指针,“合法”的解引用操作结果,一定是其Adverbial值为void的数据对象,即这个数据对象是“void”(废弃、无效、败坏)的,所以,这样的解引用是非法的。同理,对于具有void * 类型属性的指针,加法减法操作也是“void”的。

这里顺便提一点,由于“void”一词在非计算机学科专用的英汉词典中,被简单地解译为“空的”,参见下图:



因此,不少初学者将void * 理解为“空指针”,其实是和真确意义上的空指针(Null Pointer)混为一谈了,后者的意义是:不指向任何地方的指针。用来表示空指针的宏,是NULL,它被C标准规定必须在头文件<stddef.h>中获得定义,用来定义它的,是值为0的任何整数表达式,在一些编译器所提供的头文件中,这个整数表达式将被转换为void *类型。这么说,也是反过来暗示:空指针并非必须(天然地)是void *类型的,其实,根据设置空指针的起初宗旨,我们不难理解:每一种指针类型都需要在概念中有一个用来表示“不指向任何地方”的指针实例,这个实例就是各种指针类型上的空指针,而不必然是void *类型上的空指针。总之,将表示空指针的宏定义为void *类型的,仅仅是C标准所允许的一种技巧性行为。此外,虽然用值为0的任何整数表达式来定义宏NULL,但这并不意味着空指针的Enslaving值就是各位全为0的Meta值,总之,空指针的Enslaving值到底在机器的内部以何种样式存在,以及怎样的Meta值才会被视为“哪里也不会有”,这都是编译器设计者的专业工作。作为编写源代码的用户,只需要知道:一个指针在概念上的Enslaving值若是某个值为0的整数,那么,这个指针就是空指针。

顺便说一下,单纯从词语本义上说,“void”是“空的”的意思,那是完全没错的。这个词来自于拉丁语,意思是“没被占用的”,比如一间屋子没有人用或没人待在里面我们就说“屋子是空的”,这是说得通的。此外,“vacuum”(真空)跟这个词是同源的。而至于“null”,也是来自拉丁语,意思是“一个也没有”,可以延伸为“零”的意思。

我们用通用指针类型来构造自指指针,是十分简单顺利的:
  1. void * p;
  2. p = &p;
  3. ISALPHA(p);
复制代码
这里甚至不需要类型转换操作也是合法的,这样看来,&p所获得一个指向通用指针的指针(即void**类型)与通用指针(即void*类型)是相兼容的 —— 事实也的确如此,不过,其中的原理在于这一点:void*类型总与其他类型的指针相兼容 —— 在这个例子里,“其他类型”就是void**类型。而另一方面,void**类型并非总与其他类型的指针相兼容。可以考虑以下代码片段:
  1. void * vip, ** vipp;
  2. int * fip;
  3. fip = vip;   /* 兼容 */
  4. fip = vipp   /* 不兼容 */
复制代码
现在,我们终于找到一个符合构造自指指针的类型“T”了,就是void。

接下来,我们将寻找“T”的视野,拓展到C的聚合类型。

首先,我们看数组类型。观察以下片段:
  1. int fig[123];
  2. ISALPHA(fig);
复制代码
这段代码在被编译时,会被抱怨有类型上的问题,但编译还是完成了。如果将ISALPHA宏定义中的“ x == &x ”改为“ x == (int*)&x ”就可以规避这个问题。

这段代码的运行结果,告诉我们fig“是一个自指指针” —— 这或许会让初学者们大吃一惊,因为我们并没有对fig指针作任何特殊的处理,比如取地址和赋值操作。

难道一个数组标识符(有时候也叫:数组名)天然地就是一个自指指针吗?基于直觉的判断,是否定的。但我们还是应该弄清其中的缘由。我们注意到,程序用相等操作符表达式fig == &fig 来判断fig是否为一个自指指针。我们知道,凡出现在表达式中的数组标识符,它的类型就从“T的数组”经由寻常转换而变成“指向T的指针”且它的值将称为指向该数组的首个元素的指针,除非数组标识符是sizeof()操作符或取地址操作符的操作数。那么,在相等操作符表达式fig == &fig中,相等操作符左边的fig,将转换为指向该数组的首个元素的指针(类型是int *),而操作符右边的,则是一个取地址表达式,fig成了取地址操作符的操作数,此时取地址表达式将获得指向整个数组的指针(类型是int (*)[123]),上述二个数据对象具有迥异的Adverbial值,但却具有相同的Enslaving值,所以fig == &fig或安全的做法fig == (int*)&fig就可以得到为1(真)的结果。

fig == &fig之所以能够成立,其原理可以被夸张地等同于 a==a 能够成立,即:单一事物的自等性,藉由C语言的对于数组标识符的规则,衍变而成了一种表面上的指针自指性。之所以说是“表面上的”,那在于:fig == &fig 表达式中两方的fig并非是同一个指针(这跟我们本初设置这个用来判别的表达式的宗旨有异):左方的fig不是指代整个数组而是指代指向数组首个元素的指针,而右方的fig根本就不是指针而是指代整个数组。而排除了“表面上的”因素,其中实在的作为原理性因素的事实是:右方那个被视为指代整个数组的fig,经由取地址操作,被产生了一个指向整个数组的指针,而这个指针的Enslaving值,与指向该数组首个元素的指针的Enslaving值,是相等的 —— 这是以作为聚合类型的数组及其元素在空间上的存在方式作为保证的。

此外,我们须要特别注意: 正如上面已经说到的,C标准或关于C标准的文献所提到的关于数组标识符的规则是“凡出现在表达式中的数组标识符,它的类型就从‘T的数组’经由寻常转换而变成‘指向T的指针’且它的值将称为指向该数组的首个元素的指针,除非数组标识符是sizeof()操作符或取地址操作符的操作数”。那么,如果这个表达式是赋值表达式呢?出现在赋值表达式中的数组标识符,实际情况仅仅是上述原则所透露的那么简单嘛?

初学者经常试图使用数组标识符“将一个数组赋值(复制)到另外一个数组”,比如会编写如下片段:
  1. int fig[123];
  2. int feg[123];
  3. fig = feg;
复制代码
如果粗糙地理解“在表达式中数组标识符将被转换称为指向数组首个元素的指针”这个规则的话,我们会这样理解上面的第三行代码中的赋值表达式,即:将指向 fig数组首个元素的指针变更(被赋值)为指向 feg数组首个元素的指针。显然实际效果,并非如此。

或许有人会这样解释:试图改变一个数组名的指向(指向数组的指针之指向,或指向数组首个元素的指针之指向)是非法的。—— 虽然这种理解中提到的宗旨,即一种对数组的保护机制,是很容易理解的,并且是合理的,但是,似乎C标准中并未明确有这样的规定。

那么, C标准是如何确保上述对数组的保护机制被实现的呢?—— 这跟赋值操作符本身的特性有关。我们注意到,C标准对于赋值操作符有这样的规定:数组名不是可以修改的左值,它不能出现在赋值操作符的左边。更进一步地对C标准的规定进行回溯:作为左值的数组类型、不完整类型或被const限定的类型 …… ,是不允许被修改的左值。

因为上面例子中的左值是数组类型,所以它是不可修改的 —— 这真的就是“fig = feg”之所以非法的全部原理吗?那么,我们要问:假若把赋值操作符左边的fig视为一个已经换转为指向该数组首元素的指针,那么,它还是那种不可修改的左值吗?好像是的。《高质量程序设计指南——C++/C语言》在7.2中,是这样说的:

数组名字本身本身就是一个指针,是一个指针常量,即a等价于int * const a,因此你不能试图修改数组名的值。

按照这个说法(这个说法在国内许多技术讨论区中被援引),数组名a等价于int * const a,显然这个a还是左值,至少可以是左值,结合上面提到的“被const限定的类型是不允许被修改的左值”,就可以得出结论:这里的a是那种不允许被修改的左值。—— 但是这样的解释,是正确的吗?

我们注意看C99标准(6.3.2.1.3):

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue.

C标准已经明确了这一点:一个原本具有数组类型的表达式若被换转为指向该数组首个元素的指针,那么,所得到的那个指针不是左值! —— 这意味着,这样的指针,根本不能作为赋值操作符的左操作数。

这样说来,即便我们将赋值操作符左边的fig视为一个指向该数组首个元素的指针,它也不能作为赋值操作符的合法左操作数(因为它不是左值,而只有左值才能作为赋值操作符的合法左操作数)—— 将这个“不可能的可能性”都排除了,我们似乎获得了“fig = feg”非法的真相的全部 —— 不过,我们还是要追问:真的是如此吗?

我们注意看手头上的编译器,对“fig = feg”的报错信息。如果编译器确因fig不是左值而报错的话,那么它给出的信息应该是“error: lvalue required as left operand of assignment”这样的。然而,实际上它的报错信息,却是这样的:
  1. error: incompatible types when assigning to type 'int[123]' from type 'int *'
复制代码
编译器在这里强调了错误的原因是:赋值操作时的类型不能兼容! —— 这个理由,是上面已经给出的所有理由都无法解释的。我们还是回到赋值操作的本身上来。我们注意到,C标准对赋值操作的过程,有这样的表述:

赋值操作的结果之类型等于左操作数的(未转换且未限定 )类型。而这个结果将被保存到左操作数。

—— 这样的规定,保证了“fig = feg”的结果一定是fig本来的类型,即数组类型 int[123]。在赋值操作的两个操作数都是算术类型的情况下,在操作间会发生寻常赋值转换,使右操作数转换为左操作数的类型,然后将这个具有新类型的值保存到左操作数中(赋值操作表达式的副作用即告完结)。然而,在这里,作为右操作数的feg已经被转换为int *类型,这种类型是无法被进行寻常赋值转换而成为int[123]类型的。所以,“fig = feg”操作,首先是一个类型或类型转换上的错误。我们手头上的编译器,正是准确而细致地报告了这一错误,看来,这个编译器,是非常遵循C标准的。

下面,我们看结构类型。

我们知道:C语言的结构,从构造上看,就不允许包含自身,但可以包含指向自身的指针。于是,C语言藉着这个简单的规则,向用户提供了实现复杂而精妙算法的丰富可能性。基于这种规则允许结构包含指向自身的指针,我们就有了构造自指指针的可能性。也就是说,我们将一个结构构造中指向该构造自己的指针,实现为指向它自己的指针,那么,这里的充分条件是:该结构构造是以指向该构造的指针为起头的。于是,我们可以编写这样的片段:
  1. struct hog
  2.     {
  3.         struct hog *p;
  4.         int a;
  5.     } hig;

  6. hig.p = &hig;                        /*(<- 注) */
  7. ISALPHA(hig.p);
复制代码
(注 ->)上面这个代码片段,在本文的写作过程中,最早由@Starwing83给出。

请注意,在结构hog的构造中,成员int a被放置在了成员hog *p的后面,如果颠倒过来,即hog *p作为结构hog构造的第二个成员,却在将来的实例hig.p上指向结构实例hig(的起头),那么,hig.p将不是一个自指指针。

此外,利用ISALPHA宏对指针hig.p进行判别的时候,仍无法避免类型上的问题,不过可以被顺利的转换。

接下来,我们看联合类型。

跟常见的初学者的误解不同,C的联合类型并非是聚合类型的一种。在C中,联合类型独立于包含着数组、结构的聚合类型之外。联合类型与结构类型,具有非常大的差异。我们首先看以下代码片段:
  1. union pog
  2. {
  3.     union pog *p;
  4.     int a;
  5. } pig;

  6. pig.p =&pig;
  7. ISALPHA(pig.p);
复制代码
虽然联合类型与结构类型的原理不同,但上面这个片段跟再上面的那个关于结构类型的例子,在原理上是类型的,即:在一个联合构造中,设置一个指向该联合构造自己的指针。这个已经不难理解。

然而,体现出联合类型与结构类型之不同的,是我们将该联合构造的各成员位置,在编写声明的时候,位置颠倒一下,即:
  1. union pog
  2. {
  3.         int a;
  4. union pog *p;
  5. } pig;

  6. pig.p = &pig;
  7. ISALPHA(pig.p);
复制代码
如果类型的行为,发生在编写结构构造的过程中,那么,那个指向结构实例的指针将不再是一个自指指针,但是,在联合构造中,这样的颠倒,并不会妨碍这个指向联合实例的指针成为一个自指指针。这是为什么呢?

首先,联合类型的原理,决定了每一个联合实例的成员,都具有相同的Meta值,也就是该联合实例的Meta值 —— 相同的Meta值,意味着它们以同一个Meta值为空间存在的起点。那么,不仅对该联合实例进行取地址操作,可以构造出一个自指指针,而且,对该联合实例的任何成员进行取地址操作,都可以构造出一个自指指针,比如,将上面的 pig.p = &pig 更改为 pig.p = &pig.a 也可以令 pig.p 成为一个自指指针,进而利用 pig.p = &pig.p 也可以。此时,该指针能够指向自己的原理,已经不再是该指针指向它所在的联合的实例,而是嬗变为:它总是被赋予了任何具有与自己一致Meta值的数据对象(联合实例的成员、联合实例)的Meta值。也就是说,联合类型本身具有的“其实例的各成员的Meta值,与其他成员的Meta值一致,并亦与该实例本身的Meta值一致”这一特性,才是我们可以使联合构造所包含的指针被构造为自指指针的真正原理。

其次,我们须要揭示一个被大多数人所忽略的实际上的原则:联合构造所包含的各个成员之声明,在代码中的编写次序,不会影响它们将来会被获得的Meta值。而这一点,在结构类型上就不适用,因为 —— 结构构造所包含的各个成员之声明,在代码中的编写次序,决定了它们将来在数据空间上的存放次序,并且它们不会具有相同的Meta值。也就是说,之于结构类型,其成员在数据空间上的次序,是敏感于其声明在代码中的编写次序的。

接下来,我们还注意到,在上面的例子中,除了“pig.p = &pig”中赋值操作的右操作数是严格符合该指针(即左操作数)的类型的之外,“pig.p = &pig.a”和“pig.p = &pig.p”等赋值操作,都包含了不符合左操作数类型的右操作数,所以编译器在翻译这些赋值表达式的时候,都会发出抱怨。而下面的片段,给出了一个比较优美的例子:
  1. union pog
  2.     {
  3.         int *p;
  4.         int **pp;
  5.     } pig;

  6. pig.pp = &pig.p; /*(<- 注) */
  7. ISALPHA(hig.p);
复制代码
(注 ->)上面这个代码片段,在本文的写作过程中,最早由@Starwing83给出。

接下来,我们看函数类型。

首先需要说明的是,函数虽然是C的类型的一种,但函数类型的实例(或者说,函数类型的值)却在C标准的语境中被排除在“对象”概念之外。所以,作为一个与“对象”相对的概念 —— “函数指示符(Function Designator)”具有函数类型的值,却无法具有左值的地位。函数指示符在代码中的表示的一种,是函数的名称,并且,对指向函数的指针进行解引用的结果,也是函数指示符。除了函数指示符之外,函数调用表达式也不能成为左值。同时,请注意这样的例子:
  1. int *ted (int *p)
  2. {
  3.     return p;
  4. }

  5. int main(void)
  6. {
  7.     int x = 123;

  8.     *ted(&x) = 456;
  9.     printf("%d\n",x);
  10.     return 0;
  11. }
复制代码
在这个例子中,“*ted(&x) = 456”并非一个以函数调用表达式作为左值的表达式,在这里,作为左值的是一个解引用表达式。

此外,C标准保证:如果对函数指示符进行取地址操作,那么结果将是一个指向该函数实例的指针。另一方面,如果对函数指示符进行解引用操作,那么在操作间,函数指示符将被转换为指向该函数实例的指针。所以,我们可以得到:

若有 void tod(void){},
& tod => 结果是:指向tod的指针;
* tod => 过程中,tod换转为指向tod的指针;
那么,* tod 和 * & tod 是等效的,
即:对于解引用操作,函数指示符与指向函数的指针是等效的。

请注意,之前提到了:对指向函数的指针进行解引用的结果,是函数指示符。所以,* & tod 的结果,就是 tod。那么,* tod 和 * & tod 和 tod 三者是等效的。由于* tod 和 tod 是等效的,就可以将 tod 与指向 tod 的指针即& tod视为等效的。由于这里的 tod,可以指代其他一切函数指示符,所以,我们得到结论:

对于任何一个函数指示符,与一个指向该函数实例的指针,是等效的。

所以,任何一个函数指示符,可以被视为一个指针,并且,这个指针指向该函数指示符所代表的函数类型的实例。简谓之,任何一个函数指示符,都是自指指针。

对于已经定义了的函数 tod,我们可以通过ISALPHA宏来检验tod的函数指示符:
  1. ISALPHA(tod);
复制代码
整个程序在编译时和运行时,均没有任何警告和错误,验证的结果是:tod是一个自指指针。

最后,我们要解答上面产生的一个疑问:为什么我们用宏,而不是用一个设计好的函数,来验证指针呢?

我们用编写函数的方式,来取代宏 ISALPHA:
  1. void IsAlpha(void * x)
  2. {
  3.     if(x == &x)
  4.         printf("It is Alpha!\n");
  5.     else printf("It is NOT Alpha!\n");
  6. }
复制代码
那么,在任何情况下,对于那些已经被验证了是指向自己的指针,都将得到它不是自指指针的结果。这是为什么呢?—— 原因在于:

在IsAlpha函数体内,用来进行比较的是指针 x ,而这个指针仅仅是该函数中的一个局部的数据对象。对于任何函数内部的即局部的数据对象,只是作为被传递的实际参数在值范畴上的拷贝,而不是被传递的实际参数本身。即,这个局部数据对象仅仅在函数被运行的起头,其Enslaving值被赋予了实际参数的Enslaving值,但它的Meta值是不可能与实际参数的Meta值相同的,因为函数例程为其局部数据对象所实现的储存,是不可能占用实际参数的数据空间的。所以,该局部数据对象的Enslaving值是实际参数的Enslaving值,如果实际参数是一个自指指针,那么,实际参数的Enslaving值与Meta值是一致的,这样,该局部数据对象的Enslaving值就是实际参数的Meta值,却不可能是它自己的Meta值。所以,在IsAlpha函数体内,作为局部变量的 x,永远不可能是一个自指指针。该函数永远不会给出“是自指指针”的结果 —— 这跟前面例子中所提到的“jptr 看似是一个自指指针,实际却不是”的道理,非常类似,在那个例子中,jptr是iptr的一个值范畴上的拷贝,却不是iptr本身,所以,若iptr是一个自指指针,那么jptr就一定不是一个自指指针。

那么,我们如何让函数处理的对象,是我们业务逻辑上需要处理的对象本身呢?—— 当然就是利用指针了。我们在上面已经提到,指针是C语言向用户提供的一种引用机制,即可以藉由指针获得某个被引用者的Meta值。而函数的局部变量只会被生成为实际参数的一份值范畴上的副本,所以,我们如果向函数传递一个关于被引用者的指针,那么,这个被引用者的Meta值藉由实际参数的Enslaving值,被复制到由函数生成的它的局部数据对象的Enslaving层上,即函数的局部数据对象的Enslaving值,即是被引用者的Meta值,进而,在函数内部对作为指针的局部数据对象进行解引用操作,就将获得一个被引用者的左值,此时,在此左值上的操作,就是在被引用者上的操作了。

最简单的例子,可以是这样:
  1. void foo(int *x){
  2. (*x)++;
  3. }
复制代码
可以在其他函数内,做这样的调用:
  1. foo(&a);
复制代码
即可以令int类型实例a,获得与a++等效的操作。

所以,我们改写函数IsAlpha为:
  1. void IsAlpha(void ** x)
  2. {
  3.     if(*x == &*x)
  4.         printf("It is Alpha!\n");
  5.     else printf("It is NOT Alpha!\n");
  6. }
复制代码
并且可以在其他函数内,做这样的调用:
  1. IsAlpha(&p);
复制代码
如果这里的p是上面例子中被验证了的一个自指指针的话,函数IsAlpha将给出“是自指指针”的结果,而若p是上面例子中被验证了的一个自指指针的话,函数IsAlpha将给出“是自指指针”的结果,即:函数IsAlpha的效用与宏ISALPHA的效用是一致的 —— 但是,对于函数tod,则是一个例外,这是为什么?

函数tod,是我们验证的“最完美”的一个自指指针,反而在被改进了的函数IsAlpha中却得不到正确的结果,这是为什么?

我们回顾一下函数IsAlpha的定义。在改进了的函数IsAlpha中,局部变量 x 是一个指向通用指针的指针 —— 注意,与通用指针不可以被解引用不同,指向通用指针的指针是可以被解引用的,解引用的结果就是一个通用指针。而这个由解引用所得到的通用指针,其实就是被转换为了通用指针的我们需要检验的指针自己。所以,函数IsAlpha内部可以检验我们需要检验的指针。但是,如果需要检验的指针是作为指针的函数,就是一个例外。以需要检验的指针是作为指针的函数指示符tod为例,IsAlpha(&tod)这样的调用,令函数IsAlpha的局部变量 x (类型是指向通用指针的指针) 的Enslaving值被赋予了指向tod函数的指针的Meta值(这里我们将tod视为指向tod函数的指针)。

进而,函数内部对这个x的Enslaving值进行解引用,将得到一个通用指针(我们希望:这个通用指针就是指向tod函数的指针,即这个通用指针的Enslaving值是tod函数的Meta值),然而,实际上,这个通用指针的Enslaving值不会是tod函数的Meta值,为什么?—— 因为C标准规定了:任何指向对象或不完整类型的指针,都可以转换为通用指针,但指向函数类型的指针是例外!也就是说:如果将指向函数类型的指针转换为通用指针,将是一个错误的行为。所以,对x的Enslaving值的解引用,会产生一个通用指针,这实际上就是把一个指向函数类型的指针,转换为了一个通用指针,而这种行为是错误的,所以这个通用指针的Enslaving值将是一个不符合我们预期的“错误值”。

回到 *x == &*x 这个表达式。相等操作符的左边,*x,是一个通用指针(因为x是指向通用指针的指针,所以对它解引用,就是一个通用指针),如果被传递到函数IsAlpha的,是指向那些除指向函数实例的指针之外的指针的指针(即这些指针的引用),那么*x 这个通用指针,就是那些除指向函数类型的指针之外的指针本身(因此我们才能用函数IsAlpha来检验我们需要检验的指针),而如果被传递到函数IsAlpha的,是指向指向函数实例的指针(实际上就是函数实例自己)的指针,那么*x 这个通用指针,就是指向函数实例的指针,但这是违反C标准的,*x 这个通用指针将是一个垃圾指针。

相等操作符的右边,&*x,是对*x 这个通用指针的取地址操作,如果这个通用指针是我们需要检验的自指指针(指向函数实例的指针除外),那么,取地址操作的结果即自指指针的Meta值,将与*x即自指指针的Enslaving值一致,所以可以判定被检验的指针是自指指针,而如果作为通用指针的*x是指向函数实例的指针,那么*x就是一个垃圾指针,此时对这个垃圾指针进行取地址操作,结果好像也应该是垃圾值,但事实并非如此,为什么呢?(请坚持住,这是本文讨论的最后一个迷局了。)

因为:这个垃圾指针之所以是垃圾,在于它的Enslaving值被表示为垃圾值(因为它的Adverbial值出错),但这并不意味着这个垃圾指针本身位于一个垃圾位置。这是一个上面曾经提到过的再简单不过的道理:

取地址操作符,将它的操作数当作左值看待。即,取地址操作的结果,只跟它的操作数的Meta值有关,而跟这个操作数的Enslaving值无关。—— 这是隐藏在贯穿本文中的几乎所有迷局后面的最关键的“作祟点”。

其实,这个垃圾的通用指针,正是指向tod函数的指针的原原本本的自己。所以,对这个垃圾指针进行取地址操作,得到的仍是指向tod函数的指针的Meta值,即被我们传递进函数IsAlpha的那个值,这一点,从 &*x == x 也能看的出来。当然,指向tod函数的指针的Meta值,就是tod函数的Meta值,也就是指向tod函数的指针的Enslaving值。

综上,在*x == &*x 这个表达式中,相等操作符左边的是一个被转换为通用指针的指向tod函数的指针,右边的则是一个指向指向tod函数的指针的指针,即:左边的是一个垃圾指针,右边的则是一个符合逻辑意义且实际上也是正确的指针,所以,这个相等操作表达式将得到0(假)的结果 —— 这就是导致函数IsAlpha在检验指向函数的指针是否为自指指针的任务中不适用的原因。

至此,我们在那个因着我们寻找自指指针而被我们“误入”的谜题世界中的游历,就告一段落了。在这次游历中,我们花尽脑力,从一个又一个迷局的陷阱中爬了出来,并且见识了这个充满谜题的世界里的奇妙景色。而这一切,使得我们在“活着”从那里走出来的时候,真是唏嘘不已啊!

以上全部,仅供参考。

P.S.:如果本帖有幸获得奖励,请先给@starwing83发奖品,他为本文的发起做出了重要的贡献。


论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
82 [报告]
发表于 2014-04-11 19:37 |只看该作者
最近忙到昏天瞎地的,所以没法给太多的建议——其实墙哥的这篇文章之前就写完了的,我也完全看了,但是没有给出什么反馈,实在是感觉很惭愧……

额,反正我有时间针对这个事儿,我觉得还是再单独写个帖子评论一下最好,当然到时候肯定会先给@Ager看的……

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
83 [报告]
发表于 2014-04-11 19:52 |只看该作者
starwing83 发表于 2014-04-11 19:37
最近忙到昏天瞎地的,所以没法给太多的建议


辛苦啦!致敬 {:3_193:}

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
84 [报告]
发表于 2014-04-12 19:40 |只看该作者
回复 89# Ager


    文笔好,可就是写的太绕了,隐约觉得第一篇用math的角度来说工程上的东西意义不大,可一时半会想不到怎么表述理由。

论坛徽章:
2
青铜圣斗士
日期:2015-11-26 06:15:59数据库技术版块每日发帖之星
日期:2016-07-24 06:20:00
85 [报告]
发表于 2014-04-13 06:51 |只看该作者
成功经验和失败教训貌似都没什么印象了。。。 就说一些心得体会好了。。。

C标准与现实与实践

C标准对语言的规定很宽松。应该是为了能尽可能地在各种环境上实现。
宽松是指对C语言的实现者很宽松,有各种undefined/unspecified/implementation-defined behavior让实现者有选择空间。
而这种宽松会C程序员不那么好过。不仅仅是指针,各种地方都会增加复杂性。
无论C语言这种态度是否正确(就C语言的角色来说我认为是对的),总之这是事实。

另一方面,现实里那些奇奇怪怪的机器正在(已经?)逐步淘汰。
并且C语言本身也在改变。比如C99允许不同类型的struct/union指针相互转换,这是C89里没有的。
实现者的选择空间变窄而让程序员可以得到的保证更多。

实践里C语言的各种不确定性肯定会让代码变得复杂。
我认为“遵守C语言标准”并不是一个绝对准则。它是可以当权衡的筹码使用的。
在尽可能地遵守的前提下,当遵守起来不那么容易的时候,是可以在移植性与代码复杂性之间进行权衡的。

这是后续话题的基调。

以byte为例,它在C语言里的定义与大部分时候提到的那个byte —— 8bit octets —— 并不相同。
C语言对它的定义是执行环境上最小可寻址单元,并且以char类型来表示。
sizeof(char)恒为1。但并没有规定CHAR_BIT一定是8,只是规定CHAR_BIT>=8。而其他类型的sizeof都根据char的倍数来定义。
也就是说在9,12,36,48位的机器上都是可以实现C语言的。而比这更小的,比如4bit上应该怎么实现C语言就不清楚了,或许会将两个机器字拼成一个char?

当下是否还存在CHAR_BIT不为8的C语言实现?我只听说过某些DSP上有16bit的char。但也只是听闻而没有实际接触过。

实践上来说,要写出可移植的C程序是很困难的事情。
虽说可以采用一些技巧将大部分代码与具体C语言实现隔离,但就这一小部分代码也总得有人去写。并且C实现的差异只是移植中的一个环节而已。

想想在CHAR_BIT=9的机器上该怎么处理许多网络以及编码协议?它们很多都是使用8,16,32,64位的整数。
很有可能再也不能通过 *(uint16_t*)p 来获取而是像,比如Java那样, b0 | (b1 << s1) | (b2 << s2) ... 。

POSIX就规定CHAR_BIT=8: http://pubs.opengroup.org/online ... edefs/limits.h.html
于是可以:

  1. #include <limits.h>
  2. typedef int char_is_8bit[CHAR_BIT==8?1:-1];
复制代码
然后将char(byte)当作8bit octets使用。
并且POSIX也要求实现必须提供(u)int8/16/32_t等类型: http://pubs.opengroup.org/online ... edefs/stdint.h.html
于是可以直接使用uint8_t,uint16_t,uint32_t来处理各种协议。同时Windows也是定义了UINT8,UINT16,...等类型的。

对比一下用b0 | (b1 << s1) | (b2 << s2 )处理带来的额外“工作量”和移植到非POSIX非Windows之外的“可能性”。
无论最终决定是什么,这都是可以可以考虑的问题。
“就只根据当前环境”或“就只根据C语言定义”来作决策都是下策。

另外再附带一个有意思的言论:
lid
‘-a number’
‘--ambiguous=number’
    List identifiers (not numbers) that are ambiguous for the first number characters. This feature might be in useful when porting programs to ancient pea-brained compilers that don't support long identifier names. However, the best long-term option is to set such systems on fire.


如果没记错,C89要求实现至少要支持6个字符长度的标识符。而C99增加到了31个字符。(都说的是外部符号长度)。
在C99出来之前有多少代码使用了超过6个字符长度的标识符的?或者说在C99没出来之前,为了能在所有C实现上都能工作而控制命名不超过6个字符?
反正我不会这么做。 那种去元音的缩写简直没法忍。。。
取有意义的名字然后等着那些奇怪的系统被丢进火堆就行了。。。

后续的话题都与“理论vs实践”有关。

空指针与零

大部分情况下,空指针的具体表示都(应该)是程序员不需要关心的。

0在指针上下文里都会被转换为“对应类型的空指针表示”。所谓指针上下文包括但不限于:

  1. T0* p0 = 0; /* 赋值 */

  2. T1* get(params...) { if (...) return 0; ... } /* 返回 */

  3. T1* p1 = get(args...);
  4. if (p1/* p1!=0 */) { ... } /* 条件 */
  5. if (!p1 /* p1==0 */) { ... }
  6. if (p1 && expr) { ... }

  7. void f(T2* p2); f(0); /* 参数传递 */
复制代码
总之只要编译器知道这个0打算转换为某个类型的指针,就会将0转换为该类型指针所对应的空指针表示。

例如上面的例子里:
p0会被赋值为T0类型的空指针的bit pattern。
get中的return 0会返回T1类型的空指针的bit pattern。
后续的几个比较会与T1类型的空指针的bit pattern比较。
最后一个例子里会向f传入T2类型的空指针的bit pattern。

bit pattern不是全0的情况在 http://c-faq.com/null/machexamp.html 里有记录一些例子。
但无论T0,T1,T2类型的空指针的bit pattern是否是全0,上面的代码都会如希望那样工作。编译器会照顾好这些情况。

接下来说一些编译器不能得知上下文的情况。。。

calloc,memset

用calloc,memset来初始化是比较常见的情况。

  1. typedef struct T {
  2.   T0* p0;
  3.   T1* p1;
  4.   T2* p2;
  5.   /* other */
  6. } T;

  7. T* x = calloc(1,sizeof *p); /* 不能保证x->p0,x->p1,x->p2是空指针 */
  8. x->p0 = 0; /* 编译器知道这是指针上下文,于是可以保证 x->p0 是空指针 */

  9. T y;
  10. memset(&y, 0, sizeof y); /* 不能保证y.p0,y.p1,y.p2是空指针 */

  11. T z = {0}; /* 指针上下文,可以保证 z.p0,z.p1,z.p2 全是空指针 */
复制代码
于是可能会有相应的函数/宏来初始化T。用memset,calloc来初始化整体,然后将指针域显式地赋值为0。这个赋值操作可以通过条件编译在空指针的表示就是全0的环境 —— 大部分 —— 下去掉。
这也比较容易处理。

变长参数

无论变长参数是否应该避免都改变不了已经有不少函数是这样设计的事实。于是和它们打交道也是很常见的。

  1. printf("%p", 0);
复制代码
是错的。因为编译器不知道这是指针上下文,于是不会进行相应转换。
哪怕当前情况下不出问题它也是错的。

不需要进行什么复杂的工作就可以让它符合标准:

  1. printf("%p", (void*)0); /* 告知编译器这是指针上下文 */
复制代码
多的这个(void*)是很值得的。

使用NULL宏的情况比较特殊。。。

  1. printf("%p", NULL);
复制代码
在C++下(或者用C++编译器编译C代码的时候 —— Clean C)它肯定是错的。

C++里各种指针类型可以隐式转到到void*但反过来不行。而在C里双向都是隐式的。
如果C++里的NULL定义为(void*)0或者(char*)0,那上面的代码是正确的。但其他更多的代码写起来就痛苦了。
于是C++里的NULL是0。

在C++代码不能这么写。C代码如果打算要用C++编译器编译(Clean C)也不能这么写。
于是使用变长参数时不推荐使用NULL。直接用(void*)0就行。
其实在其他情况下我也不喜欢用这个没什么用的NULL,不过这只是口味问题,没什么技术上的原因。

用(unsigned )long保存地址

不(应该)太常见的情况就是把(unsigned )long当指针用了。比如Linux内核。。。

实话实说。。。 有些时候不得不用long来表示地址。。。 比如计算对齐的时候。。。
存在一些代码是有相关考虑的,比如:
obstack

  1. /* If B is the base of an object addressed by P, return the result of
  2.    aligning P to the next multiple of A + 1.  B and P must be of type
  3.    char *.  A + 1 must be a power of 2.  */

  4. #define __BPTR_ALIGN(B, P, A) ((B) + (((P) -(B) + (A)) & ~(A)))

  5. /* Similiar to _BPTR_ALIGN (B, P, A), except optimize the common case
  6.    where pointers can be converted to integers, aligned as integers,
  7.    and converted back again.  If PTR_INT_TYPE is narrower than a
  8.    pointer (e.g., the AS/400), play it safe and compute the alignment
  9.    relative to B.  Otherwise, use the faster strategy of computing the
  10.    alignment relative to 0.  */

  11. #define __PTR_ALIGN(B, P, A)                                                  \
  12.   __BPTR_ALIGN (sizeof (PTR_INT_TYPE) < sizeof (void *) ? (B) : (char *) 0, \
  13.                 P, A)
复制代码
根据sizeof (PRT_INT_TYPE) 与 sizeof (void*) 的关系,分别计算的是:

  1. (B) + (((P) - (       B) + (A)) & ~(A))
  2. (B) + (((P) - ((char*)0) + (A)) & ~(A))
复制代码
都是通过两个指针的减运算得到ptrdiff_t的整数类型,然后在此基础上计算对齐后的长度,最后再和B相加得到新的指针。
语言只定义了在同一个object上的指针运算,所以前者是符合标准的而后者不是。
后者可以让 (P) - ((char*)0) 优化掉。而前者可以避免这个计算超出ptrdiff_t的范围。

符合标准的计算会多一个减法。少一个减法的计算又确实会在AS/400上出问题。
obstack这样的处理方式把好处都占了,但又让代码变复杂。
我自己的选择会是: 先写前一种符合的,真的需要少一个减法的时候再说。而后一种不符合的从一开始就避免。

同时这样的技巧依赖B本身满足对齐,这依赖libc,并进一步依赖OS。
OS在没有“基准对齐指针”的情况下也要保证brk,sbrk,mmap,...能产生适合环境的对齐,并且让malloc和obstack在它的基础上实现。
于是OS就需要了解环境下的对齐规则是怎样,然后用long来表示地址。。。

因此在这方面总得有人绕过C语言定义直接与具体环境打交道。

指针低位

还有一些以“肮脏”的手段处理指针的情况。

因为前面提到的对齐的原因,于是很多情况下指针的低位是用不上的。于是就被用来存储各种信息。。。
比如红黑数的颜色。也是Liunx内核正在做的事情。。。
比如动态类型语言里值的类型。Emacs Lisp和许多Scheme,Common Lisp的实现都是这么干的。。。

相比少一个减法,这种技巧的诱惑力可能更大。 专门用一个int去存储几个bit确实太浪费了。。。

IEEE754

既然提到了用指针低位来表示类型就顺带提一下IEEE754。虽然与指针已经没什么关系了,但还是和理论与实践间的权衡有关。

C语言并不保证float,double,long double一定是符合IEEE754的。它连这些类型的长度都不作出保证,更不用说表示法了。。。不过好像在附录里有个地方推荐这样实现。

于是Lua也作同样的处理。。。“总之我默认映射到double,至于double到底是什么问C语言去”。
2.1 – Values and Types
Operations on numbers follow the same rules of the underlying C implementation, which, in turn, usually follows the IEEE 754 standard.

而在5.1的对应章节里连上面这句话都没有。。。

与之相对的。ECMA5(也就是通常说的Javascript的标准)明确规定Number的表示方式:
8.5   The Number Type
The Number type has exactly 18437736874454810627 (that is, 264−253+3) values, representing the doubleprecision 64-bit format IEEE 754 values as specified in the IEEE Standard for Binary Floating-Point Arithmetic, except that the 9007199254740990 (that is, 253−2) distinct "Not-a-Number" values of the IEEE Standard are represented in ECMAScript as a single special NaN value.


IEEE754与类型的关系是:NaN的表示不只一种,于是Lua和JS的许多实现(V8,JaegerMonkey)都用它们来存放类型。
为了效率不得不绕过C语言定义。

Lua在这方面的态度和C比较类似。态度暧昧,于是实现的时候就有回旋的余地。
而ECMA5就明确说了Number的行为。那不支持IEEE754的是什么机器?
这样的机器上需要运行浏览器么?感觉不会。。。
这样的机器上需要运行node.js么?也许会。。。 那用软件模拟?总之这是node.js的选择,也是它该考虑的问题。

幸好(在上面粗体部分)ECMA5标准还为NaN trick留下了一点余地。。。
64位机器流行起来(后?)这些脏手段可能会逐步尘封进历史。。。

strict aliasing

总是在说标准为了保证移植性而让符合标准的代码效率低。。。这里说一个标准为了提高效率作出的改动。。。
因为这个问题Python是大改过一次代码的。PEP 3123 -- Making PyObject_HEAD conform to standard C

就这个链接里的代码 —— 稍微修改过的 —— 简短说明一下Python遇到的问题。

  1. #define PyObject_HEAD int ob_refcnt; /* 假设这里是所有PyObject都有的域。原始代码里没有相关定义。 */

  2. typedef struct PyObject {
  3.   PyObject_HEAD
  4. } PyObject; /* 这是标准的PyObject */

  5. struct FooObject{
  6.   PyObject_HEAD
  7.   int data; /* FooObject有额外的data域 */
  8. };

  9. PyObject *foo(struct FooObject*f); /* { return (PyObject*)f;} */
  10. /* 将原始代码里的定义隐藏了,否则编译器太聪明了。并且这也是符合通常情况下分别编译的场景的。 */

  11. struct FooObject* foo_get(void); /* { return malloc(sizeof(struct FooObject)); } */
  12. /* 同样将malloc隐藏。不过和这个例子没什么关系,而是我机器上没有对应的头文件。。。 */

  13. int bar(){
  14. struct FooObject *f = foo_get(); /* 同上,从直接malloc改为通过foo_get获取,不影响结果 */
  15. struct PyObject *o = foo(f);     /* 问题出在这里。 根据foo的实现实际上f与o是同一块内存地址 */
  16. f->ob_refcnt = 0;                /* 通过f设置所有PyObject都共有的ob_refcnt域 */
  17. o->ob_refcnt = 1;                /* 通过o设置同一个PyObject的ob_refcnt */
  18. return f->ob_refcnt;             /* 返回这一个PyObject的ob_refcnt  */
  19. }
复制代码
然后:

  1. $ gcc -Wall -c -m32 -O3 py.c # -fno-strict-aliasing
  2. $ objdump -d -Mintel py.o | grep -A 3 xor
  3.   22:        31 c0                        xor    eax,eax ; 清零
  4.   24:        5b                           pop    ebx
  5.   25:        c3                           ret            ; 返回
复制代码
这是违反strict aliasing rules(记得是C99增加的)产生的问题。
除了少数几种例外情况下语言要求同一块内存必须用相同的类型去访问,于是编译器被允许认为f(FooObject)与o(PyObject)不是别名。
因此gcc就认为通过f设置的ob_refcnt与通过o设置的不是同一个,然后就只将通过f设置的0返回了(f->ob_refcnt=0,而避免重新通过f再读取一次(return f->ob_refcnt

但这样的行为是不符合预期的。。。 因为程序员没有遵守strict aliasing rules。。。

一种work around是增加-fno-strict-aliasing参数让gcc不根据这个规则进行优化。
而如果想使用这种优化的话可以这样改:

  1. struct FooObject{
  2.   PyObject HEAD;
  3.   int data;
  4. };

  5. int bar(){
  6. ...
  7. f->HEAD.ob_refcnt = 0;
  8. o->ob_refcnt = 1;
  9. return f->HEAD.ob_refcnt;
  10. }
复制代码
这是前面提到的例外情况之一:PyObject是FooObject的第1个域。语言允许通过PyObject*去访问FooObject*中的HEAD部分。于是编译器就会认为这里可能会存在别名,就会放弃优化。

这样修改符合标准了,也能和新的编译器优化配合工作。
但另一方面原来的f->ob_refcnt就需要f->HEAD.ob_refcnt。这只是简化的例子。实际上当“继承”关系更深的时候就会f->x.y.z等等。如果还打算继续坚持使用C,估计就需要用宏了。

更详细的信息可以在上面那个链接里找。比如Python现在到底是个什么样子。

Python选择在strict aliasing上遵守语言标准。
而选择违反的例子也有。比较出名的就是Linux内核。。。
这货看起来是不打算去掉-fno-strict-aliasing参数了。反正之前也有过memcpy和memmove这种轻视规范的先例了。。。

函数指针

void*可以作为通用的数据指针。因为语言保证任何其他数据指针转换到void*后能无损地转换回去。
同时语言也保证函数指针之间可以相互转换,只要在调用前转换成正确的类型即可。于是任何一个函数指针都可以作为通用函数指针。

但数据指针(包括void*)和函数指针之间的转换就不是了。。。于是没有通用的既可以保存数据有可以保存函数的指针。。。

在C语言定义下要“同时”保存数据或函数指针只能:
* 用union
* 用void* 但不直接指向函数,而是指向一个“函数指针变量”,而这就是数据了。。。 好像boost.any就是这样做的。

但从实践角度来说用void*保存函数指针也没什么影响。。。
dlsym是这么干的。。。
Due to the problem noted here, a future version may either add a new function to return function pointers, or the current interface may be deprecated in favor of two new functions: one that returns data pointers and the other that returns function pointers.

这个future version到现在都没出现。。。
它在Windows下的对应物GetProcAddress也是这样干的。而且Windows下还有大量使用整数类型LPARAM来表示指针和函数指针的情况。。。

它们都运作得好好的。。。 有可能将来会作出改变的反而是C语言的定义。。。

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
86 [报告]
发表于 2014-04-13 20:37 |只看该作者
gvim 发表于 2014-04-12 19:40
回复 89# Ager

文笔好,可就是写的太绕了,隐约觉得第一篇用math的角度来说工程上的东西意义不大,可一时半会想不到怎么表述理由。


谢谢支持与批评:)

其实,math部分,原来是没打算写的。只是,后来所讨论到的“如何用函数,即让待判指针作为函数的参数或参数的一部分,来判别该指针是否为自指指针”这个问题,把它倒逼出来的。

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
87 [报告]
发表于 2014-04-14 00:01 |只看该作者
OwnWaterloo 发表于 2014-04-13 06:51
成功经验和失败教训貌似都没什么印象了。。。 就说一些心得体会好了。。。

C标准与现实与实践


Significantly marked:)



论坛徽章:
44
15-16赛季CBA联赛之浙江
日期:2021-10-11 02:03:59程序设计版块每日发帖之星
日期:2016-07-02 06:20:0015-16赛季CBA联赛之新疆
日期:2016-04-25 10:55:452016科比退役纪念章
日期:2016-04-23 00:51:2315-16赛季CBA联赛之山东
日期:2016-04-17 12:00:2815-16赛季CBA联赛之福建
日期:2016-04-12 15:21:2915-16赛季CBA联赛之辽宁
日期:2016-03-24 21:38:2715-16赛季CBA联赛之福建
日期:2016-03-18 12:13:4015-16赛季CBA联赛之佛山
日期:2016-02-05 00:55:2015-16赛季CBA联赛之佛山
日期:2016-02-04 21:11:3615-16赛季CBA联赛之天津
日期:2016-11-02 00:33:1215-16赛季CBA联赛之浙江
日期:2017-01-13 01:31:49
88 [报告]
发表于 2014-04-14 17:20 |只看该作者
本帖最后由 windoze 于 2014-04-14 20:47 编辑

回复 87# Ager

我觉得你折腾了这一圈,构造的只是一个“能安全转化为指向自身指针的指针”,T**永远不是T*,不管你怎么搞。
不过话说回来,搞C的童鞋们本来也就不是特别在乎类型这玩艺儿。
C99里说:
intptr_t         integer type capable of holding a pointer
uintptr_t         unsigned integer type capable of holding a pointer
也就是说,C99允许你把任何一个“pointer”(好吧,它没说是data pointer还是function pointer),放在这样两个整数类型里,只要你在使用它之前把它转回原来的类型就好了。
有这条垫底,还怕什么?管它T*还是T**,塞到这个整数里不就好了吗?(我知道C99标准没说两个intptr_t能不能拿来比较,要比较要都转成void*才“合法”,不过我真心觉得接下来会做出改动的不是程序,而是C标准)
其实吧,C里面(除了浮点数外)所有东西都是整数,老A你洋洋洒洒写了这么多,怎么看都像是在分析鲁迅家院外那两棵枣树……

PS. 刚才又仔细想了一下,没理由两个intptr_t不能直接拿来比较的,因为:
1、所有指针都能“无损”的转换成void *
2、void *能做等值比较
3、void *能“无损”的转换成intptr_t
如此看来,intptr_t必须能做等值比较,因为在T *->void *->intptr_t这条路径上,做第二步时,转化成void *之前的指针类型已经不知道了,如果转化结果不能做等值比较,就等于说相等的两个void *转成intptr_t“有可能”不相等,这基本就是在胡扯……

所以,原问题T==&T,T居然可以干脆不是一个指针

  1. intptr_t p=&p;
  2. assert(p==&p);
复制代码
完全合法。

话说对这种代码给警告的编译器,我能不能说它多管闲事呢?

论坛徽章:
44
15-16赛季CBA联赛之浙江
日期:2021-10-11 02:03:59程序设计版块每日发帖之星
日期:2016-07-02 06:20:0015-16赛季CBA联赛之新疆
日期:2016-04-25 10:55:452016科比退役纪念章
日期:2016-04-23 00:51:2315-16赛季CBA联赛之山东
日期:2016-04-17 12:00:2815-16赛季CBA联赛之福建
日期:2016-04-12 15:21:2915-16赛季CBA联赛之辽宁
日期:2016-03-24 21:38:2715-16赛季CBA联赛之福建
日期:2016-03-18 12:13:4015-16赛季CBA联赛之佛山
日期:2016-02-05 00:55:2015-16赛季CBA联赛之佛山
日期:2016-02-04 21:11:3615-16赛季CBA联赛之天津
日期:2016-11-02 00:33:1215-16赛季CBA联赛之浙江
日期:2017-01-13 01:31:49
89 [报告]
发表于 2014-04-14 17:25 |只看该作者
OwnWaterloo 发表于 2014-04-13 06:51
C标准对语言的规定很宽松。应该是为了能尽可能地在各种环境上实现。

这就是挖坑啊,每次标准委员会那帮家伙开会,都在吵吵ABI规范及兼容性,每次都不了了之……

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
90 [报告]
发表于 2014-04-15 10:34 |只看该作者
windoze 发表于 2014-04-14 17:20
回复 87# Ager

我觉得你折腾了这一圈,构造的只是一个“能安全转化为指向自身指针的指针”,T**永远不是T*,不管你怎么搞。
不过话说回来,搞C的童鞋们本来也就不是特别在乎类型这玩艺儿。
C99里说:


您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP