首页 理论教育 Arduino单片机实战开发技术:理解左值和右值

Arduino单片机实战开发技术:理解左值和右值

时间:2023-10-23 理论教育 版权反馈
【摘要】:图3-3左值图示请注意,我们用问号标记了右值。这意味着右值更改为10。理解左值和右值对于真正理解Arduino C非常重要,因此,笔者开发了Bucket的类,以便于记住关于左值和右值的详细信息。图3-5存储桶示例Bucket类提供了以下三个结论:存储桶的大小取决于存储的数据类型;存储在内存中的bucket是数据项的左值;Bucket的内容是数据项的右值。

Arduino单片机实战开发技术:理解左值和右值

左值是指特定数据项驻留在内存中的内存位置。因此,数据项的左值是存储该项的内存位置。对于左值是编译器确定定义val的语句,在语法上是正确的,随后编译器要做的下一件事是检查你是否已经定义了一个名为val的变量。如果是,则在val的符号表中已经有一个条目。如果是这样,则获取val的“重复定义错误”。因为此时没有val的定义,所以到目前为止看起来不错。

到目前为止,符号表看起来如表3-5所示。

表3-5 对val进行语法检查后的符号表

需要注意的是,val的左值仍然未知。

不过,由于没有重复定义错误,编译器会向操作系统发送一条系统消息(我们假设你的电脑正在运行某些版本的Windows)。本质上,编译器发送给Windows的消息说:“嘿,Windows!是我……Arduino。我的程序员需要两个字节的空闲存储空间,你能满足我的要求吗?”这时,Windows将信息交给Windows内存管理器,然后扫描可用内存列表,可能会找到两个可用字节在某处。我们将假定它找到的空闲内存驻留在起始内存地址20200处。这个Windows内存管理器向Windows返回一条带有20200内存地址的消息。然后,Windows向Arduino发送一条消息:“嘿,Arduino!是我……Windows。你可以使用从内存地址20200开始的内存。”此时,编译器将表3-4更改为表3-6。

表3-6 添加新变量val后的符号表

注意,这里发生的事情:就是符号表中增加了一个新变量val及所在的内存地址,也就是确定了变量val内存地址或左值。因此:

(1)当且仅当数据项在符号表中具有已知左值时,才定义该数据项;

(2)如果数据项存在于符号表中,但没有指定的左值,则声明该数据项。

然而,现在请记住数据定义意味着你可以使用变量的左值来定位变量。数据声明只不过是一个数据项的属性列表。也就是说,数据项的数据声明告诉你其ID、类型和范围级别,但它不存在于内存中。数据声明主要用于类型检查。

我们可以用一个简单的图表来描述左值,如图3-3所示。图3-3反映了符号表如表3-6所示。定义它,val是因为它有一个已知的左值,因此,存在于内存中(左值源于旧的汇编语言编程时代,并一直存在用于“位置值”,或数据项存储在内存中的引用。一些学生找到了它更容易记住“左值”,因为左值构成了图3-3的“左腿”)。

图3-3 左值图示

请注意,我们用问号标记了右值。原因是使用了右值表示在左值的内存位置实际存储的内容。右值是存储在数据中心的值项的左值或其内存位置。因为不需要Arduino C将非静态数据项的右值初始化为零,或任何其他特定值定义时,应始终假定数据项包含任何随机位模式都可能存在于其左值,直到一个值被显式分配到数据中项目。鉴于此,我们将val的右值显示为问号(rvalue也是汇编语言编程时代的遗留问题,代表“寄存器值”)。

假设要将值10赋给val。执行此操作的语句是:

val=10;

同样,这是一个简单的语句,包含一个表达式和二进制赋值运算符。但是,停下来想想,编译器必须做些什么来处理该语句。

(1)编译器必须检查语句的语法错误。那里没问题了就下一步。

(2)编译器必须转到其符号表,查看名为val的变量是否存在。同样,一切看起来都很好,因为val在symbol表中。

(3)它确保val有一个有效的左值(内存地址),它会这样做(即,内存地址20200)。如果左值列为空(左值中的所有行创建表时,符号表中的列初始化为null。因为null永远不是有效的内存地址),编译器会知道这是一个数据声明,并且变量尚未定义。应该清楚的是未定义的变量不能有赋值。然而,因为val有一个有效的内存地址,编译器可以处理分配声明。

(4)为了处理赋值语句,编译器转到数据项的左值并复制赋值右侧的值语句(即10)插入左值内存位置的2字节内存中。这意味着右值更改为10。如图3-4所示。如果你可以查看内存位置20200和20201,然后你将看到:000010100(大多数PC首先存储低字节,然后存储高字节。最后结果是相同的:值10存储在左值20200)。

图3-4 右值存储

请注意,每当你的程序需要使用存储在val中的数据时,它都必须进行符号表查找,以找到val的左值,转到该内存地址,并从中获取数据的“int字节”(每个int为2字节)那个存储位置(笔者在这里有一些提示,因为实际处理是在你的计算机上进行的Arduino板,而不是PC,存储位置在运行时是已知的,这里有助于你了解变量和内存在程序中的关系)。

