Skip to content

表达式

表达式是任何编程语言的基本要素。无论算法背后的应用思想是什么,最终都会归结为数据处理,也就是计算。表达式描述了如何从一个或多个预定义的值计算出某个结果。这些值被称为操作数,而对它们执行的操作则用运算符来表示。

在表达式中,使用独立的字符或字符序列作为运算符来操作操作数,例如“+”用于加法,“*”用于乘法。这些运算符分为几个组,如算术运算符、位运算符、比较运算符、逻辑运算符和一些专用运算符。

在本书前面的章节中,我们已经使用过表达式,例如用于初始化变量。在最简单的情况下,表达式是一个常量(字面量),它是唯一的操作数,而计算结果等于该操作数的值。然而,操作数也可以是变量、数组元素、函数调用的结果(在表达式中直接调用函数)、嵌套表达式和其他实体。

所有运算符都会将其结果代入到父表达式中,直接替换操作数所在的位置,这使得我们可以将它们组合起来,形成相当复杂的层次结构。例如,在下面的表达式中,变量b和c相乘的结果会加到变量a的值上,然后将得到的值存储在变量v中:

cpp
v = a + b * c;

在本节中,我们将讨论构造和计算各种表达式的一般原则,以及MQL5中为内置类型支持的标准运算符集。稍后,在面向对象编程部分,我们将了解如何为自定义类型(即结构体和类)重载(重新定义)运算符,这将使我们能够在表达式中使用对象并对它们执行非标准操作。

基本概念

在深入探讨具体的运算符类别之前,我们需要先引入一些所有运算符都具备的基本概念,这些概念会影响运算符在特定上下文中的适用性和行为。

首先,根据所需操作数的数量,运算符可分为一元运算符和二元运算符。从名称就可以明显看出,一元运算符处理一个操作数,而二元运算符处理两个操作数。对于二元运算符,它总是位于两个操作数之间。在一元运算符中,有些必须放在操作数之前,有些则放在操作数之后。例如,一元负号(-)运算符可以改变值的符号:

cpp
int x = 10;
int y = -x;  // -10

同时,还有一个使用相同字符-的二元减法运算符。

cpp
int z = x - y; // 10 - -10 -> 20

编译器在特定上下文中选择正确的运算符(操作)是由该运算符在表达式中的使用上下文决定的。

每个运算符都被赋予了优先级。它决定了在包含多个运算符的复杂表达式中,运算符的计算顺序。优先级高的运算符先计算,优先级低的运算符最后计算。例如,在表达式1 + 2 * 3中,有两个操作(加法和乘法)和三个操作数。由于乘法的优先级高于加法,所以会先计算2 * 3的积,然后再将其与 1 相加。

稍后我们将提供完整的带优先级的运算符表。

此外,每个运算符都有结合性的特点。结合性可以是左结合或右结合,它决定了具有相同优先级的连续运算符的执行顺序。例如,表达式10 - 7 - 1从理论上讲可以有两种计算方式:

  1. 先从 10 中减去 7,得到 3,然后再从 3 中减去 1,结果为 2;
  2. 先从 7 中减去 1,得到 6,然后再从 10 中减去 6,结果为 4。

在第一种情况中,计算是从左到右进行的,这符合左结合性;因为减法操作是左结合的,所以第一种答案是正确的。

第二种计算方式符合右结合性,但不会被采用。

让我们再看一个同时涉及优先级和结合性的例子:11 + 5 * 4 / 2 + 3。加法和乘法这两种操作都是从左到右执行的。如果它们的优先级没有差异,计算结果会是 35,但正确答案是 24。如果将结合性改为右结合,结果会是 14。

为了明确地重新定义表达式中的优先级,可以使用括号,例如:(11 + 5) * 4 / (2 + 3)。括号内的内容会先计算,中间结果会代入表达式中用于其他操作。括号内的分组可以嵌套。更多细节,请参阅“使用括号进行分组”部分。

逻辑非一元运算符!就是右结合性运算符的一个例子。本质上,它的任务是将真变为假,反之亦然。与其他一元运算符一样,在这种情况下,结合性意味着操作数必须放在运算符的哪一侧。符号!放在操作数之前,即操作数在右侧。

cpp
int x = 10;
int on_off = !!x;  // 1

在这个例子中,进行了两次逻辑非操作:第一次针对变量x(右边的!),第二次针对前一次取反的结果(左边的!)。这种双重取反操作可以将任何非零值转换为 1,因为会先转换为bool类型再转换回来。

最终的运算符表也会显示结合性。

最后,处理表达式时另一个不可忽视的要点是操作数的计算顺序。这应该与属于运算符(而非操作数)的优先级区分开来。二元操作的操作数计算顺序并没有明确规定,这为编译器提供了优化代码和提高效率的空间。编译器只保证在执行操作之前会计算操作数。

有一组有限的操作,其操作数的求值顺序是有定义的。特别是,对于逻辑与(&&)和逻辑或(||),求值顺序是从左到右,如果由于左边部分的值而使右边部分不影响结果,那么右边部分可能会被省略。但就三元条件运算符? :而言,顺序更为复杂,因为根据第一个条件的真假,在计算时会计算其中一个分支。更多细节请参阅后续章节。

操作数的求值顺序可以通过表达式中存在多个函数调用的情况来说明。例如,假设表达式中使用了 4 个函数:

cpp
a() + b() * c() - d()

优先级和结合性规则仅适用于调用这些函数的中间结果,而函数调用本身可以由编译器根据源代码的特点和编译器设置以任何它“认为必要”的顺序生成。例如,参与乘法运算的函数bc可以按照[b(), c()]的顺序调用,或者反之,按照[c(), b()]的顺序调用。如果函数在执行过程中可能会影响相同的数据,那么在计算表达式时,数据的状态将是不明确的。

