《C++ primer》chapter 4: 表达式

1. 基础

C++定义了一元运算符和二元运算符,作用于一个运算对象的运算符是一元运算符,如取址符(&)和解引用符(*);作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和加法法运算符(+)。

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

当运算符作用于类类型的运算对象时,用户可以自定义其含义,因为这种自定义的过程事实上是为已经存在的运算符赋予另外一层含义,所以称之为重载运算符。

C++表达式要么是左值要么是右值。当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

使用关键字decltype的时候,左值和右值有所不同,如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型。假定p的类型是int,因为解引用运算符生成左值,所以decltype(p)的结果是int&。另一方面,取址运算符生成右值,所以decltype(&p)的结果是int**,即指向整型指针的指针。

优先级与结合律和括号共同决定了运算中符合表达式的运算组合方式。

处理符合表达式的建议:

  • 拿不准的时候最好用括号来强制表达式的组合关系符合程序的逻辑要求
  • 除了要改变运算对象的子表达式本身就是另一个子表达式的运算对象这一例外情况,当改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

2. 算术运算符

除非特殊说明,算术运算符都能用作任意算术类型,以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。再表达式求值之前,小整数类型的运算对象会提升成较大整数类型,所有运算对象最终都会转换成同一类型。

整数相除的结果还是整数,如果商含有小数部分,则直接忽略。C++11规定,不论两个运算对象的符合是否相同,商一律向0取整。

如果m和n是整数且n非0,则表达式(m/n)*n + m%n的结果与m相等。除了-m导致溢出的情况,其他时候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。

3. 逻辑运算符

短路求值:

  • 对于逻辑运算与(&&)来说,当且仅的左侧运算对象为真时才对右侧运算对象求值。
  • 对于逻辑运算或(||)来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
// s是对常量的引用;元素既没有被拷贝也不会被改变
for (const auto &s : text) {
    cout << s;
    // 遇到空字符串或者以句号结束的字符串进行换行
    if (s.empty() || s[size()-1] == '.')
        cout << endl;
    else
        cout << " "; // 否指用空格隔开
}

4. 赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值,C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。

如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且即使该值转换的话,其所占空间也不该大于目标类型的空间。无论左侧运算对象的类型是什么,初始值列表都可以为空,此时编译器创建一个值初始化的临时量并将其赋给左侧运算对象。

赋值语句满足右结合律,对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同,或者可以由右边的对象类型转换得到。

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

int ival, *pval;
ival = pval = 0;	// 错误,不能把指针的值赋给int
string s1, s2;
s1 = s2 = "OK";		// 字符串字面值可以转换为string

5. 递增和递减运算符

递增和递减运算符由两种形式,前置版本和后置版本,目前为止用到的都是前置版本,后置版本也会将运算对象加1或减1,但是求值结果是对运算对象改变之前的那个值的副本:

int i = 0, j;
j = ++i;	// j = 1, i = 1
j = i++;	// j = 1, i = 2

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

在一条语句中混用解引用和递增运算符:

// 如果我们想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增递减运算符的后置版本
auto pbeg = v.begin();
// 输出元素直至遇到第一个负值为止
while (pbeg != v.begin() && *beg >= 0)
    cout << *pbeg++ << endl;	// 输出当前值并将pbeg向前移动一个元素

6. 成员访问运算符

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

string s1 = "a string", *p = &s1;
auto n = s1.size();		// 运行string对象s1的size成员
n = (*p).size();		// 运行p所指对象的size成员,加括号是因为解引用运算符的优先级低						   // 于点运算符
n = p->size();		// 等价于(*p).size()

7. 条件运算符

条件运算符允许我们把简单的if-else逻辑嵌入到单个表达式中,条件运算符按照如下形式使用:cond ? expr1: expr2;

string finalgrade = (grade < 60) ? "fail": "pass";
// 嵌套条件运算
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail": "pass";

随着条件运算嵌套层数增加,代码可读性急剧下降,因此,条件运算的嵌套层数最好别超过两到三层。

条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它的两端加上括号。

cout << ((grade < 60) ? "fail": "pass"); // 输出pass或者fail
cout << (grade < 60) ? "fail": "pass"; // 输出1或者0

8. 位运算符

运算符 功能 用法
~ 位求反 ~ expr
<<(>>) 左(右)移 expr1 << expr2 (expr1>>expr2)
& 位与 expr1 & expr2
^ 位异或 expr1 ^ expr2
| 位或 expr1 | expr2

关于符号位如何处理没有明确的规定,所以强烈建议仅仅将为运算用于处理无符号类型。

// 使用位运算符表示学生成绩
unsigned long quiz1 = 0;		// 把这个值当成是位的集合来使用
quiz1 |= 1UL << 27;				// 表示学生27通过了测验
quiz1 &= ~(1UL << 27);		// 学生27没有通过测验
bool status = quiz1 & (1UL << 27);	// 学生27是否通过了测验?

移位运算符满足左结合律

移位运算符的优先级不高不低,介于中间,比算术运算符的优先级低,比关系运算符,赋值运算符和条件运算符的优先级高。

