C笔记-左值与右值

前言:工欲善其事,必先利其器

两种资料

学习编程语言, 有两类资料可以让人"高潮".

​ 一类是针对初学者而设计的入门类书籍, 这种书总是适时地结合生动的生活实例, 来让啥都不懂的萌新理解一些基本的和关键的东西, 达到拨云见日的效果. 为将来的进一步学习培养出良好的兴趣和打下坚实的基础. 最具代表性的就是 headfirst 系列丛书.

​ 而另一类资料, 便是标准文献了. 它就像博学的导师或者修仙小说里的随身老爷爷, 能够完美地解答你的任何疑惑(就算有解答不了的问题, 那也是暂时的, 因为标准文献本身也是不断改进和迭代的).

​ 这边作者假设读者都有一定的C基础,不是啥都不懂的萌新, 但是对于左值和右值的概念仍存有疑惑的朋友, 另外作者水平有限, 如有错误和瑕疵, 欢迎各位朋友指正.


参考资料及其使用说明

参考资料

​ 本文的参考资料是C11标准文献草案(N1570), 是免费且几乎等同于C11标准文献的版本.

  • 外网版C11标准文献资料(需FQ)

    html版

    pdf版

  • 笔者提供的国内版(笔者自建站)

    html版

  • 笔者所提供的本地下载(7z压缩包, 内含pdf与html版)

    本地下载

本文的链接及资料使用说明

  • 本文链接说明

    本文的链接部分,均是国内html版的链接

  • 本地下载的资料说明

    • c11标准文献不仅每一个章节都有编号, 且每一个自然段都有编号,方便定位

    • c11标准的html版: 可以用锚点直接定位到对应章节, 自然段 以及 注解

      • 锚点: 形如 #6.3.1.2p3 的东西, 出现在网址栏的最后, 用于定位到网页中的位置(滚轮会自动滚到对应内容处)

      • c11标准html版的锚点构成说明:

        示例1: #6.3.1.2p3

        • 6.3.1.2是具体的章节编号: 第6章第3部分1小节第2节
        • p3是对应的自然段编号: p3代表第3自然段

        示例2: #note99

        • note99是对应的注解编号: note99代表第99个注解
      • 应用说明:

        • 查看国内版c11标准的第3章第2部分7小节第4自然段,可以直接输入以下网址: peterzhang.cool:3000/pdfs/c11.html#3.2.7p4,然后回车
        • 查看本地下载的c11的html版本也可以打开c11.html之后,在网址后面加上#3.2.7p4,然后按回车即可

官方对于左值和右值的定义

​ 可见, 左值右值的概念来自赋值表达式, =号左边的为左值(可修改的左值), 它代表(定位)了一个可用于存放数据的存储空间; 而右值通常被理解为 "表达式的值"(value of an expression).

实际使用时的疑问

​ 那么到底哪些是左值, 哪些又属于右值? 什么情况下属于左值, 什么情况下属于右值呢?


左值的涵盖范围

  • 变量名

  • 指针变量

  • 一些运算符的运算结果:

    • * -- 取内容运算符
    • [] -- 数组下标运算符
    • (type-name){initialize-list} -- 复合字面量
    • . (只有左操作数为左值时,结果才为左值)
    • ->(无论左操作数为左值还是右值,结果均为左值)

    举例说明:

    • a是数组名,绝大部分情况下属于指针值(见后续部分),是右值
    • a[1]属于运算符[]的结果, 属于左值, 可以放在等号左边进行赋值操作.

重要概念: 左值转化(lvalue conversion)

#6.3.2.1p2: 满足以下条件的左值会被转化成对应的存储空间(数据对象)中所存储的值,并且不再是一个左值, 这一过程被称为 左值转化

  • 不是 sizeof, _Alignof, &, ++, -- 运算符的操作数

  • 不是 . 或 赋值运算符的左操作数

  • 该左值不是数组类型(数组类型的左值按其他规定进行转化)

    • 一维数组: 不是数组名,但可以是数组元素

    • 多维数组: 不是任意N维(N>1)的数组名或数组元素,但可以是一维的数组元素

      (也就是说: 二维数组arr[][]中, arr[1]仍旧代表一个数组, 等同于一个数组名,不满足左值不是数组类型的条件)