在处理数组和递增运算符时(请参阅“递增和递减”)也会出现类似的问题。

cpp
int i = 0;
int a[5] = {0, 1, 2, 3, 4};
int w = a[++i] - a[++i];

根据先计算左边还是右边的差值操作数,我们可能会得到-1a[1] - a[2])或+1a[2] - a[1])。由于 MQL5 编译器在不断改进,不能保证当前的结果(-1)在未来仍然保持不变。

为了避免潜在的问题,建议如果一个操作数已经在同一个表达式中被修改过,就不要再重复使用它。

在所有表达式中,通常会有不同类型的操作数。这就需要在对它们执行任何操作之前,将它们转换为某种通用类型。如果没有显式类型转换,MQL5 会在必要时执行隐式转换。此外,不同类型组合的转换规则也不同。显式和隐式类型转换将在相关部分讨论。

赋值操作

表达式的计算结果通常必须存储在某个地方。在编程语言中,赋值运算符 = 就是用于这个目的的。在它左边放置的是变量名或数组元素名,用于存储结果,而右边则是表达式(实际上就是计算的公式)。

我们已经在变量初始化时使用过这个运算符,变量初始化只在创建变量时执行一次。然而,赋值操作允许在算法执行过程中任意多次地改变变量的值。例如:

cpp
int z;
int x = 1, y = 2;
z = x;
x = y;
y = z;

变量 xy 分别被初始化为值 1 和 2,然后使用了一个辅助的第三个变量 z 和三次赋值操作来交换 xy 的值。

赋值运算符和所有运算符一样,会将其结果返回给表达式。这使得我们可以按顺序编写赋值语句。

cpp
int x, y, z;
x = y = z = 1;

在这里,1 会首先被赋值给变量 z,然后赋值给变量 y,最后赋值给变量 x。显然,这个运算符是右结合的,因为在表达式中被赋值的值是从右向左传递的。

我们可以将赋值操作作为表达式的一部分。但是,由于它的优先级低于所有其他运算符(除了“逗号”运算符,见“运算符优先级”),所以必须将其用括号括起来(更多细节,请参阅关于“使用括号进行分组”的部分)。这就可能导致一种情况,即在表达式中如果误将 == 写成了 =,就会导致语句不能按预期执行。在处理 if 语句的部分可以看到这种行为的示例。

赋值运算符对 = 左边和右边能放置的内容有一定的限制。在编程中,为了便于描述存储相关的概念,这些实体被精确地命名为:左值(LValue)和右值(RValue)(基于“左”和“右”)。

左值和右值

左值(LValue)表示一个已分配内存的实体,因此可以向其中写入值。变量和数组元素是左值的常见示例。在学习了面向对象编程(OOP)之后,我们会了解到这个类别中的另一个代表:对象,在对象中可以重载赋值运算符。左值的一个必要元素是存在一个标识符。

需要注意的是,变量和数组可能会用关键字 const 来声明,这样它们就不能作为左值,因为常量是禁止被修改的。

右值(RValue)是在表达式中使用的临时值,例如字面量、函数调用返回的值,或者表达式片段的计算结果。

左值类别具有扩展性,即属于左值的对象可以放在 = 的左边,但并不禁止它和右值一样,也可以放在 = 的右边。

而右值类别则具有限制性,即任何右值只能放在 = 的右边。

当某个左值元素被用在 = 的右边时,它的标识符实际上表示的是其当前存储在表达式公式中的内容。

然而,如果左值元素被用在 = 的左边时,它的标识符表示的是一个内存地址(存储单元),新的值(表达式的计算结果)应该被写入这个地址中。

不同的运算符对于左值或右值操作数的使用有不同的限制。例如,递增 ++ 和递减 -- 运算符(见“递增和递减”)只能用于左值。

下面是一些关于赋值运算符使用的允许和不允许的示例(脚本 ExprAssign.mq5):

cpp
// 变量声明
const double cx = 123.0;
int x, y, a[5] = {1};
string s;
// 赋值
a[2] = 21;       // 正确
x = a[0] + a[1] + a[2]; // 正确
s = Symbol();    // 正确
cx = 0;          // 常量变量不能被修改
                 // 错误: 'cx' - 常量不能被修改
5 = y;           // 5 – 这个数字(字面量)
                 // 错误: '5' - 需要左值
x + y = 3;       // 在右值(表达式计算结果)的左边
                 // 错误: 需要左值
Symbol() = "GBPUSD"; // 在函数调用结果(右值)的左边  
                     // 错误: 需要左值

编译器会返回违反运算符使用规则的错误。

算术运算

算术运算包括 5 种二元运算,即加法、减法、乘法、除法和取模运算,以及 2 种一元运算,即正号和负号。每种运算所使用的符号如下表所示。

在包含示例的列中,e1e2 是任意子表达式。结合性用 L(从左到右)和 R(从右到左)标记。第一列中的数字可被视为执行运算的优先级。

优先级 (P)符号描述示例结合性 (A)
2+一元正号+e1R
2-一元负号-e1R
3*乘法e1 * e2L
3/除法e1 / e2L
3%取模运算e1 % e2L
4+加法e1 + e2L
4-减法e1 - e2L

表中的顺序对应于优先级从高到低:一元正号和负号先于乘法和除法进行计算,而乘法和除法又先于加法和减法进行计算。

cpp
double a = 3 + 4 * 5; // a = 23

实际上,一元正号在计算中没有任何实际作用,但可以用于使表达式更清晰易读。一元负号会改变其操作数的符号。

