Fork me on GitHub

表达式基础

一、前言

介绍C++中常见的表达式。

基础概念

组合运算符和运算对象

对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级、结合律以及运算对象的求值顺序。

重载运算符

左值和右值

左值可以位于赋值语句的左侧、右值则不能。

当一个对象被用于右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的为止)。

求值顺序(重点)

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。

1
int i = f1()*f2();

我们是无法知道到底f1在f2之前调用还是f2在f1之前调用。

对于那些没有指定执行顺序的运算符来讲,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义行为,比如:

1
2
int i = 0;
cout<<i<<" "<<++i<<endl;

表达是的行为不可预知,编译器执行i或者++i的顺序是未知的。

以下4中运算符明确规定了运算对象的求值顺序&&(逻辑与) ||(逻辑或) ?:(条件运算符) ,(逗号运算符)。

求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关,在一条形如f()+g()*h()+j()的表达式中:

  • 优先级规定,g()的返回值和h()的返回值相乘。
  • 结合律规定,f()的返回值先与g()和h()的乘积相加,所得结果再与j()的返回值相加。
  • 对于这些函数的调用顺序没有明确规定。

如果f、g、h和j是无关函数,它们既不会改变同一对象的状态也不执行IO任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为。

建议: 处理复合表达式以下两条经验准则对书写复合表达式有益:

  • 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
  • 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。例如,在表达式*++iter中,递增运算符改变iter的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似的情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。显然,这是一种很常见的用法,不会造成什么问题。

算术运算符

%运算符

如果m%n不等于0,则它的符号和m相同。

m%(-n)  等于  m%n
(-m)%n  等于 -(m%n)
-21 % -8 = -5
21 % -5 = 1

逻辑和关系运算符

逻辑与(&&)和逻辑或(||) 都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为 短路求值

text是存储这string对象的vector,要求输出string对象的内容并且在遇到空字符串或者以句号结束的字符串时进行换行。

1
2
3
4
5
6
7
8
9
//s是对常量的引用;元素既没有被拷贝也不会被改变
for(const auto &s : text) {
cout<<s;
if(s.empty()||s[s.size()-1] == '.'){
cout<<endl;
}else{
cout<<" ";
}
}

值得注意的是,s被声明成对常量的引用,因为text的元素是string对象,可能非常大,所以将s声明成引用类型可以避免对元素的拷贝;又因为不需要对string对象做写操作,所以声明成对常量的引用。

优先级注意

算数运算符>关系运算符>逻辑运算符

赋值运算符满足右结合律

赋值运算符满足右结合律,这一点与其他二元运算符不太一样。

1
2
int ival,jval;
ival = jval = 0; //正确,都被赋值为0

因为赋值运算符满足右结合律,所以靠右的赋值运算 jval=0作为靠左的赋值运算符的右侧运算对象。又因为赋值运算符返回的是其左侧运算对象,所以靠右的赋值运算的结果(jval=2返回的结果为左侧运算对象jval)被赋给了ival。

1
cout<<(jval = 2)<<endl; // 输出2

赋值运算符优先级较低

1
2
3
4
5
int i ;
//一种很好的写法
while((i=get_value()) != 42) {
//其他处理
}

注意: 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

递增和递减运算符

递增和递减有两种形式:前置版本和后置版本。

1
2
3
int i =0,j;
j = ++i; //j = 1,i = 1; 前置版本得到递增之后的值
j = i++; //j = 1,i = 2; 后置版本得到递增之前的值

区别:前置版本将对象本身作为左值返回,后置版本则将对象的原始副本作为右值返回。

建议:除非必须,否则不用递增递减运算符的后置版本

有C语言背景的读者可能对优先使用前置版本递增运算符有所疑问,其实原因非常简单:前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。

混用解引用和递增运算符

1
2
//推荐写法
cout<<*iter++<<endl; //等价于*(iter++)

后置运算符的优先级高于解引用运算符。

1
vec[ival++] <= vec[ival]; //未定义的错误,先求左侧的值还是先求右侧的值不确定

