C笔记-左值与右值
前言:工欲善其事,必先利其器
两种资料
学习编程语言, 有两类资料可以让人"高潮".
一类是针对初学者而设计的入门类书籍, 这种书总是适时地结合生动的生活实例, 来让啥都不懂的萌新理解一些基本的和关键的东西, 达到拨云见日的效果. 为将来的进一步学习培养出良好的兴趣和打下坚实的基础. 最具代表性的就是 headfirst 系列丛书.
而另一类资料, 便是标准文献了. 它就像博学的导师或者修仙小说里的随身老爷爷, 能够完美地解答你的任何疑惑(就算有解答不了的问题, 那也是暂时的, 因为标准文献本身也是不断改进和迭代的).
这边作者假设读者都有一定的C基础,不是啥都不懂的萌新, 但是对于左值和右值的概念仍存有疑惑的朋友, 另外作者水平有限, 如有错误和瑕疵, 欢迎各位朋友指正.
参考资料及其使用说明
参考资料
本文的参考资料是C11标准文献草案(N1570), 是免费且几乎等同于C11标准文献的版本.
本文的链接及资料使用说明
-
本文链接说明
本文的链接部分,均是国内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; }
欢迎转载,转载请注明出处