算术运算用于数值类型或可以转换为数值类型的数据。计算结果是一个右值。在计算过程中,整数操作数的存储位置通常会扩展为所使用的“最大”整数类型,或者扩展为 int 类型(如果所有整数类型的大小都较小),并且会转换为通用类型。更多详细信息,请参阅“类型转换”部分。

cpp
bool b1 = true;
bool b2 = -b1;

在这个示例中,变量 b1 会“扩展”为 int 类型,值为 1。取反后得到 -1,再转换回 bool 类型时,结果为 true(因为 -1 不为零)。不建议在算术计算中使用逻辑类型。

整数相除的结果是整数,也就是说,如果存在小数部分,将会被省略。可以使用脚本 ExprArithmetic.mq5 来验证这一点。

cpp
int a = 24 / 7;      // 正确: a = 3
int b = 24 / 8;      // 正确: b = 3
double c = 24 / 7;   // 正确: c = 3 (!)

尽管变量 c 被声明为 double 类型,但在初始化它的表达式中使用的是整数;因此,执行的除法是整数除法。要进行带小数部分的除法,至少有一个操作数必须是实数类型(另一个操作数也会被转换为该类型)。

cpp
double d = 24.0 / 7; // 正确: d = 3.4285714285714284

运算符 % 计算整数除法的余数(它仅适用于两个整数类型的操作数)。

cpp
int x = 11 % 5;   // 正确: x = 1
int y = 11 % 5.0; // 不能使用实数
                  // 错误: '%' - 非法操作使用

当操作数符号不同时,运算符 */ 的结果为负数。对于运算符 %,适用以下规则:

  • 如果运算符 % 的除数为负数,符号“被忽略”;
  • 如果运算符 % 的被除数为负数,结果为负数;

这可以通过取模运算的另一种计算方式轻松验证:m % n = m - m / n * n。需要记住的是,对于整数除法 m / n 会进行取整;因此,一般情况下 m / n * n 不等于 m

在“数组的特性”部分,我们深入探讨了多维数组可以通过重新计算其元素的索引来表示为一维数组的概念。我们还提供了通过二维数组的坐标(列号 X 和行号 Y,字符串长度为 N)在一维数组中获取索引的公式。

index = Y * N + X

运算符 % 允许我们进行更方便的反向计算,即通过索引找到 XY

Y = index / N
X = index % N

如果在计算表达式的某个阶段得到了不可表示的结果 NaN(非数字,例如无穷大、负数的平方根等),那么对其进行的所有后续运算也将产生 NaN。可以使用 MathIsValidNumber 函数(请参阅“数学函数”)将其与正常数字区分开来。

cpp
double z = DBL_MAX / DBL_MIN - 1; // inf: 非数字

在这里,从 NaN(由除法得到)中减去 1,结果还是 NaN

加法运算对于字符串是有定义的,它执行连接操作,即把字符串组合在一起。

cpp
string s = "Hello, " + "world!"; // "Hello, World!"

其他运算对于字符串是禁止的。

递增和递减

递增和递减运算符允许以简化的方式将操作数增加或减少 1。它们最常出现在循环内部,用于在访问数组或其他支持枚举的对象时修改索引。

递增用两个连续的加号 ++ 表示。递减用两个连续的减号 -- 表示。

这类运算符有两种类型:前缀和后缀。

顾名思义,前缀运算符写在操作数之前(++x--x)。它们会改变操作数的值,并且这个新值会参与表达式的进一步计算。

后缀运算符写在操作数之后(x++x--)。它们会将操作数当前值的副本代入表达式中,然后再改变操作数的值(新值不会参与表达式的计算)。脚本 ExprIncDec.mq5 中给出了简单的示例。

cpp
int i = 0, j;
j = ++i;       // j = 1, i = 1
j = i++;       // j = 1, i = 2

后缀形式对于更紧凑地编写结合了对操作数先前值的引用及其附带修改的表达式可能很有用(如果用其他方式记录同样的操作可能需要两条单独的语句)。在所有其他情况下,建议使用前缀形式(它不会创建“旧”值的临时副本)。

在下面的示例中,会依次反转数组元素的符号,直到找到第 0 个元素为止。通过 while 循环内部的后缀递增 k++ 来确保在数组索引中移动。由于是后缀形式,表达式 a[k++] = -a[k] 会首先更新第 k 个元素,然后将 k 增加 1。然后检查赋值结果是否不等于 0(!= 0,请参阅下一部分)。

cpp
int k = 0;
int a[] = {1, 2, 3, 0, 5};
while((a[k++] = -a[k]) != 0){}
// a[] = {-1, -2, -3, 0, 5};

下面的表格按优先级顺序列出了递增和递减运算符:

优先级 (P)符号描述示例结合性 (A)
1++后缀递增e1++L
1--后缀递减e1--L
2++前缀递增++e1R
2--前缀递减--e1R

所有递增和递减运算的优先级都高于算术运算。前缀的优先级低于后缀。在下面的示例中,x 的“旧”值与 y 的值相加,然后 x 递增。如果前缀的优先级更高,那么会先对 y 进行递增操作,然后 y 的新值 6 会与 x 相加,这样我们得到的结果就是 z = 6x = 0(之前的值)。

cpp
int x = 0, y = 5;
int z = x+++y; // "x++ + y" : z = 5, x = 1

比较运算

顾名思义,这些运算用于比较两个操作数,并根据比较中所满足的条件返回一个逻辑值,即真(true)或假(false)。

下面的表格列出了所有的比较运算及其属性,例如使用的符号、优先级、示例和结合性。