左值与指针

概念上的区别

  • 左值: 可以放在赋值号的左边, 与一个存储单元(数据对象)对应, 代表了可直接获取和设置该单元内容的途径. (左值就像是一个已经拨通且未挂断的电话)
  • 指针值: 某一数据的存储位置的信息. (指针值就像是一个电话号码)

通过左值, 你可以通过它直接获取和设置存储单元(数据对象)中的内容, 就像你可以直接问已拨通电话的另一头问题或告诉另一头一些信息; 而指针值, 就像一个电话号码, 想要像左值那样获取或设置内容, 必须先要 "按照号码拨打电话", 这一步骤通常由取内容运算符 * 完成. 如果我们用另一个变量保存这个 "电话号码", 这个变量就成了 "指针变量".

注意: 指针变量是一个变量, 它是左值, 而指针值并不是左值.

举例: (我们把其他人当作是一个存储空间,而你扮演主程序)

你正在跟小张通电话 -- 左值 <==> int a;

你手里有小张的电话号码 -- 指针值 <==> &a;

你通过给小刘打电话,获取了小张的电话号码,然后再给小张打电话告诉他一些事 -- 利用指针变量 <==> int *p = &a; *(p) = 314;

左值与指针值的互相转化

我们声明的变量名是一类天然的左值, 它就像是我们和朋友直接面对面说话(或者一通已打通的电话); 而有时候,我们需要交谈的对象并不在我们身边, 这时候就需要我们自己去拨打电话.

  • 将指针值转化为对应的左值: 取内容运算符*
  • 获取某一左值的指针值: 取地址运算符&

指针值的构成

补充知识:存储单元的地址编排

  • 地址编号是基于字节的: 一个字节对应一个地址编号, 地址值(指针值)只能指向单个字节

  • 除了char外,C中的数据类型是多字节

  • 读取多字节数据的策略:

    • 地址值(指针值)指向存储单元的第一个字节

    • 定义一个取值范围, 说明取得数据的长度

指针值的构成

  • 指针值/地址值: 指向存储空间的起始字节
    • 指针值的存储类型是无符号的多字节数值
    • 指针指向的类型(int *p;中的int)并不影响指针值的sizeof大小
  • 指针指向的类型: 规定 利用指针进行一次内容读取/内容设置 所影响的字节范围
    • 一次读取或设置: 同时操作包含起始字节在内的N个字节(N由指针指向的类型确定)
    • 指针变量增加或减少1: 地址值/指针值增加或减少N

图示:

测试代码: test.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    short int test = 314;
    int *pInt = &test;
    float *pFloat;
    double *pDouble;
    long double *pLongDouble;

    printf("The sizeof short int is %d\n",sizeof(short int)); //2

    //指针(地址)是一个独立的数据存储类型,类似于int,double等,占用的内存大小相同
    printf("The sizeof pInt is %d\n",sizeof(pInt)); //4
    printf("The sizeof pFloat is %d\n",sizeof(pFloat)); //4
    printf("The sizeof pDouble is %d\n",sizeof(pDouble)); //4
    printf("The sizeof pLongDouble is %d\n",sizeof(pLongDouble)); //4

    //指针指向的类型确定读取的字节范围
    printf("The address of test is %p\n",pInt);
    printf("Input the address above and use it without a type bounded:\n");
    unsigned long long p;
    scanf("%x",&p); //手动输入上面打印的地址值
    printf("The value of p is %lx\n",p);
    printf("The value of *(short int *)p is %d\n",*(short int *)p); //314(10)
    printf("The value of *(char *)p is %d\n",*(char *)p); //只读取后8位,所以是58(10)

    //指针变量+1,指针值/地址值的变化?
    short int *pTest = &test;
    printf("The address of test is %p\n",pTest);
    pTest++;
    printf("The address of test now is %p\n",pTest);

    getchar();
    return 0;
}

