[cpp primer随笔] 07. 运算符细则

1. 赋值运算符返回被赋值对象引用

C++的赋值运算符返回被赋值对象的引用。

返回:如果将运算符视作函数,则称作返回值;如果将运算符结合运算对象视作表达式,则称作表达式的运算结果。

这种特性和Python的海象运算符:=类似,且十分有用。普通的Python赋值语句仅执行赋值功能,而海象运算符的执行结果为对象本身,这可以极大简化语句的书写模式,下面是一个例子。

# 不使用海象运算符
a = getList()
if len(a) > 0:
	...
# 使用海象运算符
if len(a := getList()) > 0:
	...

C++中的效果与之相近,例如我们要在每次循环中改变一个变量的值,并根据该值判断是否符合循环终止条件。

// 不使用赋值运算符的返回值
int i = get_value();
while(i != 42){
	i = get_value();
}

// 使用赋值运算符的返回值
int i;
while((i = get_value()) != 42){
	// ...
}

可以看出,后者可读性更强。

2. 递增递减运算符的前置后置优劣之分

递增运算符的前置版本++i和后置版本i++是大家的老朋友了。
两者都会对i进行+1的操作,但区别在于,前者返回的是i的左值引用,而后者返回的是i+1前的右值副本。

int i = 0;
int &r_i = ++i; // √, r_i绑定的就是对象i,此时r_i = 1
int j = i++; // √,i++运算结果为i加1前的右值副本,此时j = 1
int &r_j = i++; // error: 不能创建非常量右值引用

需要注意的是,很多时候,我们写i++却并未使用其加一前的副本(例如在循环的更新表达式中,我们只是单纯的i++)。这可能会带来性能影响。
具体而言,对于内置类型int,编译器会做一定程度上的优化;然而对于一些支持递增递减操作的复杂类型,如迭代器,每次计算得到的右值副本将无条件存储下来,造成空间上的浪费。这里做个实验说明:
首先,用内置算术类型int执行递增操作,为突出实验效果,前置后置均执行两次。

void test1(){
    int i = 0;
    ++i;
    ++i;
    i++;
    int r_i = i++; // 注意这里将i++结果进行存储
}

将上述程序在-O0下进行编译(即不开启编译器优化),得到汇编代码。
[cpp primer随笔] 07. 运算符细则_第1张图片
可以看到如果不保存变量的话,前置和后置的汇编结果没有差异。也就是说,对于int这种内置算术类型,在for循环中用++ii++没有太大区别。
接下来使用STL容器vector的迭代器进行递增递减操作。

void test2(){
    vector<int> a;
    auto pa = a.begin(); // pa的类型为iterator>
    ++pa;
    ++pa;
    pa++;
    pa++;
}

[cpp primer随笔] 07. 运算符细则_第2张图片
可以看到尽管pa++的值我们没有用到,但是其右值副本依旧会在栈空间中保存下来,这会带来不必要的内存开销。
因此,如果不是对加一前的副本有所需要,请尽可能使用递增/递减匀算符的前置版本

3. 条件运算符的低优先级

三目条件运算符cond ? expr1 : expr2可以在一条语句起到类似if...else...的作用。
然而三目条件运算符的优先级较低,尤其是与IO运算符(移位运算符)一起使用时需注意。

cout << ((grade < 60) ? "fail" : "pass"); // 正确,输出fail或者pass
cout << (grade < 60) ? "fail" : "pass"; // 输出1或者0,但这个表达式的值为"fail"或者"pass"
cout << grade < 60 ? "fail" : "pass"; // 编译错误,无法比较cout和60

?:<的优先级均低于<<,因此在使用时若没有加括号,则前两者的运算对象将优先与<<结合,得到错误结果。

4. 位运算使用

位运算符的内置版本可以作用于整型,也可以作用于标准库类型bitset。需要注意两点:

  • 如果运算对象为小整型,则会被自动提升为大整型;
  • 如果运算对象为带符号类型,则对于符号位的处理结果是未定义行为,可能会改变整型数据符号,因此最好使用无符号整型来做位运算

4.1 两数交换

借助异或运算的自反性,可以在不使用中间变量的情况下,完成对两个数的交换。

int a = 114, b = 514;
a = a ^ b; // 即先计算出a跟b的差异
b = a ^ b; // 该差异跟b异或得到a,跟a异或得到b
a = a ^ b; // 此时,a = 514, b = 114

5. sizeof运算符使用

sizeof运算符用于计算类型名或表达式所占字节数,有两种使用形式,分别为sizeof (type)sizeof expr。当sizeof的运算对象为一个表达式时,并不会真的对表达式求值,而是推断出表达式的结果类型,说到底还是计算类型大小。

根据上述规则,sizeof作用于不同类型时有以下特性:

  • char:返回1。
  • 引用:返回绑定元素类型大小。
  • 指针:得到指针变量本身大小。
  • 指针解引用:得到指针指向元素类型的大小,该指针无需有效。
  • 类成员:C++ 11后可以通过域运算符::直接计算类成员大小,而无需实例化该类。例如:sizeof(classA::memA)。这也从侧面印证了sizeof只看类型,而无需具体对象或值。
  • 数组:返回数组总大小,相当于对数组每个元素都求一遍sizeof的结果的和。计算数组元素个数:sizeof arr / sizeof arr[0]
  • vectorstring这种可变长度容器,将返回固定部分大小,而不考虑可以变化的部分。(即与元素数量多少无关)

6. 逗号运算符返回值

逗号运算符expr1, expr2的返回结果为右侧运算对象的值。

bool a = false, b = true;
if(a, b){
	cout << "b是逗号表达式的结果"; // 此句被执行
}

7. 运算对象的求值顺序没有明确规定

在同一个运算表达式中可能存在多个运算对象,这些运算对象的求值顺序C++没有明确规定
然而,当这些运算对象中涉及同一个对象,且试图改变该对象的值时,这将被视作一种未定义行为(Undefined Behavior, UB)。

int i = 0;
i = (++i) + (i++); // 未定义行为(谭*强:你再骂?)

上面这句在g++ 11.4.0中的编译结果为 i = 4 ,clang 16.0.0的编译结果为 i = 3,且会发出警告warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
因此,一定要避免在运算表达式中对同一个对象进行顺序未定的更改

你可能感兴趣的:(C++,c++)