优先级 (P)符号描述示例结合性 (A)
6<小于e1 < e2L
6>大于e1 > e2L
6<=小于等于e1 <= e2L
6>=大于等于e1 >= e2L
7==等于e1 == e2L
7!=不等于e1 != e2L

每个运算的原理都是根据其描述列中的标准来比较两个操作数。例如,表达式 “x < y” 表示检查 “x 是否小于 y”。相应地,如果 x 确实小于 y,比较结果将为 true,否则为 false

比较运算适用于任何类型的操作数(对于不同类型,会进行类型转换)。

考虑到左结合性以及返回的 bool 类型结果,构造一系列比较运算的结果并不总是如预期那样。例如,一个用于检查值 y 是否在 xz 的值之间的假设表达式,看起来可能是这样:

cpp
int x = 10, y = 5, z = 2;
bool range = x < y < z;   // true (!)

然而,这样的表达式处理方式是不同的。甚至编译器也会给出警告:“在操作中不安全地使用了 bool 类型”。

由于左结合性,首先检查左边的条件 x < y,其结果作为 bool 类型的临时值代入到后续表达式 b < z 中。然后将 z 的值与临时变量 b 中的 truefalse 进行比较。要检查 y 是否在 xz 之间,应该使用两个比较运算并结合逻辑与(AND)运算(这将在下一部分讨论)。

cpp
int x = 10, y = 5, z = 2;
bool range = x < y && y < z;   // false

在使用相等/不相等比较时,需要考虑操作数类型的特点。例如,浮点数在计算后常常包含“近似”值(我们在“实数”部分讨论了 doublefloat 表示的精度)。例如,0.60.3 的和并不严格等于 0.9

cpp
double p = 0.3, q = 0.6;
bool eq = p + q == 0.9;        // false
double diff = p + q - 0.9;     // -0.000000000000000111

差值为 1*10-16,但这足以使比较运算返回 false

因此,在比较实数的相等/不相等时,应该使用大于/小于运算符来比较它们的差值,并根据计算的特点手动确定可接受的偏差,或者采用通用的偏差值。回想一下,对于 doublefloat,定义了内置的精度常量 DBL_EPSILONFLT_EPSILON,对于值 1.0 是有效的。在比较其他值时必须进行缩放。在脚本 ExprRelational.mq5 中,给出了用于比较实数的函数 isEqual 的一种可能实现,它考虑了这一点。

cpp
bool isEqual(const double x, const double y)
{
   const double diff = MathAbs(x - y);
   const double eps = MathMax(MathAbs(x), MathAbs(y)) * DBL_EPSILON;
   return diff < eps;
}

在这里,我们使用了获取绝对值的函数(MathAbs)和求两个值中较大值的函数(MathMax)。它们将在第四部分的“数学函数”部分进行描述。使用 < 运算符将函数 isEqual 的参数之间的绝对差值与变量 eps 中校准的容差进行比较。

无论如何,这个函数不能用于与绝对零进行比较。为此,可以使用以下方法(可能需要根据具体需求进行一些调整):

cpp
bool isZero(const double x)
{
   return MathAbs(x) < DBL_EPSILON;
}

字符串是按字典顺序进行比较的,即逐个字符进行比较。将每个字符的编码与第二个字符串中相同位置的字符编码进行比较。比较一直进行到找到编码不同的字符,或者其中一个字符串结束。字符串的大小关系将等于第一个不同字符的大小关系,或者较长的字符串将被认为大于较短的字符串。请记住,大写字母和小写字母的编码不同,而且奇怪的是,大写字母的编码小于小写字母的编码。

空字符串 ""(实际上,它存储了一个终止符 0)不等于表示没有字符串的特殊值 NULL

cpp
bool cmp1 = "abcdef" > "abs";     // false, [2]: 's' > 'c'
bool cmp2 = "abcdef" > "abc";     // true,  by length
bool cmp3 = "ABCdef" > "abcdef";  // false, by case
bool cmp4 = "" == NULL;           // false

此外,为了比较字符串,MQL5 提供了一些函数,这些函数将在“字符串操作”部分进行描述。

在进行相等/不相等比较时,不建议使用 bool 常量:truefalse。问题在于,在像 v == truev == false 这样的表达式中,操作数 v 可能会被直观地解释为逻辑类型,但实际上它是一个数字。众所周知,在数字中零值被认为是 false,而所有其他值都被解释为 true(我们经常希望用它来表示某些结果的存在或不存在)。然而,在这种情况下,类型转换是反向的:truefalse 被“扩展”为数字类型 v,实际上分别等于 10。这样的比较结果将与预期的不同(例如,比较 100 == true 将结果为 false)。

逻辑运算

逻辑运算对逻辑操作数进行计算,并返回相同类型的结果。

优先级 (P)符号描述示例结合性 (A)
2!逻辑非!e1R
11&&逻辑与e1 && e2L
12逻辑或

逻辑非运算将 true 转换为 false,将 false 转换为 true

逻辑与运算当两个操作数都为 true 时,结果为 true

逻辑或运算只要至少有一个操作数为 true,结果就为 true

与(AND)和或(OR)运算符总是从左到右计算操作数,并且在可能的情况下使用计算捷径。如果左边的操作数为 false,那么与运算符会跳过第二个操作数,因为它不会影响结果——结果已经是 false 了。如果左边的操作数为 true,那么或运算符会跳过第二个操作数,原因相同,因为无论如何结果都将为 true

这在程序中常被用于避免第二个(及后续)操作数出现错误。例如,我们可以防止访问不存在的数组元素的错误:

cpp
index < ArraySize(array) && array[index] != 0