成员访问运算符

点运算符和箭头运算符都可用与访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式ptr->men等价于(*ptr)mem :

1
2
3
4
string s1 = "a string", *p = &s1;
auto n = s1.size();
n = (*p).size(); //运行p所指对象的size成员
n = p->size(); //等价于(*p).size()

注意:解引用运算符的优先级低于点运算符。

1
*p.size() // 错误,p是一个指针,它没有名为size的成员

条件运算符

优先级

条件运算符优先级高于赋值、逗号运算符,低于其他运算符。

例如:

1
2
3
m<n ? x : a+3 等价于:(m<n) ?(x) :(a+3)
a++>=10 && b-->20 ? a : b 等价于:(a++>=10 && b-->20) ? a : b
x=3+a>5 ? 100 : 200 等价于:x= (( 3+a>5 ) ? 100 : 200 )

结合性

条件运算符具有右结合性。

当一个表达式中出现多个条件运算符时,应该将位于最右边的问号与离它最近的冒号配对,并按这一原则正确区分各条件运算符的运算对象。

例如:

 w<x ? x+w : x<y ? x : y
与 w<x ? x+w : ( x<y ? x : y) 等价
与 (w<x ? x+w : x<y) ? x : y 不等价
注意: 随着条件运算嵌套的增加,代码的可读性急剧下降。因此,条件运算符的嵌套最好别超过两到三层。

位运算符(基础,待补充)

一个使用位运算符的例子

假设一个班级有30个学生,我们用一个二进制位来代表某个学生在依次测试中是否通过,显然全班的测试结果可以用一个无符号整数来表示:

1
unsigned long quizl = 0 ; //我们把这个值当成是位的集合来使用

将quizl类型定义位unsigned long,这样,quizl在任何机器上都将至少拥有32位;给quizl赋一个明确的初始值,使得它的每一位在开始时都有统一且固定的值。

1
2
3
4
5
6
7
//1UL是一个unsigned long类型的整数字面值1
//1UL<<27; 生成一个值,该值只有第27位为1
quizl |= 1UL<<27; //表示学生27通过了测试
quizl &= ~(1UL<<27); //学生27未通过测试
bool status = quizl & (1UL<<27); //学生27是否通过了测试?

sizeof

sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得到的值是一个size_t类型的常量表达式。

sizeof的三种语法形式:

1
2
3
sizeof(object); //sizeof(对象);
sizeof(type_name); //sizeof(类型);
sizeof object; //sizeof对象;
1
2
3
4
5
int i;
sizeof(i); //ok
sizeof i; //ok
sizeof(int); //ok
sizeof int; //error

既然写法3可以用写法1代替,为求形式统一以及减少我们大脑的负担,第3种写法,忘掉它吧!实际上,sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的不同对象其sizeof值都是一致的。这里,对象可以进一步延伸至表达式,即sizeof可以对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式进行计算。

1
sizeof(*p); //指针所占的空间大小,与指针指向的类型无关

sizeof不会实际求运算对象的值,所以即使p是一个无效(即未初始化)的指针也不会有什么影响,在sizeof的运算对象中解引用一个无效指针仍然是一种安全行为,因为指针实际并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。

C++11新标准允许我们使用作用域来获取成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象。因为要想知道类成员的大小无须真的获取该成员。

sizeof运算符的结果部分地依赖于其作用的类型:

  • 对char或者类型为char的表达式指向sizeof运算,结果为1;
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小;
  • 对指针指向sizeof运算得到指针本身所占空间的大小;
  • 对解引用指针执行sizeof运算符得到指针指向的对象所占空间的大小,指针不需有效。
  • 对数组执行sizeof运算限制得到整个数组所占空间的大小,等价于对数组这所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:

1
2
3
// sizeof(ia)/sizeof(*ia)返回ia的元素数量
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz]; // 正确:sizeof返回一个常量表达式

因为sizeof的返回值是一个常量表达式,所以我们可以用sizeof的结果声明数组的维度。

-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/10/14/表达式基础/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!