程序设计语言中大部分程序都在进行表达式的求值操作, 例如求两个数的和,求一个表达式的逻辑结果,或者通过输入输出表达式语句进行输入和输出。
这里我们对表达式进行讨论。
一、表达式
1、表达式
表达式由一个操作数或者多个操作数同操作符组合而成; 字面值是一个最简单的表达式。 表达式都会产生一个结果,如果表达式中没有操作符,则表达式
的值是操作数本身, 如果表达式中有操作符,则表达式的结果是操作符对操作数进行操作后的结果。
一般而言表达式的返回的是右值,不能给表达式赋值, 但可以获取表达式的值。
Exp:
"volcanol"; 表达式语句,"volcanol"是一个表达式,这个表达式的值就是字符串字面值本身
if(iVar) ; if语句需要一个条件表达式,iVar是一个表达式, 返回值是iVar本身, 这里会将表达式的值进行转换,转换规则后面介绍。
2、算术操作符
C++提供了多种算术操作符,这些操作符可以对所有的算术类型进行操作。 算术操作符有以下一些:
+ 一元操作符正号
- 一元操作符负号
* 二元操作符乘法
/ 二元操作符除法
% 二元操作符,求余数操作
+ 二元操作符加法
- 二元操作符减法
这些操作符具有与算术负号基本一致的意思,因此这里不过多的进行解释,需要注意的 / 和 % 两个操作符。
要点:
1、 % 操作符只能用于整型操作数, 并且求取的结果与具体系统相关。
2、 / 操作符的两个操作数都是整型时则其结果也是整型,就算是商包含小数部分,其结果也是整型; 如果其中一个操作数是浮点型,则其结果是浮点型。
Exp:
int main() { cout<<"sizeof(3.14f) is:"<<sizeof(3.14f)<<endl; cout<<"sizeof(3.14) is:"<<sizeof(3.14)<<endl; cout<<"sizeof(3.14/2) is:"<<sizeof(3.14/2)<<endl; cout<<"sizeof(3.14f/2) is:"<<sizeof(3.14f/2)<<endl; return 0; }
程序的执行结果如下所示:
[root@localhost cpp_src]# g++ test.cpp [root@localhost cpp_src]# ./a.out sizeof(3.14f) is:4 sizeof(3.14) is:8 sizeof(3.14/2) is:8 sizeof(3.14f/2) is:4
这里需要注意一点: 浮点型字面值默认为double类型, 如果操作数有一个double型,则结果是double型; 而如果有一个操作数是float类型,一个是整型,则结果是
float类型, 这里不会和C语言一样进行数据类型的隐式转换,即不会有浮点数就全转换为double类型进行计算。
要点: / 和 % 的操作数的正数和负数问题
1、如果两个操作数都是正数、负数,则 /的结果为正数
2、如果两个操作数是正数则%操作的结果是正数; 如果两个操作数都是负数,则操作结果是负数。
3、如果有一个操作数是负数,那么 / 的结果是负数或者0, 而 % 的结果由系统决定。
3、关系操作符和逻辑操作符
关系操作符和逻辑操作符返回的结果类型是bool型,这一点需要注意。
逻辑操作符: ! 逻辑非 一元操作符
&& 逻辑与 二元操作符
|| 逻辑或 二元操作符
逻辑非返回与操作数相反的逻辑值, 如果操作数为false,则逻辑非返回true, 如果操作数为true,则逻辑非返回false,
逻辑与操作符的结果与两个操作数的逻辑值相关, 当两个操作数都是逻辑true的时候,逻辑与返回true,其他情况返回false
逻辑或操作符的结果与两个操作数的逻辑值有关,当两个操作数都是逻辑false的时候,逻辑与返回false,其他情况返回true。
关系操作符: < 小于操作符, 二元操作符
<= 小于等于操作符, 二元操作符
> 大于操作符,二元操作符
>= 大于等于操作符,二元操作符
!= 不等于操作符,二元操作符
这些操作符都具有和数学计算中一致的意义, 具体这里就进行细说啦。
要点:
1、关系运算符的优先级高于逻辑&&、||的优先级, 而逻辑非的优先级高于关系运算符。
2、逻辑运算符 && 和 || 在计算的时候,先求左操作数的值, 然后再求右操作数的值,并且当计算左操作数以后就可以求出整个表达式的值的情况下,将
不再对右操作数进行求值, 这就是通常说的:短路求值。 这一点要引起注意。
3、不能串接使用关系运算符, 关系运算符在使用的过程中不能和数学表达式一样,连续使用多个关系运算符。例如:
if(3 < iVar < 10) //错误的条件表达式 .........
这里会得到一个永远的true表达式, 首先计算 3<iVar ,无论3与iVar的关系,返回的值将为0、或者1 ,这样再用0、1与10比较,则无论如何都是成立的
就会返回一个永远的 true 值, 因此需注意这个地方。
正确的用法如下:
if(3 < iVar && iVar < 10) .........
4、不能将一个值 与 bool 值 true 和false 进行相等测试,这样会造成错误。例如:
if(var == true) ..... else .....
虽然关系表达式的结果是bool值,但是关系操作符的操作数是非bool量, 当关系操作符的操作数存在bool量时,则会将bool量转换为整型量, true转换为1,而
false转换为0, 这样本来是想判断 var 是true 或者false,结果变成了var与1或者0进行比较,因此这里就不能得到正确的结果。为了改变这种逻辑上的错误,我们可以
如下所示:
if(var) .... else .....
这也算是一种技巧吧, 这种技巧在C语言中经常会用到。
4、位操作符
C语言中提供了可以对位进行操作的操作符,C++继承了这种机制,也提供了很多的位操作符,如下所示:
~ 按位取反操作符 << 按位左移操作符 >> 按位右移操作符 & 按位与操作符 | 按位或操作符 ^ 按位异或操作符
这些操作符的意义很直观,这里不进行介绍啦,值得一说的按位 ^ 操作符有一些特殊的用法,因为按位异或操作具有一些特殊的性质。
要点:
1、要注意的是<< 和 >>操作符,<< 移位的时候是低位补零操作, 而 >> 移位操作分为两种情况,一种补零操作,一种是进行符号位扩展,这需要通过程序进行判
断。C++并没有规定是按何种方式进行处理因此在程序中需要进行判断。
if ((0xFFFF FFFF >>1) & 0x80000000) ) 符号位扩展执行的语句 else 高位补零操作执行的语句
具体的实例代码如下所示:
int main() { if( (0xFFFFFFFF>>1) & 0x80000000) cout<<"fu hao kuo zhan"<<endl; else cout<<"gao wei bu ling"<<endl; return 0; }
程序的执行结果如下所示:
[root@localhost cpp_src]# ./a.out gao wei bu ling
这里就是说,我的系统利用个>>移位操作进行的是高位补零操作, 这一点在有些系统可能与这个结果正好相反。
2、这里还有一点, 左移 或者 右移的位数,必须在数据类型的位宽之类,否则结果未定义。
int main() { cout<<(0x1<<33)<<endl; cout<<(0x1>>33)<<endl; return 0; }
编译的时候会有警告信息如下所示:
[root@localhost cpp_src]# g++ test.cpp test.cpp: In function ‘int main()’: test.cpp:15: 警告:左移次数大于或等于类型宽度 test.cpp:16: 警告:右移次数大于或等于类型宽度
这就是提示操作可能未定义,因此在使用的时候,需要注意这一点。
这里给一个关于异或操作的实例:
int main() { int a= 3; int b= 4; a = a^b; b = a^b; a = a^b; cout << a << endl; return 0; }
程序的执行结果如下:
[root@localhost cpp_src]# g++ test.cpp [root@localhost cpp_src]# ./a.out 4
发现一个非常有趣的地方:
[root@localhost cpp_src]# vim op_>>_bit_big.cpp
Vim: 警告: 输出不是到终端(屏幕)
哈哈,不知道有人遇到过这样情况没有,打错字的时候,就是回出现一些异常的情况。
这里还要说的一个情况就是因为C++标准库提供了一个bitset集进行功能扩展,这个bitset类的作用就是提供一种更加高效的替代C++中位操作符的运算的机制,
因此在一般的时候建议使用bitset类进行相关的操作,这样可以简化位操作。
Exp:
int main() { bitset<32> bitVar; bitVar.set(16); cout<<bitVar<<endl; bitset<32> bitVar1(0x0 | (0x1<<16)); cout<<bitVar1<<endl; return 0; }
程序的执行结果为:
[root@localhost cpp_src]# g++ test.cpp [root@localhost cpp_src]# ./a.out 00000000000000010000000000000000 00000000000000010000000000000000
为了将0的第16位设置为1, 利用位操作符需要一个复杂的表达式,而且还可能出错,而用bitset则调用objX.set()很容易就可以进行设置。 同时由于bitset类
提供了一个objX.to_ulong()的操作,因此很容易在整型和bitset中进行转换,这样可以简化编程工作。
对于移位操作符,还有一点需要注意,因为标准的IO流重载了<<和>>两个操作符,因此在cin 和cout的操作中如果有左移和右移操作符,则需要注意加上圆括
号改变运算符的优先级,否则容易引起一些异常。
5、赋值操作符
赋值操作符需要说明的就是赋值操作符的优先级比逗号操作符的优先级高,比其他的运算符的优先级都低。
需要注意的是:赋值操作符返回的是左值, 而且其左操作数必须是左值,否则不能进行运算。
例如:
int i = 30; //注意这个地方不是赋值操作符,存在细微的差别,需要注意 i = 30; //right 30 = 100; //error
要点: 赋值操作符有一些复合赋值符,这些赋值符具有特别的意义。
+= 、 -= 、 *= 、 /= 、 %= 算术运算符结合赋值操作符得到的复合赋值操作符
<<= 、 >>= 、 &= 、|=、^= 位操作符结合赋值操作符得到的复合操作符,这里需要注意没有提供按位取反的复合赋值操作符。
Exp:
int main() { unsigned long int uLVar = 0; uLVar ~= 0x1; cout << uLVar<<endl; return 0; }
程序编译的结果如下所示:
[root@localhost cpp_src]# g++ test.cpp test.cpp: In function ‘int main()’: test.cpp:17: 错误:expected `;' before ‘~’ token
说 ~ 操作符前面缺少分号,这就是说没有~和= 结合组成的复合赋值操作符,这个地方需要特别的注意。
6、 ++ 和 -- 操作符
++ 和 -- 分为两种情况, 前缀形式 prefix 和 后缀形式 postfix , 这个不需要特别的介绍,++ 前缀返回的是增加后的对象, ++后缀返回的是增加前的对象
而--的情况一样。
需要注意的是后缀的优先级高于前缀的优先级。
7、域访问操作符 . 和 ->
. 操作符可以用于对象的成员的访问, 可以用于对象名,或者指向对象的指针以及指向对象的迭代器中来访问对象的成员。
Exp:
string strVar; strVar.size() ; 通过域访问操作符访问对象strVar的成员函数size().
而如果用迭代器或者指针来访问对象,则在使用域访问操作符之前需要对迭代器或者指针进行解引用,如下所示:
string strVar; string *pStr; pStr = &strVar; (*strVar).size();
这样有时候在程序中,很容易忘记对指针进行解引用,而这种操作又比较频繁,因此C++提供了一种更加简便的方式, 通过 -> 操作符来进行访问,可以不进行解引用
就可以访问对象的成员, 如下所示:
vector<string> iVecStrVar; vector<string>::iterator iter = iVecStrVar.begin(); (*iter).size(); //利用解引用操作符以及. 域访问操作符来访问成员 iter->size(); //利用->域访问操作符访问成员
这样可以简化操作。
8、条件操作符
C++也和C语言一样提供了一个三目运算符,就是条件运算符: 操作数1 ? 操作数2 : 操作数3 ; 条件操作符根据操作数1的真假返回操作数2或者操作数3的值,
如果操作数1为真,返回操作数2的值,如果操作数1为假,返回操作数3的值。
这里不举例子啦,可以参考其他的文章。
9、sizeof操作符
在国内一些教材中,经常将sizeof操作符看成函数, 这归结于:sizeof操作符的特殊性, sizeof操作符后面可以直接跟操作数,也可以将操作数用圆括号括起来,
这实际上是一种错误的认识, 必须纠正这种看法。 正确的是 sizeof 为操作符。
sizeof有三种用法:
sizeof(数据类型)
sizeof(对象)
sizeof 对象或者表达式
Exp:
sizeof(vector<int>); sizeof(int) sizeof(UL1);
sizeof 为编译时计算的,它计算操作数在内存中占用的内存的字节数, 这里需注意: 是字节数, 而不是长度。
具体的例子就不举啦,在前面的随笔中,我们已经多次使用这个操作符。
10、逗号操作符
逗号也是一种操作符,逗号操作符用来分隔表达式;逗号表达式经常用于for循环中。C++规定了逗号表达式的求值顺序为从左到右求值,返回值为最右边的表达式
的值。
逗号表达式经常用于for循环,例如:
for(int i=0, j = 100; i != str.size(); i++, j-- ) .......
要点:
int i = 0, j = 100; // 这里的逗号不是逗号表达式, 因为这里没有返回一个值
11、操作符的优先级和结合性
操作符具有优先级,高优先级的操作符先对操作数进行操作, 结合性描述的是当操作符的优先级相同时先对哪一个操作符进行计算;这里不对操作符的优先级
和结合性进行描述。
利用() 可以规避操作符的优先级, 可以让低优先级的操作符先进性计算。
Exp:
1 + 2 * 3 : 先对* 操作符进行计算,求出 2 * 3 = 6 ,然后对 + 操作符进行计算,为 1 + 6 = 7 ,因此表达式的值为 7
(1+2) * 3 : 因为使用了() 所以先对 + 操作符进行操作计算, 1 + 2 = 3 ,然后对 * 进行计算,为 3 * 3 = 9 ;
可以发现当通过() 规避了操作符的优先级后,一个表达式的值就可能完全改变, 因此需要注意( ) 的使用带来的效果。
12、 new 和 delete 表达式
new 和delete也是操作符,要点是: new 和 delete 的操作对象是系统的堆区域, new用于从堆区域申请空间,而delete则用于将不再使用的堆内存
重新返回到堆区域。
1、动态创建对象
int i(1024); int *pi = new int(1024); //动态创建对象,并初始化对象 string strVar(10, '9'); string *pStr = new string(10, '9'); //动态创建对象并初始化对象,
对于内置类型,动态创建对象并初始化的时候,就和定义变量一样; 对于类类型,则会调用对应的构造函数进行初始化。
2、动态创建对象默认初始化
动态创建对象默认初始化,与局部变量和对象的定义的初始化一样。
int i; int *pI = new int; //不进行初始化, string strVar; string *pStr = new string; //调用默认构造函数进行初始化
3、撤销动态创建的对象
通过delete操作符,可以撤销动态创建的对象,如下所示:
int *pI = new int; //动态创建对象, 并将指针返回赋值给pI ..... delete pI; //撤销动态创建的对象,将内存返还给系统的堆区域
要点:
1、撤销零指针
在C++中撤销 0 指针是安全的。
#define NULL ((void *)0) int *p = NULL; delete p; //撤销0指针, 为正常的操作
2、动态创建 const对象
C++允许动态创建const对象, 这一点需要引起注意。
const int *pI = new const int(100);
不能改变动态创建的const对象的值,这一点与静态定义的cosnt对象一样。
动态创建的cosnt对象同样可以利用delete操作符回收内存,如下所示:
const string *pStr = new const string; //具有默认构造函数的类类型,可以不提供初始化式 ...... delete pStr; //回收pStr指向的动态创建的const对象的内存