这里我们使用了内置函数 ArraySize,它返回数组的长度。只有当 index 小于数组长度时,才会读取具有该索引的元素并与零进行比较。

使用逻辑或(||)进行反向检查也经常用到,例如:

cpp
ArraySize(array) == 0 || array[0] == 0

如果数组为空,条件立即为 true。只有当数组有元素时,才会继续对内容进行额外检查。

如果表达式由多个通过逻辑或组合的操作数组成,那么一旦遇到第一个 true(如果有的话),就会立即得到总的结果为 true。然而,如果操作数是通过逻辑与组合的,那么一旦遇到第一个 false,就会立即得到总的结果为 false

当然,在一个表达式中可以组合不同的运算,同时要考虑它们不同的优先级:先执行取反运算,然后是与相关的条件运算,最后是或相关的条件运算。如果需要其他顺序,必须使用括号显式指定。

例如,下面这个没有括号的表达式 A && B || C && D,实际上等同于 (A && B) || (C && D)。如果要先执行逻辑或运算,就应该把它用括号括起来:A && (B || C) && D。关于使用括号的更多细节,请参阅“使用括号进行分组”部分。

脚本 ExprLogical.mq5 中给出了一些简单的示例,用于实际检查逻辑运算。

cpp
int x = 3, y = 4, z = 5;
bool expr1 = x == y && z > 0;  // 错误,因为 x != y,z 的值无关紧要
bool expr2 = x != y && z > 0;  // 正确,两个条件都满足
bool expr3 = x == y || z > 0;  // 正确,只要 z > 0 就足够了
bool expr4 = !x;               // 错误,只有 x 为 0 时结果才为 true
bool expr5 = x > 0 && y > 0 && z > 0; // 正确,三个条件都满足
bool expr6 = x < 0 || y > 0 && z > 0; // 正确,y 和 z 的条件满足就足够了
bool expr7 = x < 0 || y < 0 || z > 0; // 正确,z 的条件满足就足够了

在计算 expr6 的代码行中,编译器会给出警告:“检查运算符优先级是否存在潜在错误;使用括号来明确优先级”。

逻辑运算 &&|| 不应与位运算 &| 混淆(位运算将在下一部分讨论)。

位运算

有时,你可能需要在位级别上处理数字。为此,有一组适用于整数类型的位运算。

下面的表格按照优先级顺序列出了所有位运算符的符号、描述、结合性。

优先级 (P)符号描述示例结合性 (A)
2~按位取反(求补)~e1R
5<<左移e1 << e2L
5>>右移e1 >> e2L
8&按位与e1 & e2L
9^按位异或e1 ^ e2L
10按位或`e1

在这一组运算中,只有按位取反运算 ~ 是一元运算,其他所有运算都是二元运算。

在所有情况下,如果操作数的大小小于 int/uint,会先通过在高位添加 0 位将其扩展为 int/uint。根据操作数是有符号/无符号类型,高位的位可能会影响符号。

标准的 Windows 应用程序“计算器”可以帮助理解数字在位级别上的表示。如果你在“查看”菜单中选择“程序员”操作模式,程序中会出现几组切换按钮,用于选择以十六进制(Hex)、十进制(Dec)、八进制(Oct)或二进制(Bin)形式表示数字。其中二进制形式会显示出各个位。此外,你还可以选择数字的大小:1、2、4 和 8 字节。这些按钮可以执行我们所讨论的所有运算:取反(~)、与(&)、或(|)、异或(^)、左移(<<)和右移(>>)。

由于计算器使用有符号数,当切换到十进制模式时可能会出现负值(记住,高位被解释为符号位)。为了便于分析,合理的做法是排除出现的负号,为此需要选择高一级的字节大小。例如,要检查范围到 255(uchar,无符号一字节整数)内的值,应该选择 2 字节(否则,只有直到 127 的十进制值是正数,其他值将显示在负数区域)。

按位取反会创建一个值,其中所有 1 位的位置变为 0 位,而 0 位的位置变为 1 位。例如,对所有位都为 0 的字节取反,会得到所有位都为 1 的字节。数字 50 的二进制形式为 00110010(字节)。对其取反得到 11001101

用十六进制表示的数字 1 是 0x0001(简写)。对这些位取反得到 0xFFFE(见脚本 ExprBitwise.mq5)。

cpp
short v = ~1;  // 0xfffe = -2
ushort w = ~1; // 0xfffe = 65534

按位与运算会检查两个操作数中的每一位,并且在两个操作数对应位都为 1 的位置,将 1 存储到结果中。在所有其他情况下(即只有一个操作数的对应位为 1 或者两个操作数的对应位都为 0),在结果中写入 0 位。

按位或运算如果在两个操作数中至少有一个操作数的对应位为 1,就会在结果中写入 1 位。

按位异或运算会在结果中,在第一个操作数或第二个操作数的对应位为 1(但不是两个操作数对应位同时为 1)的位置写入 1 位。下面展示了两个数字 XY 的二进制表示,以及对它们进行位运算的结果。

X       10011010   154
Y       00110111    55
 
X & Y   00010010    18
X | Y   10111111   191
X ^ Y   10101101   173

当使用多个不同的运算符编写复杂表达式时,为了避免混淆优先级,请使用括号进行分组。

移位运算将位向左(<<)或向右(>>)移动由第二个操作数定义的位数,第二个操作数必须是一个非负整数。结果是,向左(对于 <<)或向右(对于 >>)的位会被丢弃,因为它们超出了存储单元的边界。对于左移,会在右边添加相应数量的 0 位。对于右移,如果操作数是无符号的,则在左边添加 0 位;如果操作数是有符号的,则复制符号位。在后一种情况下,对于正数在左边添加 0 位,对于负数在左边添加 1 位;即符号保持不变。

cpp
short q = v << 5;  // 0xffc0 = -64
ushort p = w << 5; // 0xffc0 = 65472
short r = q >> 5;  // 0xfffe = -2
ushort s = p >> 5; // 0x07fe = 2046

在上面的示例中,最初的左移操作“破坏”了变量 p 的高位位,而随后相同位数的右移操作在高位填充了 0 位,这导致值从 0xffc0 减小到 0x07fe

移位的大小(位数)必须小于操作数类型的大小(考虑其潜在的扩展)。否则,所有初始位都会丢失。

移位 0 位会使数字保持不变。

按位运算 &| 不应与逻辑运算 &&|| 混淆(在前一节中已讨论)。

修改运算

修改运算也被称为复合赋值运算,它允许在一个运算符中结合算术运算或位运算与普通赋值运算。

优先级 (P)符号描述示例结合性 (A)
14+=加法赋值e1 += e2R
14-=减法赋值e1 -= e2R
14*=乘法赋值e1 *= e2R
14/=除法赋值e1 /= e2R
14%=取模赋值e1 %= e2R
14<<=左移赋值e1 <<= e2R
14>>=右移赋值e1 >>= e2R
14&=按位与赋值e1 &= e2R
14=按位或赋值`e1
14^=按位异或赋值e1 ^= e2R