cout << 42 + 10;	// 正确
cout << (10 < 42);	// 正确
cout << 10 < 42;	// 错误,io运算符运算优先级高于比较运算符,这里试图比较cout和42!

9. sizeof运算符

sizeof运算符繁华一条表达式或一个类型名字所占的字节数,其所得的值是一个size_t类型的常量表达式,运算符的运算对象有两种形式:

sizeof (type),sizeof expr,后一种形式的sizeof返回的是表达式结果类型的大小,sizeof不实际计算其运算对象的值:

Sales_data data, *p;
sizeof (Sales_data);	// 存储Sales_data类型的对象所占的空间大小
sizeof data;				// data的类型大小,即sizeof(Sales_data)
sizeof p;						// 指针所占空间的大小
sizeof *p;				// p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue;	// Sales_data的revenue成员对应类型的大小
sizeof Sales_data::revenue;	// 上一种方式的等效形式

注意到,即使上面的p是一个未初始化的指针,在sizeof的运算对象中解引用一个无效指针仍然是一种安全行为。

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

  • 对char或者类型为char的表达式执行sizeof运算,结果为1;
  • 对引用类型执行sizeof运算得到被引用对象所占空间大小;
  • 对指针指向sizeof运算得到指针本身所占空间的大小;
  • 对解引用执行sizeof运算得到指针指向的对象所占空间大小,指针不需要有效;
  • 对数组执行sizeof运算得到整个数组所占空间大小,等价于对所有元素各执行一次sizeof运算并将结果求和,注意,sizeof运算不会把数组转换成指针来处理;
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不计算对象中的元素占用了多少空间。
// 可以用数组的大小除以单个元素的大小得到数组中元素的个数
constexpr sizt_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz];		// 正确,sizeof返回一个常量表达式

10. 逗号运算符

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。

// 逗号运算符经常被用在for循环中
vector<int>::size_type cnt = ivec.size();
for(vector<int>::size_type ix=0; ix!=ivec.size(); ++ix, --cnt)
  ivec[ix] = cnt;

11. 类型转换

在C++语言中,某些类型之间有关联,如果两种类型有关联,当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换,那么它们就是关联的。

// 将ival初始化为6
int ival = 3.54 + 3;	// 编译器可能会警告该运算损失了精度

算术转换把一种算术类型转换成另一种算术类型,算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换为最宽的类型。

整形提升负责把小整形转换为较大的整数类型。

在所有可能的值都能存在int里的前提下,bool, char, signed char, unsigned char, short, unsigned short会被提升成int类型,否则,提升成unsigned int类型。

较大的char类型(wchar_t, char16_t, char32_t)提升成int, unsigned int, long, unsigned long, long long和unsigned long long中最小的一种。

无符号类型的运算对象转换

首先进行整型提升,如果结果类型匹配,无须进行进一步转换,如果两个(提升后的)运算对象的类型要么带符号,要么不带符号,则小类型的运算对象转换为较大的类型。如果一个运算对象是无符号类型,另一个是带符号的,而且其中无符号类型不小于带符号类型,那么带符号的运算对象转换为无符号的。如果带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换为带符号的,如果不能,则带符号类型的运算对象转换为无符号的。

// 理解算术转换
bool flag;
char cval;
short sval;
unsigned short usval;
int ival;
unsigned int uival;
float fval;
double dval;
3.14159L + 'a';		// 'a'提升成int,然后int转换为long double
dval + ival;	// ival转换为double
dval + fval;	// fval转换为double
ival = dval;	// dval转换为int
flag = dval;	// 如果dval是0,则flag是false,否则flag是true
cval + fval;	// cval提升成int,然后int转换为float
sval + cval;	// sval和cval都提升成int
cval + lval;	// cval转换为long
ival + ulval;		// ival转换为unsigned long
usval + ival;		// 根据unsigned short和int所占空间的大小进行提升
uival + lval;		// 根据unsigned int和long所占空间的大小进行转换

其他隐式转换

数组转换成指针 在大多数用到数组的表达式中,数组自动转换为指向数组首元素的指针:

int ia[10];				// 含有10个整数的数组
int * ip = ia;		// ia转换为指向数组首元素的指针

当数组被用作decltype关键字的参数,或作为取址符,sizeof等运算符的运算对象时,上述转换不会发生。同样,如果用一个引用来初始化数组,上述转换也不会发生。

指针的转换 还有其他几种指针转换方式,包括常量整数值0,字面值nullptr能转换为任意指针类型;指向任意非常量的指针能转换为void;指向任意对象的指针能转换为const void;

转换成布尔类型 如果指针或算术类型的值为0,转换结果是false,否则转换为true。

转换为常量 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是如此。

int i;
const int &j = i;		// 非常量转换为const int的引用
const int *p = &i;	// 非常量的地址转换为const的地址
int &r = j, *q = p;	// 错误,不允许const转换为非常量

但是相反的转换不存在,因为它试图删除掉底层const。

类类型定义的转换 类类型能定义有编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。

之前的程序已经使用过类类型转换:

string s, t = "a value";		// 字符串字面值转换为string
while (cin >> s)				// while的条件部分把cin转换为布尔值
posted @ 2021-01-30 12:15  geeks_reign  阅读(152)  评论(0编辑  收藏  举报