理解左值和右值对于真正理解Arduino C非常重要,因此,笔者开发了Bucket的类,以便于记住关于左值和右值的详细信息。假设你有一堆大小不一的桶摆在周围。每个存储桶的大小刚好足以容纳特定数量的数据字节。某些存储桶只能保存1字节的数据,而其他存储桶只能保存2字节的数据。还有一些可以保存4个字节,以此类推。使用表3-1,你可以看到一个1字节的存储桶可以保存一个字节、字符、无符号字符或布尔数据项。一个2字节的存储桶可以用来保存整数、无符号整数或Word数。4字节存储桶可用于长、无符号长、浮点或双精度存储。让我们进一步假设你有一整个房间装满了这些不同大小的桶。

现在考虑下面的程序语句:

int val;

val=10;

这些都是我们之前讨论过的,第一条语句填充符号表。然而,在桶类中,可以想到第一个语句确定存储桶(一个2字节的存储桶)的大小以及存储桶在内存中的位置。

第二条语句意味着我们转到位于内存地址20200的val的bucket,然后将2字节数据放入存储桶中,数据以形成值10的模式排列,如图3-5所示。该图还显示了一个存储在内存位置20000的1字节存储桶,带有负号签名字符存储在其中。存储在内存地址20000处的存储桶只有所用存储桶的一半大存储val。(www.xing528.com)

图3-5 存储桶示例

Bucket类提供了以下三个结论:

(1)存储桶的大小取决于存储的数据类型;

(2)存储在内存中的bucket是数据项的左值;

(3)Bucket的内容是数据项的右值。

在程序中使用变量的任何时候,都可能是在使用某个特定的Bucket的左值,并在某个表达式中使用该Bucket的内容(即其右值)来定位该Bucket。

还应该清楚的是,此代码片段中的最后一条语句使用左值和右值:

int val=10;

int sum;

sum=val;

在最后一条语句中,编译器先转到符号表并找到val的左值,再使用获取val的2字节存储桶的内存地址(val的左值),接着它查找sum的左值,并转到该值内存地址,并获取其存储桶。既然两个操作数都可用,赋值运算符使编译器将val的Bucket中的内容倒入sum的Bucket中。因此,这一过程取代了sum的桶里可能有的东西和val的桶里的东西。

这里有一个重要的提示:所有简单赋值语句都将赋值运算符右侧的Bucket内容移动到赋值运算符左侧的操作数Bucket中。显然,所有简单赋值语句都会将右操作数的右值移到左操作数的右值中(从右值和左值的角度思考,读者对Arduino C有一个准确的理解)。

请看以下程序:

int val;

long bigVal=100000;

val=bigVal;

前两条语句为val和bigVal创建Bucket,并将它们放在内存中的某个位置。作为其定义的一部分,bigVal还将其右值初始化为100000。现在想想最后一条语句会发生什么:

val=bigVal;

简单地说,该语句获取bigVal存储桶,并尝试将4字节的数据倒入2字节的存储桶中。这样做会有丢失2字节信息的风险,因为val的Bucket太小保存不了bigVal的所有数据。虽然100000是一个很容易由长数据类型处理的数值,但是从表3-1可以看出,值太大,无法存储在val中。更糟糕的是,Arduino C编译器甚至没有“抱怨”分配错误问题。因此,val现在包含一些伪值,这些伪值可能会导致你的应用程序出现问题。显然,这只是程序员的一个糟糕设计,他们应该知道这一点:100000不适合整数。

当然,编译器仍然不会“抱怨”,尤其是现在这个值足够小,可以容纳两种数据类型。即使如此,在分配过程中,2字节的数据仍将被“倾倒到地板上”。请注意真相,这是编译器中的一个错误。在其他语言编译器中尝试类似的赋值,然后将收到一条错误消息。那么,要解决这个问题什么方法才是正确的,并使任何编译器都不会“抱怨”呢?

解决方法是使用cast操作符。强制转换用于将一种数据类型强制转换为另一种数据类型。如果我们想修复bigVal赋值为20时的问题,然后我们可以按以下方式使用强制转换:

val=(int)bigVal;

上面的强制转换运算符为(int)。要使用强制转换,只需运用于开始括号和结束括号。强制转换运算符的数据类型(即int)必须与数据类型匹配即接收强制转换的结果(int中的val)。也就是说,如果将值赋给int,则强制转换也必须是int。强制转换运算符必须直接放置在要转换的数据项的前面,要转换为新的数据类型。在本例中,(int)强制转换必须出现在bigVal之前。

假设稍后在程序代码中我们看到如下内容:

bigVal=val;

在本例中不需要强制转换的原因是,你试图将2字节桶中的内容“倒”入4字节桶中。因为接收桶比发送桶大,所以不存在将数据“泄漏到地板上”的风险。我没有发现任何编译器“抱怨”这种类型的不匹配数据赋值,尽管代码隐式地将int改为long。换句话说,编译器在不告诉你数据的情况下强制转换数据。这被称为无声操作,因为没有迹象表明操作正在进行。

因此,在两种不同的数据类型之间使用赋值语句时,应始终使用强制转换。你应将上述语句改写为:

bigVal=(long)val;

如果没有其他内容的话,本文将说明你确实希望强制int的数据进入long。Arduino编译器不会“抱怨”嘈杂或无声的强制转换,这是“嘈杂”强制转换的一个缺陷。为了安全起见,在执行涉及两种不同数据类型的赋值表达式时,请始终使用cast运算符。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