前言:工欲善其事,必先利其器
两种资料
学习编程语言, 有两类资料可以让人"高潮".
一类是针对初学者而设计的入门类书籍, 这种书总是适时地结合生动的生活实例, 来让啥都不懂的萌新理解一些基本的和关键的东西, 达到拨云见日的效果. 为将来的进一步学习培养出良好的兴趣和打下坚实的基础. 最具代表性的就是 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)
左值与指针
概念上的区别
- 左值: 可以放在赋值号的左边, 与一个存储单元(数据对象)对应, 代表了可直接获取和设置该单元内容的途径. (左值就像是一个已经拨通且未挂断的电话)
- 指针值: 某一数据的存储位置的信息. (指针值就像是一个电话号码)
通过左值, 你可以通过它直接获取和设置存储单元(数据对象)中的内容, 就像你可以直接问已拨通电话的另一头问题或告诉另一头一些信息; 而指针值, 就像一个电话号码, 想要像左值那样获取或设置内容, 必须先要 "按照号码拨打电话", 这一步骤通常由取内容运算符 * 完成. 如果我们用另一个变量保存这个 "电话号码", 这个变量就成了 "指针变量".
注意: 指针变量是一个变量, 它是左值, 而指针值并不是左值.
举例: (我们把其他人当作是一个存储空间,而你扮演主程序)
你正在跟小张通电话 -- 左值 <==> int a;
你手里有小张的电话号码 -- 指针值 <==> &a;
你通过给小刘打电话,获取了小张的电话号码,然后再给小张打电话告诉他一些事 -- 利用指针变量 <==> int *p = &a; *(p) = 314;
左值与指针值的互相转化
我们声明的变量名是一类天然的左值, 它就像是我们和朋友直接面对面说话(或者一通已打通的电话); 而有时候,我们需要交谈的对象并不在我们身边, 这时候就需要我们自己去拨打电话.
- 将指针值转化为对应的左值: 取内容运算符*
- 获取某一左值的指针值: 取地址运算符&
指针值的构成
补充知识:存储单元的地址编排
地址编号是基于字节的: 一个字节对应一个地址编号, 地址值(指针值)只能指向单个字节
除了char外,C中的数据类型是多字节
读取多字节数据的策略:
地址值(指针值)指向存储单元的第一个字节
定义一个取值范围, 说明取得数据的长度
指针值的构成
- 指针值/地址值: 指向存储空间的起始字节
- 指针值的类型是无符号的多字节数值
- 指针的类型并不影响指针值的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
//指针类型(地址类型)是一个独立的存储类型,占用的内存大小相同
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;
}
控制台输出:
数组名与数组下标运算
运算符归纳表格及实例说明
各种运算符运算结果左右值类型总结表
实例分析
复合字面量(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标准的规定,其执行结果也是右值,因此报错