zoukankan      html  css  js  c++  java
  • 【C】 04

      程序的生命力体现在它千变万化的行为,而再复杂的系统都是由最基本的语句组成的。C语句形式简单自由,但功能强大。从规范的角度学习C语法,一切显得简单而透彻,无需困扰于各种奇怪的语法。

    1. 表达式(expression)

    1.1 简单表达式

      一个表达式最重要的属性是它的值,可以定位其对象的值叫左值(l-value,locator value),其它叫右值(r-value)。右值只是临时值,使用完即不存在,不可把它当对象操作。

      本小节先介绍原子表达式和单个的操作符(operand)用法,基本是按优先级排序的。原子表达式(primary expression)是最简单的表达式,包括标识(identifier)、常量、括号和泛选操作(generic slection)。能做原子表达式的标识只有变量名和函数名,变量名一般是左值,但数组名被转换为指针时是右值,函数名单独使用时也转换为指针。

    int a[2], b[2];
    void fun(void);
    
    a[0] = 1;         // l-value
    a = b;            // illegal
    fun;              // ok, but not call fun

      泛型是新规范中提供的一种生成表达式的表达式,泛型的结果是表达式本身,而非表达式的值(不同于?:操作)。泛型开始于关键字_Generic,后面跟括号,括号里第一个为任意表达式,后面是多个“类型:表达式”对。类型必须是完整的且互相不相容的,可以有一个可选的default类型。表达式只能是左值、函数名或空,空表达式类型为void。泛型的结果是与第一个表达式相容的类型对应的表达式,它仍为左值、函数名或空。泛型被库用于根据参数类型选择函数,也可以用于选择变量。

    #define cbrt(X) _Generic((X),          
                    long double: cbrtl,    
                    default: cbrt,         
                    float: cbrtf           
                    )(X)
    int   i;
    long  l;
    float f;
    
    cbrt(f);                               // cbrtf(f)
    _Generic(f, double: i, float: l) = 1;  // l = 1

      后缀操作符(postfix operator)包括数组下标[]、函数调用()、成员引用.和->、后缀++(--)、组合常量。a[i]被转化为a+i,因此也可以写作i[a],a[i]返回左值。函数调用的操作数是函数指针,所以也可以直接放在函数名后面,函数返回值为右值。成员引用返回左值,组合常量也返回左值。后缀++(--)操作数必须为左值,返回原始值(右值)。

    int a[2], x, y;
    int fun(int i) {return i;}
    int (*fp)(int);
    struct S {int i;} s, *ps;
    
    a[0] = 1;                   // *(a + 1) = 1
    1[a] = 2;                   // *(1 + a) = 2
    x+++++y;                    // illegal, ((x++)++) + y, not (x++) + (++y)
    
    fp = &fun;                  // the same as fp = fun
    fun(1);                     // the same as fp(1)
    (*fp)(1);                   // the same as fp(1)
    &fun(1);                    // illegal
    
    ps = &s;
    s.i = 1;
    ps->i = 1;
    (struct S){1}.i = 2;        // ok

      一元操作符(unary operator)也可以叫前缀操作符,包括前缀++(--)、前缀+(-)、地址操作&和*、运算!和~、强制转换()、sizeof和_Alignof。前缀++(--)操作数必须为左值,返新值(左值),等同于+= 1(-=1)。取址操作&得到操作数的指针,操作数必须为左值(但不能作用于位域和register变量),结果为右值。解址操作*(dereference)获取对象,结果为左值。!作用于度量型,操作数非零则结果为0,否则结果为1。强制类型转换的结果为右值,不可继续操作。sizeof和_Alignof的结果为整型,具体类型基于实现,由宏size_t定义。

    int a = 1;
    struct S {int bits:10} s, *ps;
    
    ++a++;                           // illegal, ++(a++)
    ++++a;                           // ok, ++(++a)
    &s.bits;                         // illegal
    ps = &s;
    (*ps).bits = 1;                  // ok
    !NaN;                            // 0
    ((int*)ps).bits;                 // illegal

      代数运算符包括算术运算符(*、/、%、+、-)、位运算符(>>、<<、&、|、^)、比较运算符(==、!=、>、<、>=、<=)和逻辑运算符(&&、||)。除法的操作符为整数时,商向0舍入(truncation toward 0),也可以说余数与被除数同号。指针与整型的加减表示指针的偏移(加法中顺序任意),以类型长度为单位,所以void指针不可偏移。两个指针相减的结果是以byte为单位的整型,具体类型基于实现,由宏ptrdiff_t定义。位运算只作用于整型,对有符号数的操作未定义,移位运算超出范围未定义,位操作可以实现一些快速算法。比较和逻辑运算符的值为0或1,一般用作条件表达式,==不作用于组合类型。条件表达式必须是度量型,它(X)的结果相当于(X != 0),逻辑运算的操作数必须是条件表达式。

    unsigned int n, *p;
    void *pv;
    int a[2];
    struct S {int i;} s1, s2;
    
    (-5) / 2;                    // -2
    5 / (-2);                    // -2
    (-5) % (-2);                 // -1
    
    p = &n;
    pv = (void*)p;
    pv++;                        // illegal
    1 + p;                       // ok
    p - &n;                      // 4
    
    1U << 40;                    // undefined
    n & 0x000000FF;              // mask
    n | 0x00000001;              // set
    n & ~0x00000001;             // clear
    n ^ 0xFFFFFFFF;              // flip
    eax ^ eax;                   // fast 0
    
    // count 1 in a binary number
    n = (n & 0x55555555) + ((n >> 1) & 0x55555555); 
    n = (n & 0x33333333) + ((n >> 2) & 0x33333333); 
    n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f); 
    n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff); 
    n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
    
    if (a == a);                 // ok
    if (s1 == s2);               // illegal
    if (pv);                     // the same as if (0 != pv)

      条件(conditional)操作符(?:)是C中唯一的3目操作符,除代码相对紧凑外与if else并无不同(包括性能)。第一个操作数为条件表达式,后两个操作数同为数、同类复合类型、同类型指针或同为void,结果是右值。若同为数,需要做一般代数转换,结果也是转换后的类型。若同为指针,则结果的的限定符为所有限定符的合并。若一个为空指针,返回另一个指针类型。若一个为void指针,返回类型是void指针。

    const int    a;
    volatile int b;
    int  *pi;
    void *pv
    void f1(void) {}
    void f2(void) {}
    
    1 ? 1 : 2.0;         // 1.0
    1 ? a : b;           // const volatile int
    1 ? pi : NULL;       // int*, although NULL is (void*)0
    1 ? pi : pv;         // void*
    (1 ? a : b) = 1;     // illegal
    (1 ? f1 : f2)();     // ok, pointer

      赋值(assignment)操作符包括一般赋值操作符(=)和复合赋值操作符(x=),其中x为上段中的算术和位运算符。a x= y一般等价于a = a x b,但前者表达式a仅解析一次,在有些平台效率更高。赋值操作的左操作数必须为左值,结果为左操作数的新值(右值)。同类型的指针可相互赋值(void指针可赋值给其它指针),但要求右操作数的限定符不少于左边的。结构、联合可相互赋值,但数组不可以(数组名转化为指针),可考虑将数组放在结构中。

    int  a, b;
    int  *pi;
    const int *pc;
    void *pv;
    int  a1[2], a2[2];
    struct S {int i;} s1, s2;
    
    a = b = 1;                  // ok, a = (b = 1);
    (a = b) = 1;                // illegal
    pc = pi;                    // ok
    pi = pc;                    // illegal
    pi = pv;                    // ok
    s1 = s2;                    // ok
    a1 = a2;                    // illegal

      逗号操作符返回第二个操作数,为右值。C中有很多的符号复用,需根据语境区决定其意义。逗号在声明列表、初始化列表、函数参数列表、枚举定义中都是分隔符,而非操作符。

    // separator
    int a, b;
    struct S {int a, b;} s = {1, 2};
    enum E {FALSE, TRUE};
    void f(int m, int n) {}
    f(1, 2);
    
    // operator
    1, 2;                              // 2
    f((1 , 2) , 2);                    // f(2, 2)
    for (a = 1, b = 2; ; );

    1.2 优先级和结合律

      当表达式中的操作数也是表达式时,逻辑会变得复杂,需要一定的规则决定操作的执行顺序。C教材上一般会列出一张优先级和结合律的表,但如果企图从算法角度解释优先级,你会发现是困难和含糊的。C规范也没有这两个概念,有的只是对各操作符的语法定义,简单说就是符合语法的解释才是正确的结果。比如算术运算的语法可以定义成如下表,其中清晰地定义了乘除的优先级高于加减,且结合律是从左向右。本节所有的语法定义都是简化的,甚至是故意写错的,细节请参考规范。

    multiplicative_exp:
        unary_exp
        multiplicative_exp * unary_exp
        multiplicative_exp / unary_exp
        multiplicative_exp % unary_exp
    
    additive_exp:
        multiplicative_exp
        additive_exp + multiplicative_exp
        additive_exp - multiplicative_exp

      优先级最高的当然是原子表达式,然后依次是后缀、前缀、代数、条件、赋值和逗号。同类单目操作无优先级和结合律问题,可以简单定义如下。代数运算符都是像算术运算那样累加定义的,且结合律都是从左向右。条件操作结合律是从右向左,从如下语法中看得更清楚。赋值操作的左表达式只能是单目的,就是说a + b = c这样的式子连优先级都谈不上。逗号操作符优先级最低,结合律是从左向右的。

    postfix_exp:
        primary_exp
        postfix_exp postfix_op
    
    unary_exp:
        postfix_exp
        prefix_op unary_exp
    
    conditional_exp:
        arithmatical_exp
        arithmatical_exp ? exp : conditional_exp
    
    assignment_exp:
        conditional_exp
        unary_exp assignment_op assignment_exp
    
    exp:
        assignment_exp
        exp, assignment_exp

      代数运算的优先级如下表所示(从左向右递减),该表还是需要记住的。

    *    /    % +    - >>    << >    >=    <    <= ==    != & ^ | && ||

      有了这些语法和优先级,一些复杂的表达式就可以弄清楚了,以下列举了一些容易混淆的用法。对表达式的精确理解不仅有助于提高代码质量,而且还帮助正确理解别人的代码。

    int n, *p;
    void fun(void) {}
    struct S {int i;} s;
    
    *p++;                  // *(p++)
    ++p[0];                // ++(p[0])
    &fun();                // &(fun())
    (struct S*)p->i;       // illegal
    sizeof p ++;           // sizeof(p++)
    
    3 * 4 / 2 * 1;         // (((3 * 4) / 2) * 1)
    100 >> 1 + 1;          // 100 >> (1 + 1)
    n & 0xFFFF + 1U;       // n & (0xFFFF + 1U)
    0xFFFF ^ n == 0x3333;  // 0xFFFF ^ (n == 0x3333)
    1 | 2 ^ 3 & 4;         // 1 | (2 ^ (3 & 4))
    1 || 2 && 3;           // 1 || (2 && 3)
    
    1 == 2 ? 3 : 4;        // (1 == 2) ? 3 : 4 
    n = 1 ? 2 : 3;         // n = (1 ? 2 : 3)
    1 ? 2 ? 3 : 4 : 5;     // ok, 1 ? (2 ? 3 : 4) : 5
    1 ? n = 2 : 3;         // ok, 1 ? (n = 2) : 3
    1 ? 2, 3 : 4;          // ok, 1 ? (2, 3) : 4
    1 ? 2 : 3 ? 4 : 5;     // ok, 1 ? 2 :(3 ? 4 : 5)
    1 ? 2 : 3 == 4;        // 1 ? 2 : (3 == 4)
    
    1 ? 2 : n = 3;         // illegal, not (1 ? 2 : n) = 3 or 1 ? 2 : (n = 3)
    1 ? 2 : 3, 4;          // ok, (1 ? 2 : 3), 4
    1 + 2 = 3;             // illegal, not (1 + 2) = 3 or 1 + (2 = 3)
    1, 2, 3;               // (1, 2), 3

    1.3 时序点(sequnce point)

      表达式有时会改变对象的值(主要发生在++(--)、赋值和函数调用时),一般叫表达式的副作用(side effect)。表达式可以看成是一系列求值和副作用的序列,但即使确定了优先级和结合律,该序列的顺序仍然可能是不确定的。原因有两点:(1)双目运算两个操作数的求值顺序不确定;(2)发生的副作用并不要求立刻产生作用。但即便如此,副作用的时序还是受到一些限制的。前置++(--)的副作用在操作返回前发生,赋值操作的副作用发生在两边求值之后。一个表达式是确定的是指每一个对象的取值都发生在修改之前,且修改最多只有一次。

    int a = 0, b, *p = &a;
    
    1 * 2 + 3 % 4;           // * or % first is not defined
    b = a++;                 // a = 1 or b = 0 first is not defined
    (++p)[0] = 1;            // p = &b, then b = 1
    a = a - 1;               // get a, then a = 0
    b = a + a++;             // b = 0 or 1
    a = a++;                 // a = 1 or 2

      另外,C中还规定了一些时序点,要求到达该点时,发生的副作用全部产生作用。时序点发生在完整表达式(full expression)结束、分支表达式中和函数调用前。一个完整的表达式结束时自然要求副作用全部产生作用,这类表达式包含在表达式语句、局部变量定义、分支和循环语句、for语句开头、return语句中。分支表达式是指&&、||和?:,&&和||先求值第一个表达式,如果不满足则不处理第二个表达式,两个表达式之间有一个时序点。?:的条件表达式结束有一个时序点,后面两个表达式只有一个会被处理。函数调用时,参数全部进栈完毕和执行函数之间有一个时序点。

    int fun(int n)
    {
        int a = 0, b = a + 1;       // a = 0, then b = 1
    
        b = 0;                      //  b = 0, then to if
        if (0 == a++) b = 0;        //  a = 1, then b = 0
    
        // (1) b = 1, (2) a = 2, (3) b = 0, (4) a = 3
        for (b++; a++ < 2; b = 0);
        return a = 1;               // a = 1, then return
    }
    
    int m = 0;
    fun(m++);                       // m = 1, then fun(0)
    if (0 == m++ && (m = 0));       // m = 2, then &&, no m = 0
    if (2 == m-- || (m = 0));       // m = 1, then ||, no m= 0 
    1 == m++ ? m-- : (m = 0);       // m = 2, then m = 1, no m = 0

      还有一个关于表达式值的问题是:常量表达式会在编译时完成操作。但编译器不会改变表达式来拼凑常量,而且逗号表达式不做常量运算。另外,sizeof和_Alignof的操作数不被处理,复合常量中子表达式仍会被执行。

    int a = 0, *p;
    
    1 + 2 + a;                // the same as 3 + a
    a + 1 + 2;                // (a + 1) + 2, not a + 3
    1, 2;                     // not 2
    sizeof(a++);              // 4, a = 0
    _Alignof(a++);            // 4, a = 0
    while (a < 2)
    {
        p = (int[]){a, a++};
    }                         // a = 2, p[0] = 0

    2. 语句(statement)

    2.1 一般语句

      这里讨论的语句是指要执行的代码,不包括全局的声明和定义,最大的语句就是函数定义的语句块(block)。以下是语句的语法定义,方括号表示可选。

    stt:
        [exp];
        labeled_stt
        compound_stt
        selection_stt
        iteration_stt
        jump_stt
    
    labeled_stt:
        label: stt
        case int_const: stt
        default: stt
    
    compound_stt:
        {[declaration_stt_list]}

      空语句是一个表达式语句,表达式返回void,注意使用中容易造成的误解。标签语句是一个完整的语句,label可以累加,但尾部必须有语句,label语句不阻挡程序的继续执行。语句块可以为空,也可以包含多个声明和语句。函数体必须为一个语句块,否则空语句无法与声明中分号区分。auto变量的定义语句可以看作分配空间和赋值两步(即使没有初始化),在进入块时分配空间,在执行到时进行赋值。旧规范中要求定义必须在block头部,但新规不作限制。

    void f(void);              // declaration, not void f(void) {}
    void f(void) return;       // illegal, should be block
    
    void f(void)
    {
        int a = 0;
        if (0 != a);           // empty statement
            a = 1;             // always do
        goto LABEL0;
        int b = 1;             // ok in C99
    
        while(1) {}            // the same as while(1);
        while(1)
        {
            static int s = 0;  // not executed
            int m;             // m = rand each time
            int n = 1;         // n = 1 each time
            n++;
        }
    
    LABEL0: a = b;             // rand
    LABEL1:                    // ok, many lables
    LABEL2: ;                  // ok, empty statement
    LABEL3:                    // illegle
    }

    2.2 分支语句

    selection_stt:
        if (exp) stt
        if (exp) stt else stt
        switch (exp) stt

      分支和循环语句中的控制表达式必须是条件表达式,switch语句中的控制表达式还要求是整型。像括号匹配一样,if语句中的else总是和它之前第一个未匹配的if匹配。else后面可以跟任何语句,包括另一个if语句或其它,但没有elseif关键字。switch语句的控制表达式需要做整型提升,case中的中的值也会转换成相应的类型。case和default的顺序任意,default是可选的。匹配不上case则执行default,没有default则不执行。swtich语句中要习惯使用break语句结束分支,否则会继续执行下去。

    signed char c = -1;
    
    if (1) if (2); else;     // if (1) { if (2); else; }
    if (1) elseif (2);       // illegal
    if (1); else while (2);  // ok
    
    switch (c);              // ok, do nothing
    switch (c)               // promote to int
    {
        default:  break;     // ok
        case -1:  break;     // can be negtive
        case 1LL: break;     // convert to int
    }

    2.3 循环语句

    iteration_stt:
        while (exp) stt
        do stt while (exp);
        for ([declaration or exp]; [exp]; [exp]) stt

      循环语句会持续执行,直至条件不满足。当从循环外跳进循环内时,也会执行循环逻辑。do while语句比较适合为语句块定义宏:(1)有{}打包;(2)可自由跳出;(3)可作为语句添加分号。for语句中的符号是分隔符,三个表达式皆可省略。中间的控制表达式省略时,替换为非零常量。但其它控制表达式(while、if)不可省略,因为没有空表达式返回void,控制表达式外的括号也是不能省的。新规范中,for语句的第一个表达式可以为局部变量定义,但不可以同时有定义和表达式。为表达式时,其中的逗号是操作符,而为定义时则为分隔符。可以把整个for语句看成一个语句块,循环体是子语句。

    #define FUN()                   
    do {                            
        n++;                        
        if (0 == n) break;          
    } while (0)
    
    int n = 2;
    goto LABEL;
    while (n > 0) {LABEL: n--;}     // n = 0
    
    FUN();
    for ( ; ; );                    // the same as while (1);
    while () {}                     // illegal
    while 1 {}                      // illegal
    
    for (static int s; ; );         // illegal
    for (n = 2, int s; ; );         // illegal
    for (int s, n = 2; ; );         // ok, cover outer n
    for (int i = 0; i; i) {int i;}  // ok

    2.4 跳转语句

    jump_stt:
        goto label;
        continue;
        break;
        return [exp];

      跳转语句可以随意跳进跳出一般变量作用域,跳进时先分配变量空间,跳出时先释放变量空间。但可变长数组却是要动态生成的,所以不可以从外部跳进它的作用域(但可以跳出)。goto语句可以跳转到函数中任何一个label语句,包括其前面的语句。goto的使用应当以自然为准,不宜强制不用或多用。continue语句结束本轮循环体,break结束当前循环或switch语句。返回值不是void的函数必须return非空表达式,返回值为void的函数只能return空表达式或没有return语句。

    void f(void) {}                  // ok
    int f(void) {}                   // illegal
    int f(void) {return;}            // illegal
    
    int main(void)
    {
    {
    int b = 2; int vb[b]; LABEL: // can get by goto a = 1; // illegal from goto b = 1; // ok from goto } { int a = 2; int va[a]; goto LABEL; // ok for a b va, illegal for vb } for (int i; ; i++) { while (1) {break;} // to next while while (1) {goto END;} // good use of goto continue; // no i = 0, but do i++ i = 0; } END:;
    }
  • 相关阅读:
    distroless 镜像介绍及调试基于distroless 镜像的容器
    C# 设置或验证 PDF中的文本域格式 E
    Java 在PDF中添加工具提示|ToolTip E
    MongoDB Security
    Spring Boot MongoDB
    MongoDB 安装
    nginx重试机制proxy_next_upstream
    (转)VC中等比例缩放图像
    5 Ways You can Learn Programming Faster
    如何批量去除文件名中的某些字符串?
  • 原文地址:https://www.cnblogs.com/edward-bian/p/3870689.html
Copyright © 2011-2022 走看看