目录
算术操作符
+运算
-运算
*运算
/运算
%运算
移位操作符
<<左移位
正数移位
负数移位
>>右移位
位操作符
&按位与
|按位或
^按位异或
位运算符练习
赋值操作符
=赋值操作符
+=,-=,*=,/=,%=,>>=,<<=,&=,|=,^=复合赋值操作符
单目操作符
!逻辑反操作
-负值操作
+正值操作
&取地址操作
sizeof计算类型大小操作
~二进制取反操作
--前置
--后置
++前置
++后置
*解引用操作
(类型)强制类型转换操作
sizeof和数组
关系操作符
>,>=,<,<=,!=,==
逻辑操作符
&&逻辑与操作(并且)
||逻辑或操作(或者)
练习?
条件操作符
?:条件操作
逗号表达式
下标引用操作符
[]下标引用操作
函数调用操作符
()函数调用操作
结构成员操作符
.结构体.成员名操作
->结构体指针->成员名操作
表达式求值
隐式类型转换
算术转换
操作符的属性
算术操作符
+运算
-运算
*运算
/运算
整数/运算
当a/b,且a,b都是整型时,得到的结果是整型。
int main()
{
int a = 7, b = 2;
int c = a / b;
printf("%d\n", c);
a = 7;
system("pause");
return 0;
}
浮点数/运算
当a/b,且a,b中任一浮点型时,得到的结果是浮点型。
int main()
{
int a = 7;
float b = 2.0;
float c = a / b;
printf("%f\n", c);
a = 7;
system("pause");
return 0;
}
%运算
a%b,a与b必须是整数,其余算术运算符没有这个要求。
a%b的结果是余数。
int main()
{
int a = 7, b = 2;
int c = a%b;
printf("%d", c);
system("pause");
return 0;
}
移位操作符
位移操作符移动的是二进制位,操作符的两边必须是整数。
在计算机中由0和1组成的数字就是2进制,原理与10进制相同
十进制 | 123 | 100 | 20 | 3 | | | 1*10的平方 | 2*10的1次方 | 3*10的0次方 | 二进制 | 7(111) | 4 | 2 | 1 | | | 1*2的平方 | 1*2的1次方 | 1*2的0次方 |
二进制位在内存中的存储是以补码的形式,其最高位表示符号位(0位正,1为负),那么什么是补码?
正数与0的补码:
以10为例:
原码:0000 0000 0000 0000 0000 0000 0000 1010
反码:0000 0000 0000 0000 0000 0000 0000 1010
补码:0000 0000 0000 0000 0000 0000 0000 1010
整数与0的原码、反码、补码是相同的。
负数
以-10为例:
原码:1000 0000 0000 0000 0000 0000 0000 1010
反码:1111 1111?1111 1111?1111 1111?1111 0101? ? ? ?原码除符号位外取反。
补码:1111 1111?1111 1111?1111 1111?1111 0110? ? ? ?反码+1
<<左移位
位移规则:数字最左边的位抛弃,右边补0。
正数移位
以10为例,向左移动1位。
10的补码:0000 0000 0000 0000 0000 0000 0000 1010
向左移动:0000 0000 0000 0000 0000 0000 0001 0100
移动后结果为20。
int main()
{
int a = 10;
printf("%d", a << 1);
system("pause");
return 0;
}
以20为例,向左移动1位。
20的补码:0000 0000 0000 0000 0000 0000 0001 0100
向左移动:0000 0000 0000 0000 0000 0000 0010 1000
移动后结果为40。
int main()
{
int a = 20;
printf("%d", a << 1);
system("pause");
return 0;
}
以15为例,向左移动1位。
15的补码:0000 0000 0000 0000 0000 0000 0000 1111
向左移动:0000 0000 0000 0000 0000 0000 0001 1110
移动后结果位30。
int main()
{
int a = 15;
printf("%d", a << 1);
system("pause");
return 0;
}
负数移位
以-10为例
-10的补码:1111 1111?1111 1111?1111 1111?1111 0110?
? 左移一位:1111 1111?1111 1111?1111 1111?1110 1100?
移动后结果为-20。
以-20为例
-20的补码:1111 1111?1111 1111?1111 1111?1110 1100??
? 左移一位:1111 1111?1111 1111?1111 1111?1101 1000?
移动后结果为-40。
>>右移位
右移操作符分为逻辑右移和算术右移,两种操作符的使用取决于编译器。
逻辑右移的规则:位的左边填充0,右边丢弃。
算术右移的规则:位的左边使用符号位填充,右边丢弃。
以-10为例
-10的补码:1111 1111?1111 1111?1111 1111?1111 0110?
? 右移一位:1111 1111?1111 1111?1111 1111?1111 1011?
移动后结果为-5。
以10为例,向右移动1位。
10的补码:0000 0000 0000 0000 0000 0000 0000 1010
向右移动:0000 0000 0000 0000 0000 0000 0000 0101
移动后结果为5。
左移和右移在不超过位的范围的情况下,相当于乘以2和除以2。
注意:在使用位运算的时候,不能使用负数作为位数,这个是标准未定义的。
位操作符
&按位与
规则:当两个数按位与,相同的位数只要有一个为0,该位的结果就为0。
以3&5为例
3的补码:0000 0000 0000 0000 0000 0000 0000 0011
5的补码:0000 0000 0000 0000 0000 0000 0000 0101
3&5补码: 0000 0000 0000 0000 0000 0000 0000 0001
结果为1。
int main()
{
printf("%d", 3 & 5);
system("pause");
return 0;
}
|按位或
规则:当两个数按位或,相同的位数只要有一个为1,该位的结果就为1。
以3|5为例
3的补码:0000 0000 0000 0000 0000 0000 0000 0011
5的补码:0000 0000 0000 0000 0000 0000 0000 0101
3|5补码: 0000 0000 0000 0000 0000 0000 0000 0111
结果为7。
int main()
{
printf("%d", 3 | 5);
system("pause");
return 0;
}
^按位异或
规则:当两个数按位异或,相同的位数数字不同结果为1,数字相同结果为0。
以3^5为例
3的补码:0000 0000 0000 0000 0000 0000 0000 0011
5的补码:0000 0000 0000 0000 0000 0000 0000 0101
3^5补码: 0000 0000 0000 0000 0000 0000 0000 0110
结果为6。
int main()
{
printf("%d", 3 ^ 5);
system("pause");
return 0;
}
位运算符练习
1、不能创建临时变量,实现两个数的交换。
先看一组推论。
3的补码: 0000 0000 0000 0000 0000 0000 0000 0011
3的补码: 0000 0000 0000 0000 0000 0000 0000 0011
3^3补码: 0000 0000 0000 0000 0000 0000 0000 0000
3^3补码: 0000 0000 0000 0000 0000 0000 0000 0000
5的补码: 0000 0000 0000 0000 0000 0000 0000 0101
异或补码:0000 0000 0000 0000 0000 0000 0000 0101
结果为5,由此可知,3异或3的结果为0,0异或5的结果为5
推广,同一个数异或结果为0,0与任何数异或结果为任何数
使用交换律
3的补码: 0000 0000 0000 0000 0000 0000 0000 0011
5的补码: 0000 0000 0000 0000 0000 0000 0000 0101
3^5补码: 0000 0000 0000 0000 0000 0000 0000 0110
3^5补码: 0000 0000 0000 0000 0000 0000 0000 0110
3的补码: 0000 0000 0000 0000 0000 0000 0000 0011
异或补码:0000 0000 0000 0000 0000 0000 0000 0101
结果为5,由此可知异或支持交换律。
既然^运算符支持交换律,那么可以推导出下面的公式。
a=3,b=5
a=a^b;此时a的结果为a^b
b=a^b;b的结果为a^b^b=a,所以b已经保存原本a的值
a=a^b;a的结果为a^b^a=b,此时b保存的是原来a的值,所以a的结果是原来b的值
int main()
{
int a = 3;
int b = 5;
printf("交换前a=%d,b=%d\n", a, b);
a = a^b;
b = a^b;
a = a^b;
printf("交换后a=%d,b=%d\n", a, b);
system("pause");
return 0;
}
不适用第三个变量也可以使用加法运算进行求解。
int main()
{
int a = 3, b = 5;
printf("交换前a=%d,b=%d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("交换后a=%d,b=%d\n", a, b);
system("pause");
return 0;
}
?但这样的方法有个问题,就是在数字很大的时候容易溢出,并且效率也不够高,所以在交换的时候最好用的还是使用第三个变量进行交换。
2、求一个整数存储在内存中的二进制中1的个数。
思路1,余2法,num余2的结果为1,说明num在二进制的最右边的值为1,之后除以2在某种程度上可以看成是右移一位.因此只要不断的除以2,直到0为止,就可以找到num中所有的1.
int main()
{
int num=7;
int count=0;
while(num)
{
if(num%2==1)
{
count++;
}
num/=2;
}
return 0;
}
但这这个方法有个问题,就是当num为负数时,num%2的结果为-1,所以不能使用。
思路2移位法,就是1从前进0步开始直到前进到32-1步为止,每前进一次都要与一次,如果结果为1则count++,如果为0则count不++
int main()
{
int num = 7;
int i = 0;
int count = 0;
for (i = 0; i <32; i++)
{
if (num&(1 << i))
{
count++;
}
}
printf("%d", count);
system("pause");
return 0;
}
这种方法每个数必须循环32次浪费时间。
思路3拆数法,任意不为0的数,一定表示有1的存在,所以可以用这个数与比比它小以的数,比它小一的数表示拆掉了这个数的最低为
以7为例
2个1
5的补码 0000 0000 0000 0000 0000 0000 0000 0101
4的补码 0000 0000 0000 0000 0000 0000 0000 0100
5&4补码 0000 0000 0000 0000 0000 0000 0000 0100
剩余1个1
1个1
4的补码 0000 0000 0000 0000 0000 0000 0000 0100
3的补码 0000 0000 0000 0000 0000 0000 0000 0011
4&3补码 0000 0000 0000 0000 0000 0000 0000 0000
剩余0
int main()
{
int num = 100;
int count = 0;
while (num)
{
count++;
num = num&(num - 1);
}
printf("%d", count);
system("pause");
return 0;
}
赋值操作符
=赋值操作符
+=,-=,*=,/=,%=,>>=,<<=,&=,|=,^=复合赋值操作符
单目操作符
!逻辑反操作
规则将真变成假,假变成真(C语言中0为假,非0为真)
当num为真时进入if
int main()
{
int num = -100;
int count = 1;
if (num)
{
printf("%d", count);
}
system("pause");
return 0;
}
当num为假时进入if
int main()
{
int num = 0;
int count = 1;
if (!num)
{
printf("%d", count);
}
system("pause");
return 0;
}
-负值操作
规则改变一个数的符号(正负转换)
int main()
{
int num = 10;
num = -num;
printf("%d\n", num);
num = -num;
printf("%d\n", num);
system("pause");
return 0;
}
+正值操作
没有任何意义的操作。
&取地址操作
a变量是整型,在内存中占有4个字节,&取地址运算符取出的是第一个字节的地址,上面的图片代码化是int *p=&a;含义是将a的首字节的地址取出,并放入类型为(int *)的指针变量p中;
sizeof计算类型大小操作
规则计算一个数据类型所占内存空间的大小,单位是字节,表达式可以是整型,类型等等不能是函数哦。
int main()
{
int num = 10;
printf("%d\n", sizeof(int [10]));
printf("%d\n", sizeof(int*));
system("pause");
return 0;
}
~二进制取反操作
规则:二进制按位取反是指将一个数的每一位取反,0变1,1变0,注意该操作符只能作用于整型。
int main()
{
int a = 1;
printf("%d", ~a);
system("pause");
return 0;
}
1的补码: 0000 0000 0000 0000 0000 0000 0000 0001
取反补码:1111 1111 1111 1111 1111 1111 1111 1110
按照有符号类型读取
补码取反:1000 0000 0000 0000 0000 0000 0000 0001
反码加1: 1000 0000 0000 0000 0000 0000 0000 0010
结果为-2
无符号整型
取反补码:1111 1111 1111 1111 1111 1111 1111 1110
结果为4294967294
--前置
规则:先自减,后使用
--后置
规则:先使用,后自减
++前置
规则:先自增,后使用
++后置
规则:先使用,后自增
int main()
{
int a = 3;
int b = ++a;
printf("%d\n", a);
printf("%d\n", b);
system("pause");
return 0;
}
//代码结果为4 4说明a自增后才给b赋值。
int main()
{
int a = 3;
int b = a++;
printf("%d\n", a);
printf("%d\n", b);
system("pause");
return 0;
}
//代码结果为4 3说明a先赋值给b才自增。
*解引用操作
规则:解引用操作于指针变量一起使用,意义是通过指针变量中所保存的地址,找到对应的地址中存储的值。
?所以*p等于a。
int main()
{
int a = 10;
int *pa = &a;
*pa = 20;
printf("%d\n", a);
system("pause");
return 0;
}
(类型)强制类型转换操作
规则:将类型强制转换成()内的类型。
int main()
{
float a = 10.6;
printf("%d\n", (int)a);
system("pause");
return 0;
}
sizeof和数组
sizeof操作符计算类型的大小,但是计算要注意区分计算的类型具体是什么
void test(int arr[], char ch[])
{
printf("2=%d\n", sizeof(arr));//2
printf("4=%d\n", sizeof(ch));//4
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("1=%d\n", sizeof(arr)); //1
printf("3=%d\n", sizeof(ch));//3
test(arr, ch);
system("pause");
return 0;
}
1的结果为40 计算的是数组的大小
2的结果为4或者8 计算的是指针的大小
3的结果为10 计算的是数组的大小
4的结果为4或者8 计算的是指针的大小
关系操作符
>,>=,<,<=,!=,==
规则:判断两个值大小或者等于,不等于关系的操作符。
注意:==容易于=混用。
逻辑操作符
&&逻辑与操作(并且)
规则:判断表达式的是否为真(C语言中0为假,非0为真),&&判断的是当两个值都为真时表达式的结果为真,若其中一个为假则表达式的值为假。
注意:&&的特性会控制表达式的求值顺序,也就是说a&&b,当a的值为0时b就不再计算。
int main()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int i = a++&&++b&&d++;(1)
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//(1)2 3 3 5
a = 0;
b = 2;
c = 3;
d = 4;
i = a++&&++b&&d++;(2)
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//(2)1 2 3 4
a = 0;
b = 2;
c = 3;
d = 4;
i = ++a&&++b&&d++;(3)
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//(3)1 3 3 5
system("pause");
return 0;
}
?(1)表达式,因为每一个数非0为真,没有促发短路运算,所以表达式中每个变量的值都被修改。
(2)表达式,虽然a++的结果为1,但因为后置++,先使用再自增,所以表达式中得到的值为0,促发短路运算,后面没有再计算。
(3)表达式,虽然a还是0,但前置++,a先进行了自增然后使用,所以表达式中的得到1,因此进行了后面的计算。
||逻辑或操作(或者)
规则:判断表达式的值的真假,a||b中,只要a或者b任意为真,整个表达式的值都为真。
注意:||会控制表达式的求值顺序,只要a为真,表达式的结果就为真,后面的不再计算。
int main()
{
int a = 1;
int b = 0;
int c = 3;
int d = 4;
int i = a++||b++||d++;
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//2 0 3 4
a = 0;
b = 2;
c = 3;
d = 4;
i = a++||++b||d++;
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//1 3 3 4
a = 0;
b = 2;
c = 3;
d = 4;
i = ++a||++b||d++;
printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//1 2 3 4
system("pause");
return 0;
}
练习?
求1000到2000之间的闰年。
闰年的判断标准是能被4整除并且不能被100整除,或者能被400整除的数是闰年。
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year += 4)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
printf("%d ", year);
}
}
system("pause");
return 0;
}
条件操作符
?:条件操作
规则:? 表达式1?表达式2:表达式3,如果表达式1的值为真,那么计算表达式2的值并作为整个表达式的值,如果1为假,那么计算表达式3的值并作为整个表达式的值。
注意:控制表达式的运算顺序,表达式2和表达式3中只有一个被运行。
逗号表达式
规则:逗号表达式就是用逗号隔开的表达式(a,b,c),整个表达式从左向右依次执行,整个表达式的结果就是c的结果。
注意:逗号表达式控制求值顺序,如果前面的表达式没有赋值,那么将毫无意义。
逗号运算符简化代码
a = test();
fun(a);
while (a > 0)
{
a = test();
fun(a);
}
while (a = test(), fun(a), a > 0)
{
}
下标引用操作符
[]下标引用操作
规则:对数组进行操作,[]使用两个操作数,一个是数组名,一个是索引值
以arr[5]为例
arr[5]=*(arr+5);
因为加法交换律
*(arr+5)=*(5+arr);
*(5+arr)=5[arr];
int main()
{
int arr[10] = {0};
5[arr] = 7;
printf("%d", arr[5]);
system("pause");
return 0;
}
函数调用操作符
()函数调用操作
规则:函数调用操作符用来调用函数,最少可以有一个操作符。
结构成员操作符
.结构体.成员名操作
->结构体指针->成员名操作
上面两个操作符都是对结构体进行操作。
struct stu
{
int num;
int num2;
};
void init(struct stu* ps)
{
ps->num = 10;
ps->num2 = 20;
}
print(struct stu* ps)
{
printf("%d\n", (*ps).num);
printf("%d\n", (*ps).num2);
}
int main()
{
struct stu s = { 0 };
init(&s);
print(&s);
system("pause");
return 0;
}
表达式求值
表达式求值的操作顺序一部分是由优先级和结合性确定,同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的,为了获得这个精度,表达式中的短整型和字符类型操作数在使用前被转换为普通类型,这个转换称为整型提升。
整型提升的意义:CPU内整型运算器(ALU)的操作数字节长度一般就是int的字节长度。
举例:
char a,b,c;
c=a+b;
首先a与b进行整型提升,提升到int类型,然后进行加法运算,运算完以后结果进行截断,存入c中。
整型提升的规则:按照变量的数据类型的符号位来进行提升。
char c=-1;
一、将-1放入c中
-1的补码:1111 1111 1111 1111 1111 1111 1111 1111
截断放入c中:1111 1111
二、使用时将-1从c中取出
-1的补码:1111 1111
整型提升:1111 1111 1111 1111 1111 1111 1111 1111
char c=1;
一、将1放入c中
1的补码:0000 0000 0000 0000 0000 0000 0000 0001
截断放入c中:0000 0001
二、使用时将1从c中取出
1的补码:0000 0001
整型提升:0000 0000 0000 0000 0000 0000 0000 0001
无符号与正数的整型提升相同,前面补0
整型提升的计算
int main()
{
char a = 5;
char b = 126;
char c = 0;
c = a + b;
printf("%d\n", c);
system("pause");
return 0;
}
char a = 5;
5的补码: 0000 0000 0000 0000 0000 0000 0000 0101
存入a中: 0000 0101
char b = 126;
126的补码:0000 0000 0000 0000 0000 0000 0111 1110
存入b中: 0111 1110
char c = 0;
0的补码: 0000 0000 0000 0000 0000 0000 0000 0101
存入c中: 0000 0000
c=a+b;
a提升: 0000 0000 0000 0000 0000 0000 0000 0101
b提升: 0000 0000 0000 0000 0000 0000 0111 1110
a+b: 0000 0000 0000 0000 0000 0000 1000 0011
截断存入: 1000 0011
printf("%d\n", c);按照有符号整型打印
整型提升得到c的补码: 1111 1111 1111 1111 1111 1111 1000 0011
补码取反: 1000 0000 0000 0000 0000 0000 0111 1100
反码加一: 1000 0000 0000 0000 0000 0000 0111 1101 结果-125
举例2
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
system("pause");
return 0;
}
代码运行结果是c;
运行过程;
0xb6的二进制:1011 0110(182)
a == 0xb6
整型提升:1111 1111 1111 1111 1111 1111 1011 0110
补码减一:1111 1111 1111 1111 1111 1111 1011 0101
取反 :1000 0000 0000 0000 0000 0000 0100 1010(-74)
0xb6常量的值为182,表达式中a的值为-74所以不相等。
0xb600的二进制:1011 0110 0000 0000(46592)
b == 0xb600
整型提升:1111 1111 1111 1111 1011 0110 0000 0000
补码减一:1111 1111 1111 1111 1011 0101 1111 1111
取反 :1000 0000 0000 0000 0100 1010 0000 0000(-18944)
0xb600常量的值为46592,表达式中b的值为-18944所以不相等。
0xb6000000的二进制:1011 0110 0000 0000 0000 0000 0000 0000
c == 0xb6000000
不使用整型提升:1011 0110 0000 0000 0000 0000 0000 0000
补码减一: 1011 0101 1111 1111 1111 1111 1111 1111
取反: 1100 1010 0000 0000 0000 0000 0000 0000(-1241513984)
如果想让三个表达式成立,整型提升时要注意类型,不能是有符号的类型。
int main()
{
unsigned char a = 0xb6;
unsigned short b = 0xb600;
int c = 0xb6000000;
char d = 0;
if (a == 0xb6)
printf("a\n");
if (b == 0xb600)
printf("b\n");
if (c == 0xb6000000)
printf("c\n");
system("pause");
return 0;
}
举例3
int main()
{
char c = -1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
system("pause");
return 0;
}
代码运行的结果为1,4,4,说明+c与-c在计算时进行了整型提升,此时的类型为int,所以结果为4
算术转换
如果某个操作数的各个操作数属于不同类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigfned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
注意:算术转换要合理,否则会存在一些潜在的问题
float f=3.14;
int num=f;
num的值为3.
操作符的属性
复杂的表达式求值有三个影响的因素。
1、操作符的优先级。
2、操作符的结合性。
3、是否控制求值顺序。
优先级:两个相邻操作符之间谁先计算。
结合性:当两个相邻的操作符优先级相同时,计算就看结合性是从左向右,还是从右向左。
影响求值顺序;部分操作符求值时,会影响表达式计算的顺序。
运算符表:
一些问题的表达式
表达式1
a*b+c*d+e*f
求值顺序
*与+相邻时,*的优先级高
一、a * b + c * d + e * f
1 4 2 5 3
二、a * b + c * d + e * f
1 3 2 5 4
优先级只能决定相邻的*和+时*的运算先于+,但不能决定最后一个*与第一个+的先后顺序。
表达式2
c + --c
--的优先级比+高,所以--先计算,但是并不能确定第一个c的值多久进行求值,可能在--c之前,也可能是在--c之后。
表达式3
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("%d\n", i);
system("pause");
return 0;
}
该代码在不同的编译器中结果不同。
代码4
int main()
{
int i = 1;
i = ++i + ++i + ++i;
printf("%d\n", i);
system("pause");
return 0;
}
VS下,结果为12;
运行顺序
i = ++i + ++i + ++i;
1 4 2 5 3
linux下,结果为10
i = ++i + ++i + ++i;
1 3 2 5 4
代码5
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun()*fun();
printf("%d\n", answer);
system("pause");
return 0;
}
VS下结果为-10;
运行顺序
fun() - fun() * fun();
1 5 2 4 3
但是在不同的编译器下运行的顺序有很多不同。
注意:如果表达式中不能确定唯一顺序,那个表达式就存在问题。
|