这些运算符对操作数 e1e2 执行相应的操作,然后将结果存储在 e1 中。

e1 @= e2 这样的表达式(其中 @ 是上表中的任意一个运算符)大致等价于 e1 = e1 @ e2。这里用“大致”一词是为了强调存在一些细微的差别。

首先,如果 e2 的位置是一个运算符优先级低于 @ 的表达式,e2 仍然会在 @ 运算之前被计算。也就是说,如果用括号来标记优先级,我们会得到 e1 = e1 @ (e2)

其次,如果在表达式 e1 中存在对变量的附带修改,这些修改只会进行一次。下面的例子展示了这一点。

cpp
int a[] = {1, 2, 3, 4, 5};
int b[] = {1, 2, 3, 4, 5};
int i = 0, j = 0;
a[++i] *= i + 1;           // a = {1, 4, 3, 4, 5}, i = 1
                           // 不等价!
b[++j] = b[++j] * (j + 1); // b = {1, 2, 4, 4, 5}, j = 2

在这里,数组 ab 包含相同的元素,并使用索引变量 ij 进行处理。同时,数组 a 的表达式使用了 *= 运算,而数组 b 的表达式使用了与之等价的普通运算形式。结果并不相等:索引变量和数组都不同。

其他运算符在位级操作的问题中会很有用。例如,下面的表达式可以用于将特定的位设置为 1:

cpp
ushort x = 0;
x |= 1 << 10;

在这里,将 10000 0000 0000 0001)向左移动 10 位,得到一个第 10 位为 1 的数(0000 0100 0000 0000)。按位或运算将这一位复制到变量 x 中。

要将相同的位重置为 0,我们可以这样写:

cpp
x &= ~(1 << 10);

在这里,对向左移动 10 位的 1(我们在前面的表达式中看到过)应用取反运算,这会使所有位的值发生改变:1111 1011 1111 1111。按位与运算会将变量 x 中已清零的位(在这种情况下是一位)重置为 0,而 x 中的其他位保持不变。

条件三元运算符

条件三元运算符允许在单个表达式中,基于某个条件来描述两种计算选项。该运算符的语法如下:

condition? expression_true : expression_false

在第一个操作数 condition 中必须指定逻辑条件。这可以是比较运算和逻辑运算的任意组合。两个分支都必须存在。

如果条件为真,将计算表达式 expression_true;如果条件为假,将计算表达式 expression_false

该运算符保证表达式 expression_trueexpression_false 中只有一个会被执行。

两个表达式的类型必须相同,否则,将会尝试对它们进行隐式类型转换。

请注意,在 MQL5 中处理表达式的结果始终表示一个右值(在 C++ 中,如果表达式中只有左值,那么该运算符的结果也将是左值)。因此,下面的代码在 C++ 中可以正常编译,但在 MQL5 中会报错:

cpp
int x1, y1; ++(x1 > y1? x1 : y1); // '++' - 需要左值

条件运算符可以嵌套,也就是说,允许将另一个条件运算符用作条件或任何一个分支(expression_trueexpression_false)。同时,如果不使用括号来明确表示分组,条件所关联的对象可能并不总是清晰的。让我们看一下来自 ExprConditional.mq5 的示例。

cpp
int x = 1, y = 2, z = 3, p = 4, q = 5, f = 6, h = 7;
int r0 = x > y? z : p != 0 && q != 0? f / (p + q) : h; // 0 = f / (p + q)

在这种情况下,第一个逻辑条件是比较 x > y。如果为真,将执行包含变量 z 的分支。如果为假,将检查附加的逻辑条件 p != 0 && q != 0,该条件也有两个表达式选项。

下面是更多的运算符示例,其中逻辑条件用大写字母表示,而计算选项用小写字母表示。为了简单起见,它们都设为变量(来自上面的示例)。实际上,这三个组成部分中的每一个都可以是更复杂的表达式。

对于每一行代码,你都可以追踪结果是如何得到的,注释中已进行了说明。

cpp
bool A = false, B = false, C = true;
int r1 = A? x : C? p : q;                              // 4
int r2 = A? B? x : y : z;                              // 3
int r3 = A? B? C? p : q : y : z;                      // 3
int r4 = A? B? x : y : C? p : q;                      // 4
int r5 = A? f : h? B? x : y : C? p : q;              // 2

