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

两种资料

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

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

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

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


参考资料及其使用说明

参考资料

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

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

    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)


左值与指针

概念上的区别

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

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

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

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

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

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

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

左值与指针值的互相转化

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

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

指针值的构成

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

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

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

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

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

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

      C笔记-左值与右值-LMLPHP

指针值的构成

  • 指针值/地址值: 指向存储空间的起始字节

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

图示:

C笔记-左值与右值-LMLPHP

测试代码: 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 //指针类型(地址类型)是一个独立的存储类型,占用的内存大小相同
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;
}

控制台输出:

C笔记-左值与右值-LMLPHP

数组名与数组下标运算


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

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

C笔记-左值与右值-LMLPHP

实例分析

  • 复合字面量(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标准的规定,其执行结果也是右值,因此报错
05-08 08:37