目录

表达式

C++ Primer 笔记 第四章


表达式基础

运算符分为一元运算符(Unary operator) 和二元运算符 (Binary operator)。“元”代表有几个 operands。每个运算符的运算优先级(Precedence) 和结合律(Associativity)由运算符自身决定。在执行表达式的过程中,有两种情况会出现:

左值和右值

所有的表达式都被区分为左值Lvalue)和右值Rvalue)。这种叫法继承自 C,意味着可以在 = 赋值运算符左边的值就是左值,否则就是右值。在 C++ 中,左值和右值的区分相对更加复杂:比如某些情况下的 const 变量就不能被赋值,某些函数的结果对象但返回右值等等。总的来说,当表达式是左值的时候,我们使用该表达式的 idendity (在内存中的位置);当表达式是右值的时候,我们使用该表达式的(内容)。

通俗的来说,左值是明确有定义的,在内存中固定存在的对象(地址确定),而右值大多数代表临时对象(有内容,无确定地址)

运算符是根据需求左值/右值和返回左值/右值来区分的。一个比较重要的特点是:

当需求右值的时候(也就是希望使用内容而不是位置的时候),可以使用左值代替(此时使用左值中的内容),但是反过来不行(右值只能表示内容)。

之前见过的几个例子:

  1. 赋值运算中,“=” 运算左边如果是一个 non-const 的左值,那么表达式得到的结果类型也是左值。(比如 a = 10; 得到的是 a)
  2. 能取到地址的值都是左值,而取地址操作结果返回一个指针,是右值。
  3. 解引用(*)运算,下标运算和迭代器的解引用,都产生左值。
  4. 迭代器的自增 / 自减 也需要左值进行运算,但只有 prefix 版本返回左值(++ / –i)。
decltype 的返回值类别

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();

至于先执行哪个函数,结合律和优先级是保证不了的。当这几个函数影响到同一个对象的时候,该表达式就是 Undefined 的。

我们可以通过管理表达式的求值顺序来避免上述问题。一些 tips:

  1. 用括号保证优先级。
  2. 如果改变了operand 的值,就不要将其放到同一个表达式的其他任何地方(例外:复合表达式中,虽然一个子表达式改变了 operand,但该表达式会作为另外一个子表达式的 operand 的时候,上述规则无效。比如 *++iter,++iter 改变了 Iter 的值,但 * 需要计算出 ++iter 以后再进行解引用,因此表这种用法没有问题)。

算术运算符

算术运算符的优先级可以同样可以参考P166。有几点需要注意的是:

  1. 一元运算符的优先级 > 二元乘除 > 二元加减。
  2. 运算的时候都是从左到右(左结合律)
  3. 算术运算符得到的结果都是右值
bool 不能用于计算

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%nm 同号(早期的标准允许 m%n 向负无穷方向取整,但现在已经被禁止了)。因此,除非 -m 有溢出导致符号变化(比如 -127 溢出为 128),总有 mm%n 同号。该规则只适用于末除,即:

逻辑 / 关系运算符

Operands 要求:

返回值:

逻辑运算符

逻辑与和逻辑或

&&|| 的求值策略被称为 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.

Equality Test & bool

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 类型的变量作比较。

赋值运算符

很多情况下需要考虑初始化与赋值的区别。

赋值运算符有几个要点:

  1. 赋值运算符的左边必须是可以修改的左值。常量 / literal 都不能用。
  2. 赋值结果的类型与左边的 operand 相同,也是左值。
  3. 赋值运算中如果左右两个 operand 的类型不同,右 operand 会转化成和左 operand 相同的类型,比如将浮点数赋值给整数,最后得到的是整数。

List 初始化

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 这样的形式单独存在时, ++ii++ 都是完成了 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 的性能提升会显得尤其明显。

Ref: 在程序开发中,++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,因此该表达式按如下的顺序完成了三个步骤:

  1. pbeg 留下一份原始拷贝
  2. pbeg 自增去了下一个位置
  3. 解应用通过留下的拷贝读取了之前 pbeg 所处位置的元素
operand can be evaluated in any order

再次提醒,除非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 Shift Operators

位移运算符(Bitwise Shfit Operators)的作用是对指定的二进制数据进行位移操作。位移运算符具有两个 operands,左边的 operands 是需要被移动的二进制数据,右边的 operands 则是一个非负整数,用于指定移动的位数。位移运算最终会产生原有数据的位移过后一份拷贝。