由于该运算符是右结合的,复合表达式从右到左进行分析,也就是说,最右边由 ?: 组合的三个操作数的结构会成为左边外部条件的操作数。然后,考虑这个替换,再次从右到左分析表达式,依此类推,直到得到最终完整的上层 ? : 结构。

因此,上面的表达式分组如下(括号表示编译器的隐式解释;实际上,为了使源代码更直观,可以在表达式中添加这样的括号,这种方法是值得推荐的)。

cpp
int r0 = x > y? z : ((p != 0 && q != 0) ? f / (p + q) : h);
int r1 = A? x : (C? p : q); 
int r2 = A? (B? x : y) : z; 
int r3 = A? (B? (C? p : q) : y) : z; 
int r4 = A? (B? x : y) : (C? p : q); 
int r5 = (A? f : h) ? (B? x : y) : (C? p : q);

对于变量 r5,第一个条件 A? f : h 为后续表达式计算逻辑条件,因此会转换为 bool 类型。由于 A 等于 false,则从变量 h 中取值。h 不等于 0;因此,第一个条件被认为是真的。这导致激活分支 (B? x : y),由于 B 等于 false,所以从该分支返回变量 y 的值。

该运算符中必须包含所有三个组件(一个条件和两个可选表达式)。否则,编译器将生成错误“意外的标记”:

cpp
// ';' - 意外的标记
// ';' - 预期 ':' 冒号
int r6 = A? B? x : y; // 缺少可选表达式

在编译器的术语中,标记是源代码中不可分割的片段,具有其独立的含义或用途,例如类型、标识符、标点符号等。整个源代码会被编译器分割成一系列的标记。所讨论的运算符符号也是标记。在上面的代码中有两个符号 ?,必须有两个与之匹配的符号 :,但这里只有一个。因此,编译器会提示语句结束符号 ; 过早出现,并询问到底缺少什么:“预期冒号”。

由于条件运算符的优先级非常低(在完整的优先级表中为 13,见“运算符优先级”),建议将其用括号括起来。这样更容易避免条件运算符的操作数被相邻的具有更高优先级的操作“捕获”的情况。例如,如果我们需要通过两个三元运算符的和来计算某个变量 w 的值,一种直观的方法可能如下:

cpp
int w = A? f : h + B? x : y;                           // 1

这将与我们预期的工作方式不同。由于优先级较高,h + B 的和被视为一个单一的表达式。考虑到从右到左的解析方式,这个和会被当作一个条件,并转换为 bool 类型,编译器甚至会发出警告“表达式不是布尔类型”。编译器的解释甚至可以用括号来直观表示:

cpp
int w = A? f : ((h + B) ? x : y);                       // 1

为了解决这个问题,我们应该按照自己的方式添加括号。

cpp
int v = (A? f : h) + (B? x : y);                       // 9

条件运算符的深度嵌套会对代码的可读性产生不利影响。应避免超过两到三个嵌套层级。

逗号运算符

明确表示为 , 的逗号运算符放置在两个从左到右独立计算的表达式之间。换句话说,这个运算符本身不执行任何操作,只是允许在一条语句中指定两个或多个表达式的顺序。

在序列中位于右侧的表达式可以使用左侧表达式的计算结果,因为左侧表达式已经被处理了。

该运算符的结果是最右侧表达式的结果。这个运算符具有最低的优先级。

目前,在 MQL5 中,逗号运算符的使用仅限于 for 语句的头部。

示例:

cpp
for(i=0,j=99; i<100; i++,j--) 
   Print(array[i][j]);

让我们回顾一下 MQL5 中逗号运算符的关键要点:

求值顺序:

表达式从左到右进行处理。因此,右侧的表达式可以使用左侧表达式的结果,因为左侧表达式已经被处理过了。

结果和优先级:

逗号运算符的结果是最右侧表达式的值。需要注意的是,逗号运算符的优先级最低,这意味着表达式中的其他运算符可能具有更高的优先级。

特殊运算符

sizeof 和 typename

sizeof 运算符

sizeof 运算符返回其操作数的大小(以字节为单位)。该运算符的语法为:sizeof(x),其中 x 可以是一个类型或一个表达式。在这种情况下,表达式不会被计算,因为 sizeof 运算符是在编译阶段执行的,实际上,在表达式中它的位置会被一个常量所替代。

对于固定大小的数组,该运算符返回分配的总内存量,即所有维度的元素数量乘以类型的字节大小。对于动态数组,它返回存储数组属性的内部结构的大小。

下面给出一些带有解释的示例(ExprSpecial.mq5):

cpp
double array[2][2];
double dynamic1[][1];
double dynamic2[][2];
Print(sizeof(double));                           // 8
Print(sizeof(string));                           // 12
Print(sizeof("This string is 29 bytes long!"));  // 12
Print(sizeof(array));                            // 32
Print(sizeof(array) / sizeof(double));           // 4 (元素数量)
Print(sizeof(dynamic1));                         // 52
Print(sizeof(dynamic2));                         // 52

要打印到日志中的结果在注释中进行了标注。

double 类型占用 8 个字节。string 类型的大小是 12 字节。这 12 个字节存储了我们在处理 string 类型的部分中提到的服务信息。对于任何字符串(即使是未初始化的字符串)都会分配这些内存。请注意,包含 29 个字符文本的字符串大小也是 12 字节。这是因为空字符串和包含一些内容的字符串都有一个用于存储内存引用的内部结构。要获取文本长度,我们应该使用 StringLen 函数。

