运算符与表达式
数学是科技发展的基础,数学公式的意义十分重要,比如改变世界的欧拉方程:
e
π
i
+
1
=
0
e^{\pi i}+1=0
eπi+1=0
为了用机器创造世界,就需要机器能够表达公式;
运算符
运算符是编译器可识别并执行特定数学或逻辑操作的符号,C++内置丰富的运算符,并提供了以下类型的运算符:
1.算术运算符;
2.关系运算符;
3.逻辑运算符;
4.位运算符;
5.赋值运算符;
6.杂项运算符;
表达式
在程序中,运算符用于操作数据,数据被称为操作数,使用运算符将操作数连接而成的式子称为表达式;
表达式具有以下特点:
1.常量与变量都是表达式,比如,常量3.14,变量i;
2.运算符的类型对应表达式的类型,比如算术运算符对应算术表达式;
3.就像python中的表达式,每个C++表达式都有返回值,即表达式有运算结果;
算术运算符
有两个变量A和B:
int A=10;
int B=20;
有以下操作:
需要两个操作数才能完成:
+ 两个数相加
/ 分子除以分母,两个整型数得到整型,若含有浮点型,则结果为浮点型
% 取余数,两个数必须为整型
只要一个操作数就能完成(单目运算):
++ 自增运算,整数值增加1,++可以在操作数的前或者后
-- 自减运算,整数值减少1,--可以在操作数的前或者后
但++和--位于操作数前后会有微小的差异,差异放到后期的面向对象阐述
//前置
std::cout << ++A << endl;
输出11
//后置
std::cout << A++ << endl;
输出10
关系运算符
关系运算符如下:
== 检查两个操作数的值是否相等,相等则为true,返回1,否则false返回0
!= 检查两个操作数的值是否不等,不等则为true
> 检查左操作数是否大于右操作数
< 检查左操作数是否小于右操作数
同理还有
>= 和 <=
C++与python相比,关系运算的写法也更加严格,关系运算的表达式需要用括号包围,示例:
std::cout << (A == B) << endl;
运算结果为false,false是一个表达式,值为0,所以输出0
逻辑运算符
假设变量A的值为1,变量B的值为0,(用布尔型存储可以节省空间),存在以下逻辑运算:
&& 逻辑与,如果两个操作数都非0,则条件为真,返回1
|| 逻辑或,如果有一个操作数非0,则条件为真,返回1
//单目运算,可以不用括号包围
! 逻辑非,条件为真的逻辑表达式会转为假
在圣经中,有一句话:
To be or not to be,that’s a question
现在可以通过机器获得答案:
(A==true||A!=true)
德摩根律
A
∪
B
‾
=
A
‾
∩
B
‾
,
A
∩
B
‾
=
A
‾
∪
B
‾
\overline{A\cup B}=\overline{A}\cap \overline{B},\overline{A\cap B}=\overline{A}\cup \overline{B}
A∪B=A∩B,A∩B=A∪B
通过C++可以描述为:
(!(A||B)==(!A&&!B)) //1
(!(A&&B)==(!A||!B)) //1
使用断言assert
在开发测试环节,常常会用到断言assert判断样例是否能被正确处理:
#include <assert.h>
int main()
{
bool A = true;//值为1
bool B = false;//值为0
//使用断言验证 德摩根律
//断言1
assert( (!(A || B) == (!A && !B)) );
//断言2
assert( ((A || B) == (!A && !B)) );
return 0;
}
断言1是德摩根律,断言2不是德摩根律,在执行到断言1时,不会出错,执行到断言2会报错:
只有当断言assert()内的表达式为true(值为1)才会顺利执行,否则(表达式返回false,值为0)就会报错
位运算符
位运算符作用于位,并逐位执行操作,真值表如下:
p q p&q p|q p^q (异或)
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0
位运算是用于操作数的bit逐位运算,逻辑运算的操作对象是布尔类型;
位运算符还有:
~ 取反
<< 左移
>> 右移
位运算符&,|,^属于双目运算符,其结合性都是从左到右,优先级高于逻辑运算符,低于关系运算符;
同一层的优先级:&>^>|;
优先级最好通过括号确定更不容易出错
实例:
int a=10;
int b=20;
cout<<(a&b)<<endl; //01010&10100=00000=>0
cout<<(a|b)<<endl; //01010|10100=11110=>30
cout<<(a^b)<<endl; //01010^10100=11110=>30
//与补码相关
cout<<(~a)<<endl; //~ 0000 0000 0000 1010=1111 1111 1111 0101=>-11
cout<<(a<<2)<<endl; //00001010<<2=00101000=>40
cout<<(a>>2)<<endl; //00001010>>2=00000010=>2
在取反操作上,反而得到一个负数,这与补码相关,因为C++内部数值以补码表达;除此之外,移位运算也与有无符号数相关,所以还需要补充补码与有无符号数相关的内容;
补码
在电路设计上,机器只能做加法不能做减法,所以需要找到一种方法(补码)让机器的加法实现减法;
机器数
一个数在机器中以二进制表示,机器数是带符号的,在机器中,用一个数的最高位保存符号位,正数为0,负数为1:
以上的两个整型数,默认4个字节,即32位;
真值
真值是真正数学意义上的数值,由于机器数第一位是符号位,所以机器数的形式值不等于真值;
无符号数的补码
无符号数补码就是平时接触的二进制十进制转换:
b
(
1011
)
=
1
×
2
3
+
0
×
2
2
+
1
×
2
1
+
1
×
2
0
=
11
b(1011)=1\times 2^{3}+0\times 2^{2}+1\times 2^{1}+1\times 2^{0}=11
b(1011)=1×23+0×22+1×21+1×20=11
有符号数的补码
对于长度为
w
w
w的有符号数的补码,转为对应数值的计算为:
−
x
w
−
1
2
w
−
1
+
∑
i
=
0
w
−
2
x
i
2
i
-x_{w-1}2^{w-1}+\sum_{i=0}^{w-2}x_{i}2^{i}
−xw−12w−1+i=0∑w−2xi2i
比如:
b
(
1011
)
=
−
1
×
2
3
+
0
×
2
2
+
1
×
2
1
+
1
×
2
0
=
−
5
b(1011)=-1\times 2^{3}+0\times 2^{2}+1\times 2^{1}+1\times 2^{0}=-5
b(1011)=−1×23+0×22+1×21+1×20=−5
可以发现,无符号数的补码等于原码;
有符号数的正数,其补码等于原码;如果是负数,补码等于原码取反再加一;
因为正数的补码等于原码,负数的补码则不同,正数相加还是正数,正数加负数即为减法,得到结果还是补码,而这个补码正好对应差的真值,这就是机器只保存补码的原因;
补码对应的数值范围如下:
U代表无符号,T代表有符号
补充:字节序
32位机器用32bit(32字长)即4byte作为一个字,8byte就称为双字,字是CPU一次性处理的单元,而一个字在内存中如何以byte存放?
假设现在有一个字的内容是整型数0x01234567
,这个字的4个byte将被连续存于存储器的0x100,0x101,0x102和0x103的位置(存储器的单元是字节);
字节序即为多字节对象存储在内存中的字节顺序,有两种不同的存储方案:大端法和小端法;
1.大端法:最高有效字节在最前面的方式称为大端法,用于大多数IBM机器,internet传输
2.小端法:最低有效字节在最前面的方式成为小端法,用于Intel兼容的机器
观察机器数
观察以下机器数:
int i1=0;
int i2=-1;
int i3=-2147483648; //32字长的最小有符号整型数
int i4=2147483647; //32字长的最大有符号整型数
unsigned int u1=0;
unsigned int u2=4294967295; //32字长的最大无符号整型数
unsigned int u3=2147483648;
unsigned int u4=2147483647;
cout << &i3 << endl;
在调试阶段,根据i3地址(0x0077F9A4)找到i3:
i3=-2147483648,补码即0x8000 0000;
上图的visual studio设置为每行显示两字节,可以看出我的机器上字排序为小端法,因为我的机器是英特尔机器,英特尔机器都是小端法;
另外也反映了,机器内保存的数都是补码形式;
补码与位运算
验证真值
定义以下函数:
//二进制转无符号整型
unsigned int btou(unsigned int num)
{
return (unsigned int)(num);
}
//二进制转有符号整型
int btot(int num)
{
return (int)(num);
}
验证真值,体现有无符号数的区别:
cout << btou(0xFFFFFFFF) << endl;
cout << btot(0xFFFFFFFF) << endl;
输出分别为:
另外补充一点,补码的编码与真实值的关系如下:
这是一个分段函数,每一段呈正比关系;
回顾之前的按位取反:
int a=10;
cout<<(~a)<<endl; //~ 0000 0000 0000 1010=1111 1111 1111 0101=> -11
现在进行探索:
int a = 10;
int b = ~a;
cout << &a << endl;
cout << &b << endl;
根据a的地址0x003CFDA0找到a:
0x003CFDA0 0a 00 ..
0x003CFDA2 00 00 ..
由于机器是英特尔架构,所以实际这个int的4字节(1个字)值为:0000 000a,这是补码,但a是正数,补码等于原码,所以a的值确实是10;
b的地址是0x003CFD94,找到b:
0x003CFD94 f5 ff ?.
0x003CFD96 ff ff ..
b的补码为FFFF FFF5,二进制形式为:
1111 1111 1111 0101
容易看出b的补码确实是a的补码按位取反,通过验证真值部分实现的函数int btot(int num)
,得到这个补码对应的有符号值为-11;现在就能清晰解释为什么对10取反会得到-11这样的奇怪结果了;
移位运算补充
左移运算情况单一:
右移运算分两种情况,一个是逻辑右移,一个是算术右移;
逻辑右移:移走的位填充为0
算术右移:移走的位填充与符号位有关,负数填充1
对于有符号数,尽量不要使用右移运算,因为到底是逻辑右移还是算术右移完全取决于编译器的判断;
赋值运算符
赋值运算符有如下几种常用形式:
= 简单的赋值运算,右边操作数的值赋给左边操作数
+= 右边操作数的值加上左边操作数的值赋值给左边操作数
同样的还有:
-=
*=
/=
%= 把两操作数的余数赋给左边操作数
回想python,这类似in-place(当然,python数值对象没有in-place之分,但tensor或ndarray对象存在in-place)
结合位运算,延伸出赋值位运算:
<<= 左移赋值运算 c<<=2等价于c=c<<2
>>= 右移赋值运算
&= 按位与再赋值 c&=2等价于c=c&2
^=
|=
实例:
int c=a+b;
cout<<c<<endl;
c+=a
cout<<c<<endl;
杂项运算符
杂项运算符有以下:
sizeof 返回变量占用空间的字节数,可见sizeof不是函数而是运算符
condition?x:y 条件运算符:C++内唯一一个三目运算符,如果condition为真,返回x,否则返回y
, 逗号运算符,顺序执行运算,整个逗号表达式的值是以逗号分隔的最后一个表达式的值
即:表达式 a,b,c 的值为c的值
.和-> 成员运算符,用于引用类,结构和共用体的成员
Cast 强制转换运算符,比如int(2.2)会返回2
& 指针运算符&可以返回变量的地址
* 指针运算符*指向一个变量,例如*var将指向变量var
实例:
int x = 10, y = 20;
cout << sizeof(x) << endl;
int c = x > y ? 1 : 0;
int e = (x, y, c);
cout << e << endl; //0
float f = float(e);
float* p = &f;
cout << *p << endl; //0
成员运算符举例:
typedef struct {
short Sunday = 0;
short Monday = 1;
short Tuesday = 2;
short Wednesday = 3;
short Thursday = 4;
short Friday = 5;
short Saturday = 6;
}Week;
Week w;
cout << w.Friday << endl; //5
cout << sizeof(w) << endl; //14,因为short占用2byte
关于运算符优先级,需要记住:
1.一般,单目运算符优先级高于双目运算符;
2.最好加括号确保优先级的正确性