C++ Primer 笔记 第四章
运算符分为一元运算符(Unary operator) 和二元运算符 (Binary operator)。“元”代表有几个 operands。每个运算符的运算优先级(Precedence) 和结合律(Associativity)由运算符自身决定。在执行表达式的过程中,有两种情况会出现:
所有的表达式都被区分为左值(Lvalue)和右值(Rvalue)。这种叫法继承自 C,意味着可以在 =
赋值运算符左边的值就是左值,否则就是右值。在 C++ 中,左值和右值的区分相对更加复杂:比如某些情况下的 const 变量就不能被赋值,某些函数的结果对象但返回右值等等。总的来说,当表达式是左值的时候,我们使用该表达式的 idendity (在内存中的位置);当表达式是右值的时候,我们使用该表达式的值(内容)。
通俗的来说,左值是明确有定义的,在内存中固定存在的对象(地址确定),而右值大多数代表临时对象(有内容,无确定地址)
运算符是根据需求左值/右值和返回左值/右值来区分的。一个比较重要的特点是:
当需求右值的时候(也就是希望使用内容而不是位置的时候),可以使用左值代替(此时使用左值中的内容),但是反过来不行(右值只能表示内容)。
之前见过的几个例子:
decltype
应用于表达式时,对左值返回其引用,对右值返回其本身的类型:
int i = 0;
int *p = &i;
decltype(*p); // *p is lvaule, return int&
decltype(&p) // &p is rvalue. p is pointer, &p return the address of the pointer, so decltype return int**
默认情况下复合表达式中的运算根据运算符的优先级和结合律来判断。使用括号可以强制改变优先级。
Ref:C++ Operator Precedence
需要注意的是,优先级和结合律只确定了 operands 与哪些运算符编组;但并没有确定组内 operands 运行的顺序。比如下例:
int i = f1() + f2();
可以看出 f1()
与 f2()
会先相加,再将结果赋值给 i
。但问题在于,我们并不知道 f1()
先执行还是 f2()
先执行。这种情况下,如果两个表达式涉及到修改同一个值,问题就来了:
int i = 0;
cout << i << ++i << endl; //undefined
很显然 «
运算符无法确定有关于 i
的表达式的执行顺序。如果前面的 i
先运算,那打印结果会是 0 1
;但如果后面的 ++i
先执行,打印结果则是 1 1
。也有可能编译器根本就不会按我们想象来进行运算。对于没有指定 operands 运算顺序的表达式,如果 operands 牵涉到并改变了同一个值,结果将是 undefined.
有四种运算符是保证了 operands 的执行顺序:&&
、||
、?:
、,
。&&
会先执行左边的 operand, 当左边的 operand 为真的时候才会执行右边的 operand。
很显然,优先级/结合律 与 表达式的执行顺序是完全两个独立的系统。比如下面的例子:
f() + g() * h() + j();
g()
和 h()
先返回值再相乘g()
与 h()
先相乘,再与 f()
和 j()
相加。
至于先执行哪个函数,结合律和优先级是保证不了的。当这几个函数影响到同一个对象的时候,该表达式就是 Undefined 的。
我们可以通过管理表达式的求值顺序来避免上述问题。一些 tips:
*++iter
,++iter 改变了 Iter 的值,但 *
需要计算出 ++iter 以后再进行解引用,因此表这种用法没有问题)。算术运算符的优先级可以同样可以参考P166。有几点需要注意的是:
bool 类型的变量应该避免进行算术运算:
bool b = true;
boob b2 = -b;
当 b
为 true 以后,一元运算符 -
将 b
的值从 true
直接提升到了 -1
,而任意不为 0 的 bool 值都为真;因此 b2
也为真。
undefined 算数运算通常由两种情况导致:数学上的无意义和计算机上的溢出(Overflow)。溢出发生在计算结果超出了类型可以表示的范围的时候:
short sv = 32767; //max value for 16bit shor
sv += 1; // overflow
这样的计算也是 undefined 的。
整数除以整数的结果也是整数,小数部分会被抹掉。
末除(Modulus)的 operand 必须是整数:
int iv = 42;
double dv =3.14;
iv % dv; //error
C++11 中, 如果 m
n
均为非零整数,有 m = (m / n) * n + m%n
。也就是说,m%n
与 m
同号(早期的标准允许 m%n
向负无穷方向取整,但现在已经被禁止了)。因此,除非 -m
有溢出导致符号变化(比如 -127 溢出为 128),总有 m
与 m%n
同号。该规则只适用于末除,即:
Operands 要求:
返回值:
false
,反之为真&&
)只有在所有 operands 都为 true 的时候才能为 true||
)只需任意一个 operands 为 true 即为 true
&&
和 ||
的求值策略被称为 Short-Circult Evaluation,即总是从左边的 operand 开始求值。右边的 operand 只有在左边的 operand 不能确定结果的情况下才会进行求值。
/* e.g. && */
index != s.size() && !isspace(s[index]); // right operand will be evaluated unless index is reached the end of s
/* e.g. ||, print newline when s empty or s not empty but hit the '.' */
for (const auto &s : text) {
cout << s;
if (s.empty() || s[s.size() - 1] == '.') {
cout << endl;
}
else
cout << " ";
}
因为 string 对象较大,因此一般使用引用访问会更效率;但因为只需要读,因此上面的例子的循环控制变量使用了 const auto &s 来定义。
逻辑非(!
) 运算符将 operand 的值取反后返还:
if(!vec.empty()) // if vector is not empty
关系运算符(<
、≤
、≥
、>
)用于比较 operand 的大小,返回 bool 类型的值。由于这个原因,存在多个关系运算的情况下,我们不能将其简单的链接在一起使用,而是需要搭配逻辑运算符使用:
if (i < j < k) // i < j return a bool, then the condition is actually compring a bool with k
if (i < j && j < k) // if i < j and j < k.
bool 类型一个重要的用途是用于相等判断。C++ 中判断相等条件语句很简单:
if(val) // true if val is not equal to 0
if(!val) //true if val equals to 0
值得说明的是,上述的条件变量 val 的类型会被隐式的转换成 bool 类型用作比较。很多人会这么写:
if(val == true);
这样写的问题在于,如果 val
不是 bool 类型的变量,val
就不会转变为 bool 类型的变量。取而代之的是,true
会直接转化为与 val
相同的类型的变量。因此,如果 val
是整型,那么 true
就会转变为 1
,因此整个条件就变成了:
if(val == 1)
这和之前的条件几乎是完全不一样的。
使用 bool literal (true / false) 作为比较的 operand 是不好的习惯。这些 Literal 应该只用于与类型为 bool 类型的变量作比较。
很多情况下需要考虑初始化与赋值的区别。
赋值运算符有几个要点:
C++11 的标准中, List initialization 是不能进行 narrowing conversion 的。因此,list 初始化要求赋值运算的两边类型和数量必须匹配:
int k = {3.14}; //error, narrowing conversion
对于类的 List 初始化,如何进行要看类如何是如何重载赋值运算符的。
赋值运算的结合律是从右到左的。比如:
int ival, jval;
ival = jval = 0; // expression will do jval = 0 first; then do ival = jval
多重赋值中,所有左边的 operand 的类型必须跟对应右边的 operand 匹配,或者是可以转换为同样的类型:
int val, *pval;
ival = pval = 0;// error, pointer can't be converted to int
赋值运算经常用于循环的判定中;由于其优先级很低,因此需要括号来保证优先级。比如下面的例子:
int i = get_val();
while(i != 42) {
i = get_val();
}
很显然 i = get_val();
能作为循环的条件。但如果要将其放入循环条件中,就需要加上括号:
int i = get_val();
while((i = get_val()) != 42) { //loop until get 42 for i
}
任意一种复合赋值运算符都等同于如下形式:
a = a op b;
唯一的区别是,使用复合赋值运算符只求值一次,而普通形式会求值两次。
假设用自增运算符(自减也是相同的)对变量 i 进行操作,那么我们会得到两种情况:++i
(prefix) 和 i++
(postfix).
简单的说来:++
可以认作一个函数。当 ++i
这样的形式单独存在时, ++i
与 i++
都是完成了 i = i + 1
的运算。其区别主要体现在作为 operand 对别的对象赋值的时候。
j = ++i; // i = i +1 then j = i
j = i++ // i_temp = i then i = i +1 then j = i_temp
也就是说,函数 ++i 的返回值是 i+1
,函数 i++ 的返回值是 i
。至于 i = i + 1
,无论是 prefix or postfix 都会首先执行。下面是比较细节的分析:
// prefix
int& int::operator++() // returne a reference
{
*this += 1; // i = i + 1
return *this; // return updated i
}
//postfix
const int int::operator++(int) //return int
{
int oldValue = *this; // save i to i_temp
++(*this); // i = i + 1
return oldValue; // return i_temp
}
从结果来说:
从程序效率上来说:
尽量在能用 ++i 的地方都用 ++i。从上面的信息里可以看出,++i 避免了开辟一块新的临时空间来返回原有 i 的值。在某些复杂迭代器的运行中,++i 的性能提升会显得尤其明显。
Postfix 形式的自增应用于希望使用当前的值后再将其自增的情形。一个典型的例子就是迭代器:比如我们要遍历打印一个 vector,有了 *iter++
这种形式,语句就可以更简洁了:
auto pbeg = v.begin();
while (pbeg != v.end())
*pbeg++; // print current value then move to the next position.
对于 *pbeg++ 来说,自增的优先级高于解应用。所以这个表达式也等价于 *(pbeg++)
。同时 pbeg++
返回的是当前的 pbeg
,因此该表达式按如下的顺序完成了三个步骤:
再次提醒,除非operand的运算顺序明显(如上例),我们应该尽量避免用多个运算对同一个数值进行运算/修改。比如像下面的情形:
while (beg != s.end() && !isspace(*beg))
*beg = toupper(*beg++);
beg
迭代器同时被两个表达式影响。因为结合律无法保证求值顺序,因此可能带来两种结果:
*beg = toupper(*beg) // left first
*(beg+1) = toupper(beg) // right first
由这样的情形带来的 undefined 操作是一定要注意和避免的。
成员访问运算符(Menber Access Operators)有 “.” 和 “→” 两种形式。 “.”(dot)运算符比较常用,一般用于访问类成员。→
是一个解引用 + 访问成员函数的组合操作简写:
string s1 = "a string", *p = s1;
auto n = s1.size(); //member access
n = (*p).size(); // get size from what p points
n = p->size(); equivalent to (*p).size();
→
运算通常用于知道对象的指针,并希望调用对象的成员函数的情形。正常写法是上面代码的第三栏那样,→
是该写法的简化版本,避免了忘记对解引用加括号的问题(再次回顾一下解引用的优先级不明确会带来的问题)。
对于.
运算,运算结果与参与运算对象的类型相同(这里指左值/右值)。在 (*p).size()
里,*p
是左值,所以我们可以得知 →
运算符返回的是左值。
if-else 语句可以用条件运算符的形式表达出来:
condition ? expr1(if_true) : expr2(if_false);
条件运算符的结果可以作为另外一个条件运算符的参数(可以是条件也可以是结果表达式)。下例通过两个条件运算的嵌套将输入的成绩分成了 3 个结果段:
finalgrade = (grade > 90) ? "high pass"
:(grade > 60) ? "fail" : "pass";
需要注意的是,条件运算符遵循右结合律,因此会将 (grade > 60) ? “fail” : “pass”
作为整个 operand 来使用。
条件运算符的优先级非常低。在使用 cout 对象输出条件运算符的结果时,需要使用 parenthesis 来强调条件运算符的优先级:
cout << ((grade > 60) ? "pass": "fail");
否则就会先输出条件判定的结果(0 或者 1),然后使用 cout 对象本身进行条件判断。
位运算符(Bitwise Operators)接收整型的 operand,将其视作一系列的 bit 来进行运算。位运算符允许我们对单独的 bit 进行操作。位运算符同时也适用于标准库的 bitset
类型(一种可以表示任意大小二进制集合的数据类型)。
位运算的 operand 可以是带符号的数,但需要注意是,Left Shift 操作很可能改变符号位(Sign bit)的值。这样的操作是 undefined 的。在值为负数的情况下,位运算符对符号位如何操作是取决于机器的,因此最好将位运算符用于处理 unsinged 类型的数据。
常见的位运算符如下:
位移运算符(Bitwise Shfit Operators)的作用是对指定的二进制数据进行位移操作。位移运算符具有两个 operands,左边的 operands 是需要被移动的二进制数据,右边的 operands 则是一个非负整数,用于指定移动的位数。位移运算最终会产生原有数据的位移过后一份拷贝。
右边的 operand 必须是非负整数,并且必须小于结果数据的位数。任何造成了超出结果范围的位移都将导致 undefined.
位移运算符分为左运算符 «
和右运算符 »
:
位求反运算符 (Bitwise NOT operator) ~
会将当前的二进制数据按位求反:
上图的数据从 char
提升到了 int
(原因见后)。提升过后所有的空位都会被 0 填充,经过反转都变成了 1.
位与(bitwise AND)&
、位或(bitwise OR)|
和位异或(bitwise NOR)^
这三个运算符按照指定的规则组合 operands:
&
:operands 都有 1,则为 1, 否则为 0|
: operands 两边至少有一个 1,则为 1,否则为 0^
: operands 两边不相等(1 和 0 或者 0 和 1)则为 1,否则为 0位移运算符遵循左结合律(包括其 stream 重载版本):
cout << 1 << 2 << endl; //equivalent to the below line
((cout << 1) << 2) << endl;
位移运算符的优先级低于算数运算符,在输入输出 operand 有运算的时候需要用括号保证优先级。
sizeof
运算符会以字节为单位返回一个类型或者是表达式所占的内存空间。sizeof
遵循右结合律,返回值是类型为 size_t
的常量表达式。该运算符有两种形式:
sizeof (type)
sizeof expr
第二种形式下,sizeof 返回的是表达式返回值所占用的空间。Sales_data data, *p;
sizeof(Sales_data);
sizeof data; //equivalent to sizeof(Sales_data);
sizeof p; // pointer size
sizeof *p; //size of type to which p points
sizeof data.revenue; // size of the member
sizeof Sales_data::revenue; //size of the member, alternative way
上面例子中:
sizeof *p
的执行顺序是先解引用。因为 sizeof 并不会执行 operand,因此该指针是否有效在这里是不重要的。sizeof 返回的结果部分依赖于被处理的类型:
1
。因为 sizeof 在 array 上的特性,可以通过 array 的总大小除以 array 元素的大小来得到 array 的长度:
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz];
逗号运算符(Comma operator)获取两个 operands, 保证了从左向右求值的顺序。左边的表达式会被优先求值,其结果会被丢弃。逗号运算符返回的结果是其右边表达式返回的值。如果右边的表达式是 lvalue,那么返回值也是 lvalue。
逗号运算符的一个常见的应用是在 for 循环中:
vector<int>size_type cnt = ivec.size();
for(vector<int>size_type ix = 0; ix !=ivec.size(); ++ix, --cnt) {
ivec[ix] = cnt;
}
只要 ++ix
一直可执行,那么 –cnt
就会一直运行下去。
在运算中,如果两种类型可以转换,则称这两种类型是有关联的。基于某些原因,编译器会在程序员没有参与的情况下对参与运算的类型进行转换,这种转换被称为隐式转换(Implicit conversion)。
隐式转换是否发生取决于 operand 的类型。可能发生隐式转换的情况有:
算术类型的转换分为:
需要说明的是,unsigned fit signed 在这里指的是 signed operand 比 unsigned operand 大一号,并且可以表示 unsigned type。 按书上的例子来说,如果 long
大于 int
,且 unsigned int 所有的值都可以用 long 表示,那么所有的 unsigned int 都会被转化成 long。
Perfect Ref: C++ Implicit Conversion (Signed + Unsigned)
bool flag; char cval;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; // 'a' promoted to int, then that int converted to long double
dval + ival; // ival converted to double
dval + fval; // fval converted to double
ival = dval; // dval converted (by truncation) to int
flag = dval; // if dval is 0, then flag is false, otherwise true
cval + fval; // cval promoted to int, then that int converted to float
sval + cval; // sval and cval promoted to int
cval + lval; // cval converted to long
ival + ulval; // ival converted to unsigned long
usval + ival; // promotion depends on the size of unsigned short and int
uival + lval; // conversion depends on the size of unsigned int and long
在绝大部分应用数组的表达式里,当我们使用数组的时候,数组会自动的转化成指向数组第一个元素的指针。这个转换有如下几个例外:
类的转换通过自定义实现,类的转换只能每次一次。
while (cin >> s){} //the result of (cin >> s) is an istream class object and converted to the bool.
int ia[10]; // array of ten ints
int* ip = ia; // convert ia to a pointer to the first element
char *cp = get_string();
if (cp) /* ... */ // true if the pointer cp is not zero
while (*cp) /* ... */ // true if *cp is not the null character
int i;
const int &j = i; // convert a nonconst to a reference to const int
const int *p = &i; // convert address of a nonconst to the address of a const
int &r = j, *q = p; // error: conversion from const to nonconst not allowed
cast-name<type> (expression)
其中 type 是被转换的目标类型,expression 是要转换的值。转换引用类型将会得到一个左值。static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
。
任何有明确定义,且不包含 low_level const 的表达式的类型转换,都可以使用 static_cast 来转换,比如:
//force a floating-point division
double slope = static_cast<double>(j) / i;
将较大的算术类型赋值给较小的算术类型的时候,static_cast 非常有用。它明确的告诉了编译器我们得知了将损失精度,但同时也并不在乎的意愿。编译器往往会给出一些 warning 来警告由大至小的类型转换,这时可以将警告关闭。void*
的指针类型转换回原本类型的指针:
void *p = &d;
double *dp = static_cast<double*>(p);
进行此类操作的时候需要确保转换后得到的类型就是指针所指的类型,否则结果是 undifined.
const_cast 用于将 operand 中的 low_level_const 的限制去掉:
const char *pc;
char *p = const_cast<char*>(pc);// ok, but writing through p is undefined
需要注意的是,const_cast 只能改变写权限,不能改变被访问对象的类型。比如上面的例子, pc
被定义为指向一个常量的指针;我们通过了 const_cast 获取了 pc
的写权限,并将 pc
赋值给了新指针 p
。但由于 p
仍然指向常量,所以通过 p
去修改指向的值是 Undefined 的。也就是说,
使用 const_cast 对常量进行写操作是 Undefined 的。const char *cp;
char *q = static_cast<char*>(cp); //error static_cast can't cast away const
static_cast<string>(cp); //ok, convert string liertal to string
const_cast<string>(cp); //error, const_cast only change constness
这是一个不常用的、危险的类型转换。从结果上来说,reinterpret_cast 会导致当前被转换的类型直接被视作为另外一种类型(不通过任何的类型转换)。比如:
double d = 01101010 00111100 01101010 01000001;
int i = reinterpret_cast<int>(d);
经过该转换后,i
变成了一个拥有 32
bit 的 int 类型。int *ip;
char *pc = reinterpret_cast<char*>(ip);
ip
是指向 int 的指针,这里被重新诠释为了指向 char 的指针。但实际上,ip
依然指向一个 int 类型。因此,如果把 ip
当成 char*
来使用,就会导致 run-time 时期的错误。void*
类型的参数。如果想在 C++ 使用这些库,我们可以使用 reinterpret_cast 将这些参数转换 void*
类型,等到函数内部的时候再使用 reinterpret_cast 将其转换回来使用。早期版本的 C++ 提供两种 cast 的形式:
type (expr); //function-style
(type) expr; //cstyle
避免使用 cast:
显示转换会直接干扰正常的类型检测,因此最好避免使用 cast,尤其是 reinterpret_cast。对于其他的 cast 来说, static_cast 与 dynamic_cast 要尽量的少使用;const_cast 的使用除了在函数重载的场景以外,其他都会被考虑为是设计缺陷。
因此,尽量使用其他方式取代 cast,如果无可避免,设置 scope 来控制被 cast 值的影响范围,并在文档中注明对相关类型的推测。