固定大小数组的大小实际上是通过元素数量(2*2 = 4)乘以 double 类型的大小(8 字节)来计算的,总共是 32 字节。因此,像 sizeof(array) / sizeof(double) 这样的表达式可以得出数组中元素的数量。

对于动态数组,内部结构的大小是 52 字节。数组 dynamic1dynamic2 定义上的差异不会影响这个值。

sizeof 运算符对于获取类和结构体的大小特别有用。

typename 运算符

typename 运算符返回一个字符串,其中包含传递给它的参数的名称,该参数可以是一个类型或一个表达式。对于数组,除了数据类型关键字外,还会打印一个标签(以一对括号表示,根据数组的维度可能会有多个括号)。

cpp
Print(typename(double));                         // double
Print(typename(array));                          // double [2][2]
Print(typename(dynamic1));                       // double [][1]
Print(typename(1 + 2));                          // int

对于自定义类型,如类、结构体和其他类型(我们将在第三部分讨论),类型名称会跟随实体类别,例如“class MyCustomType”。此外,对于常量,在字符串描述中会添加“const”修饰符。

因此,要获取由一个单词组成的简短类型名称,请使用附加文件 TypeName.mqh 中的宏 TYPENAME

在所谓的模板中了解类型名称可能是必要的,这些模板可以根据模板参数中定义的不同类型从源代码生成类似的实现。

使用括号进行分组

在前面的章节中,我们已经多次看到,由于运算符的优先级,一些表达式可能会产生意想不到的结果。为了明确地改变计算顺序,我们应该使用括号。包含在括号内的表达式部分,相对于其所在的环境,会具有更高的优先级,而不考虑默认的优先级。括号对可以嵌套,但不建议超过 3 到 4 层嵌套。最好将过于复杂的表达式拆分成几个更简单的表达式。

脚本 ExprParentheses.mq5 展示了在一个表达式中添加括号的演变过程。该表达式的最初意图是使用左移操作 << 来设置变量 flags 中的位。如果变量 offset 不为零,则位号取自 offset,否则为 1(请记住,编号从 0 开始)。然后将得到的值乘以系数 coefficient。在这个示例中无需寻找任何实际应用意义,但更复杂的结构也是可能出现的。

cpp
int offset = 8;
int coefficient = 10, flags = 0;
int result1 = coefficient * flags | 1 << offset > 0 ? offset : 1;     // 8
int result2 = coefficient * flags | 1 << (offset > 0 ? offset : 1);   // 256
int result3 = coefficient * (flags | 1 << (offset > 0 ? offset : 1)); // 2560

第一个版本,没有括号,甚至编译器都会觉得可疑。它给出了我们已经知道的警告:“表达式不是布尔类型”。问题在于,这里的三元条件运算符是所有运算符中优先级最低的。因此,? 之前的整个左边部分都被视为它的条件。在这个条件内部,计算顺序如下:乘法、按位左移、“大于”比较和按位或运算,最终得到一个整数。当然,它可以被用作 truefalse,但最好使用显式类型转换将这种意图“告知”编译器。如果没有显式类型转换,编译器认为这个表达式可疑是有道理的。第一次计算结果为 8,这是不正确的。

让我们在三元运算符周围添加括号。编译器的警告将会消失。然而,表达式的计算仍然是错误的。由于乘法的优先级高于按位或运算,在使用通过左移得到的位掩码之前,变量 coefficientflags 会先相乘。结果是 256。

最后,再添加一对括号,我们将得到正确的结果:2560。

运算符优先级

以下是按照优先级顺序排列的所有运算符的完整表格。

优先级 (P)符号描述示例结合性 (A)
0::作用域解析n1 :: n2L
1()分组(e1)L
1[]索引[e1]L
1.解引用n1.n2L
1++后缀递增e1++L
1--后缀递减e1--L
2!逻辑非!e1R
2~按位取反(求补)~e1R
2+一元正号+e1R
2-一元负号-e1R
2++前缀递增++e1R
2--前缀递减--e1R
2(type)类型转换(n1)R
2&取地址&n1R
3*乘法e1 * e2L
3/除法e1 / e2L
3%取模运算e1 % e2L
4+加法e1 + e2L
4-减法e1 - e2L
5<<左移e1 << e2L
5>>右移e1 >> e2L
6<小于e1 < e2L
6>大于e1 > e2L
6<=小于等于e1 <= e2L
6>=大于等于e1 >= e2L
7==等于e1 == e2L
7!=不等于e1 != e2L
8&按位与e1 & e2L
9^按位异或e1 ^ e2L
10按位或`e1
11&&逻辑与e1 && e2L
12逻辑或
13? :条件三元c1 ? e1 : e2R
14=赋值e1 = e2R
14+=加法赋值e1 += e2R
14-=减法赋值e1 -= e2R
14*=乘法赋值e1 *= e2R
14/=除法赋值e1 /= e2R
14%=取模赋值e1 %= e2R
14<<=左移赋值e1 <<= e2R
14>>=右移赋值e1 >>= e2R
14&=按位与赋值e1 &= e2R
14=按位或赋值`e1
14^=按位异或赋值e1 ^= e2R
15,逗号e1 , e2L

如我们所见,方括号用于指定数组元素的索引,因此具有最高的优先级之一。

除了我们之前讨论过的运算符之外,这里还有一些尚未了解的运算符。

我们将在面向对象编程(OOP)中学习作用域解析运算符 ::。同时,我们也会用到解引用运算符 .。它们的操作数是类型(类)及其属性的标识符,而不是表达式。

取地址运算符 & 用于通过引用传递函数参数以及在面向对象编程中获取对象的地址。在这两种情况下,该运算符都应用于变量(左值)。

显式类型转换操作将在下一章中讨论。