控制台输出:

数组名与数组下标运算

#6.3.2.1p3: 满足下列条件的数组类型值(通常是数组名)会被转换为一个指向该数组首个元素的首个字节的指针值(注意,不是指针变量而是指针值):

  • 数组名不是sizeof或&的操作数
  • 不是用来初始化一个数组的数组字面量

因此:

  • 数组名本身是属于左值的, 但是这并没有什么卵用

  • 因为绝大多数情况下(包括位于赋值号左边的时候),数组名会被转换为指针值(不再是左值)

  • 数组名经过下标[]运算或*运算符却会变成左值,代表数组内某一元素,可以用于赋值


运算符归纳表格及实例说明

各种运算符运算结果左右值类型总结表

实例分析

  • 复合字面量(compound literial)

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int p = ((int){314})++; //works just fine
        printf("p is %d\n",p); //314
    
    	//int *p = ((int [2]){314,110})++; //error: lvalue required as increment operand
    
        getchar();
        return 0;
    }
    

    分析:

    • int p = ((int){314})++;

      复合字面量(int){314}生成一个未命名的左值(其值为314)

      对该左值应用后缀形式的++运算符,生成一个右值(314)

      将该右值赋值给变量p

    • int *p = ((int [2]){314,110})++; //报错语句

      复合字面量(int [2]){314,110}生成一个未命名的数组左值

      数组左值经过转化,变成指向该数组第一个元素的指针值(右值)

      对该指针值应用后缀++运算符报错(++运算符的操作数必须是左值)

  • 结构体相关运算符(*与->)

    结构体运算符 . :

    #include <stdio.h>
    #include <stdlib.h>
    
    //声明结构体s
    struct s { double i; };
    
    //声明联合体g
    union {
        struct {
            int f1;
            struct s f2;
        } u1;
        struct {
            struct s f3;
            int f4;
        } u2;
    } g;
    
    struct s f(void){ //返回结构体s的函数
        return g.u1.f2; //返回g.u1.f2
    }
    
    int main(void)
    {
        //测试: 结构体变量
        struct s varible = {3.1415};
        varible.i++;
        printf("varible.i.i is %f\n",varible.i); //4.1415
    
        //测试: 结构体返回值函数
        struct s f(void);
        //f().i = 20.0; //error: lvalue required as left operand of assignment
    
        getchar();
        return 0;
    }
    

    分析:

    • varible.i++;语句工作正常: 说明其执行结果为左值
    • f().i = 20.0;语句报错: 说明f().i不是左值
      • 函数调用的返回值是右值(尽管它返回的是文件域的联合体变量的成员的内容)
      • 右值.i,根据C11标准的规定,其执行结果也是右值,因此报错

    结构体指针运算符->:

    #include <stdio.h>
    #include <stdlib.h>
    
    struct s { double i; };
    union {
        struct {
            int f1;
            struct s f2;
        } u1;
        struct {
            struct s f3;
            int f4;
        } u2;
    } g;
    
    struct s * f(void){ //返回结构体指针的函数
        return &(g.u1.f2);
    }
    
    int main(void)
    {
        //测试: 结构体指针返回值函数
        struct s * f(void);
        f()->i = 20.0;//结构体指针指向的成员是左值
        printf("return value is: %f\n", f()->i);
        struct s newS = {3.14};
        *(f()) = newS; //函数返回的结构体指针也是右值,用*之后才变为左值
        printf("Now,value is: %f\n", f()->i);
    
        getchar();
        return 0;
    }
    
posted @ 2020-07-31 19:28  peterzhangsnail  阅读(1121)  评论(0编辑  收藏  举报