• 如何通过阅读C标准来解决C语言语法问题


    有时候必须非常专注地阅读ANSI C标准才能找到某个问题的答案。一位销售工程师把下面这段代码作为测试用例发给Sun的编译小组。

    foo(const char **p)
    {}
    
    int main(int argc, char **argv)
    {
        foo(argv);
    
        return 0;
    }

    如果编译这段代码,编译器会发出一条警告信息:

    line 5: warning: argument is incompatible with prototype

    (第5行:警告:参数与原型不匹配)。

    提交代码的工程师想知道为什么会产生这条警告信息,也想知道ANSI C标准的哪一部分讲述了这方面的内容。他认为,实参char *s与形参const char*p应该是相容的,标准库中所有的字符串处理函数都是这样的。那么,为什么实参char **argv与形参const char **p实际上不能相容呢?

    答案是肯定的,它们并不相容。

    要回答这个问题颇费心机,如果研究一下获得这个答案的整个过程,会比仅仅知道结论更有意义。对这个问题的分析是由Sun的其中一位工程师进行的,其过程如下:

    我们先来学习一下ANSI C标准中的一些基本术语:

    unportable code(不可移植的代码):

    -implementation-defined(由编译器定义的):由编译器设计者决定采取何种行为(也就是说,在不同的编译器中所采取的行为可能并不相同,但它们都是正确的)

    例如:当整型数向右移位时,要不要扩展符号位。

    -unspecified(未确定的):在某些正确情况下的做法,标准并未明确规定应该怎样做。

    例如:参数求值的顺序。

    bad code(坏代码):

    -undefined(未定义的):在某些不正确情况下的做法,但标准未规定应该怎样做。你可以采取任何行动,可以什么也不做,也可以发出一条警告信息,或者可以中止程序以及让CPU陷入瘫痪,甚至可以发射核导弹(只要你安装了能发射核弹的硬件系统)。

    例如:当一个有符号证书溢出时该采取什么行动。

    -constraint(约束条件):这是一个必须遵守的限制或要求。如果你不遵守,那么你的程序的行为就会变成像上面所说的属于未定义的。这就出现了一种很有意思的情况:分辨某种东西是否是一个约束条件是很容易的,因为标准的每个主题都附有一个“约束条件(constraint)”小节,列出了所有的约束条件。现在又出现了一个更为有趣的情况:标准规定编译器只有在违反语法规则和约束条件的情况下才能产生错误信息!这意味着所有不属于约束条件的语义规则你都可以不遵循,而且由于这种行为属于未定义行为,编译器可以采取任何行动,甚至不必通知你!

    在ANSI C标准第6.3.2.2节中讲述约束条件的小节中有这么一句话:

    每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)。

    这就是说参数传递过程类似于赋值。

    所以,除非一个类型为char **的值可以赋值给一个const char **类型的对象,否则,肯定会产生一条诊断信息。要想知道这个赋值是否合法,就请回顾标准中有关简单赋值的部分,它位于第6.3.16.1节,描述了下列约束条件:

    要使上述的赋值形式合法,必须满足下列条件之一:

    1.两个操作数都是指向有限限定符或者无限限定符的相容类型的指针;

    2.左边所指向的类型必须具有右边指针所指向类型的全部限定符。

    正是这个条件,使得函数调用中实参char*能够与形参const char *匹配(在C标准库中,所有的字符串处理函数就是这样的)。

    -const char*是一个指向(有const限定符的char类型)的指针。(不能修改其值)

    -char*是一个指向(没有限定符的char类型)的指针。

    因此,const char*和char*都是指向char类型的指针,只不过,const char*指向的char类型是const限定符修饰的。因此,如下代码

    char *cp;
    const char * ccp;
    ccp = cp;

    这样的赋值是正确的,因为

    -操作数指向的都是char类型,因此是相容的;

    -左操作数具有右操作数所指向的全部限定符(注意理解下,这里右操作数没有限定符,也就是说,有限定符的类型包含没有限定符的类型image,此图只适用于这里的方便理解,别无它用), 同时左操作数自己有限定符const。

    -char类型与char类型是相容的,左操作数所指向的类型char具有右操作数所指向类型char的限定符(这里,右操作数所指向的类型为char,右操作数所指向的类型char的限定符为“无”)。这里const为左操作符自身的限定符。

    注意,反过来就不能进行赋值。如果反过来赋值,就违反了赋值的约束条件:cp指向的对象的值可以修改,而ccp指向的对象的值不可修改。如果让cp去指向ccp所指向的那个不可修改的对象,如果合法,岂不是变得可以修改了??

    如果不信,试试下面的代码:

    cp = ccp; /*结果产生编译警告*/  //这样赋值,左操作数指向的类型没有右操作数指向类型的const限定符,
                                  //不符合约束条件2

    标准6.3.16.1节有没有说char **类型的实参与const char**类型的形参是相容的?

    答案是:没有。

    下面我们来分析下 const float *到底是什么?

    已知:C的语法规定:const char* 等价于 char const *。

    const float *类型并不是一个有限定符的 类型,它的类型是“指向一个(具有const限定符的float类型)的指针类型”,也就是说,const限定符是修饰指针所指向的类型,而不是指针本身。

    类似的,const char**也是一个没有限定符的指针类型,它的类型是“指向[指向(有const限定符的char类型)的指针类型]的指针类型”。

    由于char **和const char **都是没有限定符的指针类型,但它们所指向的类型不一样。

    char **是“指向 char *类型的指针类型”,也就是说,下面代码:

    char ** argv;

    定义的变量argv指的是:一个指向(指向char类型的指针类型)的指针类型。

    const char **类型是“指向const char *类型的指针类型”,也就是说,下面代码

    const char **p;

    定义的变量p指的是:一个指向[指向(有const限定符的char类型)的指针类型]的指针类型

    对于const char** 和char**来说,二者都是没有限定符的指针类型,但是他们指向的类型不是有,前者指向char*, 而后者指向const char*,因此它们不相容,所以char**类型的操作数不能赋值给const char**类型的操作数。

    由上可知,显然,(指向char类型的指针类型)    与    [指向(有const限定符的char类型)的指针类型]   是两种类型,所以他们是不相容的。因此,类型为char**的实参与类型为const char **类型的形参是不相容的,违反了标准第6.3.2.2中所规定的约束条件,编译器必然会产生一条诊断信息。

    Ref:

    《C专家编程》中文版page19

    http://www.cnblogs.com/chenleiustc/archive/2011/04/09/2010647.html

    转载本Blog文章请注明出处,否则,本作者保留追究其法律责任的权利。 本人转载别人或者copy别人的博客内容的部分,会尽量附上原文出处,仅供学习交流之用,如有侵权,联系立删。
  • 相关阅读:
    Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[2]
    Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[1]
    进程和线程
    Linux 技巧:让进程在后台可靠运行的几种方法
    Linux 网络命令必知必会之 tcpdump,一份完整的抓包指南请查收!
    这些好用的 Chrome 插件,提升你的工作效率
    golang学习笔记-go mod的使用
    性能监控和分析工具---nmon
    Jenkins
    程序员画图工具总结
  • 原文地址:https://www.cnblogs.com/drfxiaoliuzi/p/4680174.html
Copyright © 2020-2023  润新知