表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。
字面值和变量是最简单的表达式(expression)。
其结果(result)就是字面值和变量的值。
把一个运算符(operator)和一个或多个运算对象(operand)组合起来可以生成较为复杂的表达式(expression)。
重载运算符
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。
作用于类类型的运算对象时,用户可以自行定义其含义。自定义的过程实际上是为已经存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded)。
优先级和结合律
复合表达式(compound expression)是指含有两个或多个运算符的表达式。
求复合表达式的值需要首先将运算对象和运算符合理地组合在一起,优先级和结合律决定了运算对象组合的方式。
求值顺序
优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。
int i = f1() * f2();
我们知道f1和f2一定会在执行之前被调用,但无法得知谁先被调用。
int i = 0;
cout << i << "" << ++i <<endl;
对于那些没有执行顺序的运算符来说,如果表达式指向并修改了一个对象,将会引发错误并产生未定义的风险。
如上程序中,<<运算符没有明确规定何时以及如何对运算对象求值,输出结果不可预知。
注:有4种运算符明确规定了运算对象的求值顺序。
逻辑与(&&)、逻辑或(||)、条件(?:)、逗号(,)
建议:处理复合表达式
1.拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
2.如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
算数运算符
取模运算符(%)负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型。
int ival = 12;
double dval = 3.14;
ival % 12; //正确
ival % dval; //错误
在除法运算中,如果两个运算对象的符号相同则商为正,否则商为负。
C++11新标准规定商一律向0取整(即直接切除小数部分)
21 / 6; //3
21 / -7; //3
-21 / -8; //2
21 / -5; //-4
在取余运算中,如果m%n不等于0, 则它的符号和m相同。
21 % 6; //3
21 % 7; //0
-21 % -8; //-5
21 % -5; //1
逻辑和关系运算符
短路求值(short-circuit evaluation):逻辑与(&&)逻辑或(||)都是先求左值运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。
//对vector<string>对象,输出string对象的内容并且在遇到空字符串或者以句号结束的字符串是进行换行
for (const auto &s : text)
{
cout << s;
if(s.empty() || s[s.size() - 1] == '.')
cout << endl;
else
cout << "";
}
tip: s被声明成了对常量的引用。因为text的元素是string对象,可能非常的大,所以将s声明成引用类型可以避免对元素的拷贝;又因为不需要对string对象做读写操作,所以s被声明对常量的引用。
关系运算符【都满足左结合律】:关系运算符比较对象的大小关系并返回布尔值。
进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
赋值运算符
C++11新标准允许运用花括号({})括起来的初始化列表作为赋值语句的右侧运算对象。
内置类型:初始化列表最多只能包括一个值。
类类型:赋值运算细节由类本身决定。对于vector来说,vector模板重载了赋值运算符并且可以接受初始化列表。
int i;
while ((i = get_value()) != 42)
{
//……
}
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
递增递减运算符
递增运算符(++)和递减运算符(--):①提供一种简洁的书写形式
②很多迭代器不支持算术操作,用于操作迭代器
建议:除非必须,否则不用递增递减运算符的后置版本
原因:前置版本避免了不必要的操作
前置(++i):把值加1后直接返回改变的运算对象
后置(i++):将原始值存储下来以便返回这个未修改的内容副本。
如果我们不需要修改前的值(比如:for循环),那么后置版本就是一种浪费。
代码解析
auto pbeg = v.begin();
while (pbeg != v.end() && *pbeg >= 0)
cout << *pbeg++ << endl;
语句【*pbeg++】中混用了【*】和【后置++】不易理解。
【后置++】优先级高于【*】,因此等价于【*(pbeg++)】。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为其求值的结果,解引用的对象是pbeg未增加之前的值。
cout << *item << endl;
++item;
语句输出pbeg开始时指向的那个元素 并将指针向前移动一个位置。
运算对象可以任意顺序求值
大多数运算符都没有规定运算对象的求值顺序,一般情况下不会有什么影响。
如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值,运算对象的求值顺序就很关键了。
while(beg != s.end() && !isspace(*beg) )
*beg = toupper(*beg++);
上式循环将产生未定义;
*beg = toupper(*beg);
*(beg + 1) = toupper(*beg);
条件运算符(?:)
条件运算符(?:)允许我们把简单的if-else逻辑嵌入到单个表达式中。
string finalgrade = (grade < 60) ? "fail" : "pass";
允许在条件运算符的内部嵌套另一个条件运算符。
string finalgrade = (grade < 60) ? "fail" : (grade < 90 ) ? "good" : "great";
条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。
注:①随着条件运算嵌套层数的增加,代码的可读性急剧下降。因此,条件运算嵌套最好别超过两至三层;
②条件运算符的优先级非常低,当一条长表达式中嵌套了条件运算子表达式时,通常需要在它的两端加上括号。
sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。
满足右结合律
不计算其运算对象的值【可以对无效指针进行解引用】
返回一个size_t类型的值
sizeof (type); //无需一个具体的对象
sizeof expr;
sizeof运算符的结果部分的依赖其作用的类型:
①char或者char的表达式:结果为1;
②引用类型:被引用对象空间
③指针: 指针本身大小空间
④解引用指针:指针指向对象空间大小
⑤数组:整个数组所占空间大小
⑥对string、vector对象:返回该类型固定部分的大小,不会计算对象中元素所占空间
逗号运算符
从本质上讲,逗号的作用是导致一系列运算被顺序执行。
int j = 10;
int i = (j++, j+100, 999+j); //1010
最右边的那个表达式的值将作为整个逗号表达式的值,其他表达式的值会被丢弃。
类型转换
何时发生隐式转换【自动执行类型转换,无需程序员介入(了解)】
①在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
②在条件中,非布尔值转换成布尔值
③初始化时,初始值转换成变量类型
④赋值语句,右侧运算对象转换成左侧运算对象
⑤算术运算或关系运算的运算对象有多种类型,需要准换成同一种类型
⑥函数调用时发生类型转换【第六章】
算术转换
如果某个运算对象类型不一致,这些运算对象将转换成同一种类型。
①:整型提升
②:类型同带符号/同无符号:小类型转换成大类型
无符号+带符号:Ⅰ无符号>=带符号——>无符号【负值可能出现错误】
Ⅱ无符号<带符号———>1)无符号都能存在带符号中——>带符号
2)否则————>无符号
其他隐式类型转换
数组转换成指针: 在大多数用到数组的表达式中,数组自动转换成指向数组首地址的指针;
当数组被作为decltype关键字的参数,或者作为取地址符(&)、sizeof以及typeid(19章)等运算符的运算对象时,不会发生转化。
指针转换:常量整型数0或者字面值nullptr能转换成任意类型指针;
指向非常量指针能转换成void*
任意指针能转换成const void*
类类型定义的转换: 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换
string s;
while(cin >> s)
IO库定义了从istream向布尔值转换的规则。
显式转换
(有的时候不得不使用强制类型转换,但这种方法是十分危险的)
cast-name<type>(expression)
cast-name 包括:static_cast, dynamic_cast, const_cast, reinterpret_cast
static_cast
任何具有明确定义的类型转化,只要不包含底层const,都可以使用static_cast.
double slope = static_cast<double> (j)/i; //
将较大类型赋值给较小类型非常有用,如此就关闭了编译器发出的警告(精度损失),告诉编译器我知道并且不在乎损失
void *v = &d;
double *dp = static_cast<double*> (p);
对于编译器无法自动执行的类型转化也非常有用。使用static_cast找回存在于void*中的值。
const_cast
const_cast只能【双向】改变运算对象的底层const
const char *pc;
char *p = const_cast<char*>(pc); //正确 但通过p写值未定义
注:①只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量都将引发编译器错误【表达从const转非const,或反过来,只能用const_cast】
②不能用const_cast改变表达式的类型【用const_cast时,要转的类型跟被转的类型,要一致,只是有没有const修饰符的区别】
术语表:
整型提升:把一种较小的整数类型转换成与之最接近的较大整形类型的过程。不论是否真的需要,小整数类型(short、char等)总会得到提升