• 【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:;
    }
  • 相关阅读:
    Java高级之类结构的认识
    14.8.9 Clustered and Secondary Indexes
    14.8.4 Moving or Copying InnoDB Tables to Another Machine 移动或者拷贝 InnoDB 表到另外机器
    14.8.3 Physical Row Structure of InnoDB Tables InnoDB 表的物理行结构
    14.8.2 Role of the .frm File for InnoDB Tables InnoDB 表得到 .frm文件的作用
    14.8.1 Creating InnoDB Tables 创建InnoDB 表
    14.7.4 InnoDB File-Per-Table Tablespaces
    14.7.2 Changing the Number or Size of InnoDB Redo Log Files 改变InnoDB Redo Log Files的数量和大小
    14.7.1 Resizing the InnoDB System Tablespace InnoDB 系统表空间大小
    14.6.11 Configuring Optimizer Statistics for InnoDB 配置优化统计信息用于InnoDB
  • 原文地址:https://www.cnblogs.com/edward-bian/p/3870689.html
Copyright © 2020-2023  润新知