4.1基础
作用于一个运算对象的运算符是一元运算符,两个则是二元运算符,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,运算对象数量没有限制。
c++的表达式要不然是右值,要不然就是左值。在c++语言中,二者的区别就没那么简单了。当一个对象被用作右值的时候,用的是对象的值(内容);当被用作左值的时候,用的是对象的身份(在内存中的位置)。
关于左右值可以参考:https://www.zhihu.com/question/23520802
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。如int i = 0;
cout << i <<" " << ++i <<endl; //未定义的
bool b = true; bool b2 = -b; //b2是true 因为c++中布尔值不应该参与运算。如果直接cout << -b <<endl;会输出-1。上述情况下输出b2还会是1,原因是int型的-1又被转换为bool值,不为0即为true。
4.2算术运算符
参与取余运算的运算对象必须是整数类型。
4.3 逻辑和关系运算符
if (i < j < k) //若k大于1则为真。先判断i<j,返回的bool值作为第二个运算符的左侧运算对象。
if (val)很合理,但是if (val == true)就不太合理,因为true这个bool值会先转换为val的类型,如转换为1,就变为了if(val == 1)了。如果真想知道val的值是不是1,直接写出1这个数值就好了,而不要与true 比较。
4.4赋值运算符
int i = 0, j = 0 , k = 0; const int ci = i;
1024 = k; //错误:字面值是右值
i + j = k; //错误:算术表达式是右值
ci = k ; //错误:ci是常量左值
赋值运算符满足右结合律。 int ival, jval; ival = jval = 0;
复合赋值运算中,唯一的区别是左侧运算对象的求值次数,使用复合运算符只求值一次,使用普通运算符求值两次。这两次包括:一次作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。
4.5递增和递减运算符
递增和递减运算符有 前置版本 和 后置版本。前置版本首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。而后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本。
int i = 0, j;
j = ++i; // j = 1, i = 1
j = i++; // j = 1, i = 2
建议:除非必须,否则不用后置版本。
因为前置版本避免了不必要的工作,把值加1后直接返回改变了的运算对象。而后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,这个操作就是一种浪费。
如果想既将变量加1减1又能使用原来的值,就用后置版本。
如:auto pbeg = v.begin();
while (pbeg != v.end() && pbeg >= 0 )
cout << pbeg++ << endl;
对于的pbeg++,后置递增运算符的优先级是大于解引用运算符的。其实质是,先将pbeg指针加1,然后返回pbeg的初始值的副本作为其求值结果,再解引用。所以,会输出当前值并将pbeg向前移动一个元素。即,后置递增运算符返回初始的未加1的值。
另:pbeg++,这是一种被广泛使用并且有效的写法。
while (beg != s.end() && !isspace(*beg))
beg = toupper(beg++) //错误:该赋值语句未定义
上述情况问题在于:赋值运算符左右两端都用到了beg,并且右侧的运算对象还改变了beg的值,所以它是未定义的。有可能它会这么处理该表达式:
- beg = toupper(beg); //先求左侧值
- (beg+1) = toupper(beg); //先求右侧值
4.6 成员访问运算符
点运算符和箭头运算符;ptr->mem等价于(ptr).mem
解引用符的优先级低于点运算符。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
iter++->empty(); 等价于(iter++).empty()。
4.7 条件运算符
4.8 位运算符
位运算符是作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
~位求反运算符。^位异或运算符。
4.9 sizeof运算符
返回一条表达式或一个类型名字所占的字节数。
sizeof *p:在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。
sizeof无需我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。
4.10 逗号运算符
4.11.1算术转换
算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象转换成最宽的类型。
整型提升,负责把小整数类型(bool、char、unsigned char、unsigned short)转换成较大的整数类型。只要它们所有可能的值都能存在int里,就会提升成int类型,否则提升成unsigned int类型。
较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的类型,前提是转换后的类型要能容纳原类型所有的值。
4.11.2隐式类型转换
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,当数组被用作decltype关键字的参数或者作为取地址符、sizeof及typeid等运算符的运算对象时,上述转换不会发生。
4.11.3显式转换
强制类型转换(cast),尽管有时不得不使用这种强制类型转换,但这种方法本质上非常危险。
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
其中type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static_cast,dynamic_cast,const_cast和reinterpret_cast中的一种。cast-name指定了执行的是哪种转换。
static_cast:需要把一个较大算术类型赋值给较小类型时非常有用。此时强制类型转换告诉程序读者和编译器:我们知道并且不在乎潜在的精度损失。如double slope = static_cast<double>(j) / i;
const_cast:只能改变运算对象的底层const。
对于将常量对象转换为非常量对象的行为,我们一般称其为“去掉const性质”(cast away the const)。
如const char *pc; char p = const_cast<char>(pc);
const_cast常用于有函数重载的上下文中。
reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释。
如:int *ip; char pc = reinterpret_cast<char>(ip);
我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。如: string str(pc); 可能导致异常的运行时行为。 使用reinterpret_cast是非常危险的!
建议:避免强制类型转换。且对于reinterpret_cast尤其适用。在有函数重载的上下文中使用const_cast无可厚非,但在其他情况下使用也就意味着程序存在某种设计缺陷。
早期版本的c++语言中,显式强制转换包含两种形式:
type (expr); //函数形式的强制转换
(type) expr; //c语言风格的强制转换