右边的 operand 必须是非负整数,并且必须小于结果数据的位数。任何造成了超出结果范围的位移都将导致 undefined.

位移运算符分为左运算符 « 和右运算符 »

Bitwise NOT operator

位求反运算符 (Bitwise NOT operator) ~ 会将当前的二进制数据按位求反: 上图的数据从 char 提升到了 int(原因见后)。提升过后所有的空位都会被 0 填充,经过反转都变成了 1.

AND、OR 和 NOR

位与(bitwise AND)& 、位或(bitwise OR)| 和位异或(bitwise NOR)^ 这三个运算符按照指定的规则组合 operands:

位移运算符遵循左结合律

位移运算符遵循左结合律(包括其 stream 重载版本):

cout << 1 << 2 << endl; //equivalent to the below line
((cout << 1) << 2) << endl;
位移运算符的优先级低于算数运算符,在输入输出 operand 有运算的时候需要用括号保证优先级。

sizeof / 逗号运算符

sizeof 运算符

sizeof 运算符会以字节为单位返回一个类型或者是表达式所占的内存空间。sizeof 遵循右结合律,返回值是类型为 size_t 的常量表达式。该运算符有两种形式:

sizeof (type)
sizeof expr
第二种形式下,sizeof 返回的是表达式返回值所占用的空间

需要提到的是,sizeof 并不会执行其 operand:
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 的结果与类型

sizeof 返回的结果部分依赖于被处理的类型:

因为 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 的类型。可能发生隐式转换的情况有:

  1. 小整型的提升:运算中小于 int 的整型会第一时间转化为合适大小的整型。
  2. 非 bool 两类型转化为 bool 类型:在条件语句中会进行 non-bool to bool 的转化
  3. 初始化 / 赋值中的类型匹配(右边匹配左边):初始化中,会将 initializer 的类型转化为对象的类型;赋值中,会将右边 operand 的类型转换为左边 operand 的类型。
  4. 算术 / 关系运算中,operands 需要转化为相同类型。
  5. 函数调用的类型转换。

算术转换

算术类型的隐式转换

算术类型的转换分为:

需要说明的是,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.

Examles

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 是要转换的值。转换引用类型将会得到一个左值

显式转换分为4种类型:static_castdynamic_castconst_castreinterpret_cast

static_cast

任何有明确定义,且不包含 low_level const 的表达式的类型转换,都可以使用 static_cast 来转换,比如:

//force a floating-point division
double slope = static_cast<double>(j) / i;
将较大的算术类型赋值给较小的算术类型的时候,static_cast 非常有用。它明确的告诉了编译器我们得知了将损失精度,但同时也并不在乎的意愿。编译器往往会给出一些 warning 来警告由大至小的类型转换,这时可以将警告关闭。

除此之外,static_cast 还能用于处理一些编译器不会自动处理的类型转换,比如将指向 void* 的指针类型转换回原本类型的指针:
void *p = &d;
double *dp = static_cast<double*>(p);
进行此类操作的时候需要确保转换后得到的类型就是指针所指的类型,否则结果是 undifined.

const_cast

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

这是一个不常用的、危险的类型转换。从结果上来说,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 时期的错误。

从上面的例子看来,reinterpret_cast 并没有什么作用;但通过 C++ 的标准,reinterpret_cast 实际上被用于这样一种场景:为了使用 C library. 很多 C library 中的函数会接收 void* 类型的参数。如果想在 C++ 使用这些库,我们可以使用 reinterpret_cast 将这些参数转换 void* 类型,等到函数内部的时候再使用 reinterpret_cast 将其转换回来使用。

Ref: question about reinterpret_cast

Old-style cast

早期版本的 C++ 提供两种 cast 的形式:

type (expr); //function-style 
(type) expr; //cstyle

避免使用 cast:

显示转换会直接干扰正常的类型检测,因此最好避免使用 cast,尤其是 reinterpret_cast。对于其他的 cast 来说, static_cast 与 dynamic_cast 要尽量的少使用;const_cast 的使用除了在函数重载的场景以外,其他都会被考虑为是设计缺陷。

因此,尽量使用其他方式取代 cast,如果无可避免,设置 scope 来控制被 cast 值的影响范围,并在文档中注明对相关类型的推测。