● 表达式由一个或多个运算对象(操作数)组成, 对表达式求值将得到一个结果。 字面值(常量)和变量是最简单的表达式, 其结果就是字面值和变量的值。 把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。
● 函数调用也是一种特殊的运算符, 它对运算对象的数量没有限制。
● 注意: 指针不能转换成浮点数。
● 在表达式求值的过程中, 如果两个运算对象的类型不同也没有关系, 只要它们能被转换成同一种类型即可。
● 小整数类型(如 bool、char、short) 等通常会被提升成较大的整数类型,主要是int。
左值和右值
● C++ 的表达式要不然是右值, 要不然就是左值。
● 一个左值表达式的求值结果是一个对象或者一个函数, 然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。
● 注意: 当一个对象被用作右值的时候, 用的是对象的值(内容); 当对象被用作左值的时候, 用的是对象的身份(在内存中的位置)。
● 注意 : 在需要右值的地方可以用左值来代替, 但是不能把右值当成左值(也就是位置)使用。 当一个左值被当作右值使用时, 实际使用的是它的内容(值)。
● 以下几种运算符要用到左值:
(1) 赋值运算符需要一个(非常量)左值作为其左值运算对象, 得到的结果也仍然是一个左值。
(2) 取地址符作用于一个左值运算对象, 返回一个指向该运算符对象的指针, 这个指针是一个右值。
(3) 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
(4) 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
● 求复合表达式的值需要首先将运算符和运算对象合理地组合在一起, 优先级与结合律决定了运算对象组合的方式。
● IO(输入输出运算符) 满足左结合律。
求值顺序
● 对于那些没有指定执行顺序的运算符来说,**如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。比如: << 运算符并没有明确规定何时以及如何对运算对象求值, 因此下面的表达式是未定义的: **
int i=0;
cout<<i<<" "<<++i<<endl; // 未定义
● 有四种运算符明确规定了运算对象的求值顺序: 逻辑与运算符、逻辑或、条件运算符和逗号运算符。
● 注意: 运算对象的求值顺序与优先级和结合律无关。
算术运算符
● 除非另做特殊说明,算术运算符都能作用于任意算术类型、 以及任意能转换为算术类型的类型。 算术运算符的运算对象的求值结果都是右值。 在表达式求值之前, 小整数类型的运算对象被提升成较大的整数类型(主要是int), 所有运算对象最终会转换成同一类型
● 注意 : 当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本
● 注意 : 一元负号运算符对运算对象值取负后,返回其(提升后的)副本。
● 注意: 布尔值不应该参与运算, 对于大多数运算符来说, 布尔类型的运算对象将被提升为int类型。
● 注意 : % 负责计算两个整数相除所得的余数, 参与取余运算的运算对象必须是整数类型。 在除法运算中,如果两个运算对象的符号相同则商为正(如果不为0的话), 否则商为负。结果为负值的商C++新标准则规定商一律向0 取整(即直接切除小数部分)。
逻辑和关系运算符
● 关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。这两类运算符的运算对象的求值结果都是右值
● 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值、再求右侧运算对象的值, 当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。 这种策略称为短路求值
● 逻辑与(&&) 运算符,它们的左侧运算对象是为了确保右侧运算对象求值过程的正确性和安全性
关系运算符
● 关系运算符比较运算对象的大小关系并返回布尔值。关系运算符都满足左结合率
● 如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件
if(val == true ) // 只有当val 等于1 时条件才为真!
{
}
如果val 不是布尔值,这样比较就失去了原来的意义, 如果val 不是布尔值, 那么进行比较之前会首先把true 转换成 val 的类型。
● 注意 : 在进行比较运算时除非比较的对象是布尔类型, 否则不要使用布尔字面值true和false 作为运算对象。
赋值运算符
● 赋值运算的结果是它的左侧运算对象,并且是一个左值。 相应的, 结果的类型就是左侧运算对象的类型。
如果赋值运算符的左右两个运算对象类型不同, 则右侧运算对象将转换成左侧运算对象的类型
● 如果左侧运算对象是内置类型, 那么初始值列表最多只能包含一个值, 而且该值即使转换的话其所占空间也不应该大于目标类型的内存空间。
● 对于类类型来说, 赋值运算的细节由类本身决定。 对于vector来说, vector模板重载了赋值运算符并且可以接收初始值列表, 当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。
● 无论左侧运算对象的类型是什么, 初始值列表都可以为空。 此时, 编译器创建一个值初始化的临时量并将其赋给左侧运算对象。
● 赋值运算符满足右结合律。
● 因为赋值运算符的优先级低于关系运算符的优先级, 所以在条件语句中, 赋值部分通常应该加上括号。
递增递减运算符
● 这两种运算符必须作用于左值运算对象。 前置版本将对象本身作为左值返回, 后置版本则将对象原始值的副本作为右值返回。
● 如果我们想在一条复合表达式中即将变量加1或减1又能使用它原来的值, 这时就可以使用递增和递减运算符的后置版本
● 箭头运算符作用于一个指针类型的运算对象, 结果是一个左值。 点运算符分成两种情况: 如果成员所属的对象是左值,那么结果是左值; 反之, 如果成员所属的对象是右值, 那么结果就是右值
条件运算符
● 当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时, 运算的结果是左值; 否则运算的结果是右值。
● 条件运算符满足右结合律, 意味着运算对象(一般)按照从右向左的顺序组合。
● 注意: 随着条件运算嵌套层数的增加,代码的可读性急剧下降。 因此, 条件运算的嵌套最好别超过两到三层。
● 条件运算符的优先级非常低, 因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。比如输出语句的时候。
sizeof 运算符
● sizeof 运算符返回一条表达式或者一个类型名字所占的字节数。sizeof 运算符满足右结合律, 其所得的值是一个 size_t 类型的常量表达式。 运算符的运算对象有两种形式:
sizeof (type)
sizeof expr
说明: 在第二种形式中, sizeof 返回的是表达式结果类型的大小。与众不同的是, sizeof 并不实际计算其运算对象的值。
注意: 在sizeof的运算对象中解引用一个无效指针(比如:未初始化)仍然是一种安全的行为,因为指针实际上并没有被真正使用。 sizeof不需要真的解引用指针也能知道它所指对象的类型。
● C++11 新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员, 但是 sizeof 运算符无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。
● sizeof 运算符的结果部分地依赖于其作用的类型:
(1) 对char 或者类型 为 char 的表达式执行 sizeof 运算, 结果得1。
(2) 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
(3) 对指针执行sizeof运算得到指针本身所占空间的大小。
(4) 对解引用指针执行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; // 另一个获取revenue大小的方式
(5) 对数组执行sizeof 运算得到整个数组所占空间的大小, 等介于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。 注意: sizeof运算不会把数组转换成指针来处理
(6) 对string 对象 或 vector对象 执行 sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
(7) 因为执行sizeof运算能得到整个数组的大小, 所以可以用数组的大小除以单个元素的大小得到数组中的元素的个数。
注意 :因为sizeof 的返回值是一个常量表达式, 所以我们可以用sizeof的结果声明数组的维度。
int ia[10] = { 1,2,3,4,5,6,7,8,9 };
constexpr size_t sz = sizeof(ia) / sizeof(*ia); // 返回ia的元素个数
int arr2[sz]; // sizeof 返回一个常量表达式
逗号运算符
● 逗号运算符含有两个运算对象, 按照从左向右的顺序依次求值。 那么逗号运算符也规定了运算对象求值顺序。
● 那么对于逗号运算符来说, 首先对左侧的表达式求值, 然后将求值结果丢弃掉。 逗号运算符真正的结果是右侧表达式的值。 如果右侧运算对象是左值, 那么最终的求值结果也是左值,
● 逗号运算符经常被用在for 循环当中。
类型转换
● 如果两种类型可以相互转换, 那么它们就是关联的。
● int ival=3.541+3; // 编译器可能会警告该运算损失了精度
说明 : C++ 语言不会直接将两个不同类型的值相加, 而是先根据类型转换规则设法将运算对象的类型统一后再求值。 上述的类型转换是自动执行的, 称为 隐式转换。
算术类型之间的隐式转换被设计得尽可能避免损失精度。 很多时候, 如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象, 整型会转换成浮点型。 在上面的列子中, 3转换成double类型,然后执行浮点数加法, 所得结果的类型是double
接下来就要完成初始化的任务了。 在初始化过程中, 因为被初始化的对象的类型无法改变, 所以初始值被转换成该对象的类型。 上述的例子, 加法运算得到double类型的结果转换成int类型的值, 这个值被用来初始化 ival。 由double 向int 转换时忽略掉了小数部分, 在上面的表达式中, 数值6被赋值给了 ival。
何时发生隐式类型转换
● 在下面的这些情况下, 编译器会自动地转换运算对象的类型:
(1) 在大多数表达式中, 比int类型小的整型值首先提升为较大的整数类型。
(2) 在条件中, 非布尔值转换成布尔类型
(3) 初始化过程中, 初始值转换成变量的类型; 在赋值语句中, 右侧运算对象转换成左侧运算对象的类型。
(4) 如果算术运算或关系运算的运算对象有多种类型, 需要转换成同一类型。
算术转换
● 算术转换的含义是把一种算术类型转换成另外一种算术类型, 算术转换的规则定义了一套类型转换的层次, 其中运算符的运算对象将转换成最宽的类型。 例如: 如果一个运算对象的类型是 long double , 那么不论另外一个运算对象的类型是什么都会转换成 long double。
整型提升
● 整型提升负责吧小整数类型转换成较大的整数类型。 对于bool、 char、 signed char、 unsigned char、short、和 unsigned short
等类型来说, 只要它们所有可能的值都能存在int里, 它们就会提升为int
类型。 否则 , 提升成 unsigned int
类型
较大的 char
类型 (wchar_t、char16_t char32_t
) 提升成 int、unsigned int、long、unsigned long、 long long 和 unsigned long long 中 最小的一种类型, 前提是转换后的类型要能容纳原类型所有可能的值
无符号类型的运算对象
● 注意 : 如果某个运算符的运算对象类型不一致, 这些运算对象将转换成同一种类型。 但是如果某个运算对象的类型是无符号类型, 那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
● 注意 : 像往常一样, 首先执行整型提升。 如果结果的类型匹配, 无须进行进一步的转换。 如果两个(提升后) 运算对象的类型要么都是带符号的、要么都是无符号的, 则小类型的运算对象转换成较大的类型。
● 注意 : 如果一个运算对象是无符号类型、 另外一个运算对象是带符号类型, 而且其中的无符号类型不小于带符号类型, 那么带符号的运算对象转换成无符号类型。
unsigned int a = 10;
int c = -11;
cout << (a + c) << endl; //值为4294967295
● 剩下的一种情况是带符号类型大于无符号类型, 此时转换的结果依赖于机器。 如果无符号类型的所有值都能存在该带符号类型中, 则无符号类型的运算对象转换成带符号类型。
如果不能, 那么带符号类型的运算对象 转换成无符号类型
其他隐式类型转换
● 数组转换成指针: 在大多数用到数组的表达式中, 数组自动转换成指向数组首元素的指针。
注意 : 当数组被用作decltype
关键字的参数,或者作为取地址符(&)、sizeof 及 typeid 等运算符的运算对象时, 上述转换不会发生。 同样的, 如果用一个引用来初始化数组, 上述转换也不会发生。 当在表达式中使用函数类型时会发生类型的指针转换。
指针的转换
● C++ 还规定了几种其他的指针转换方式, 包括常量整数值0或者字面值nullptr 能转换成任意指针类型;*指向任意非常量的指针能转换成 void ** 指向任意对象的指针能转换成 const void*
。
● 转换成布尔类型: 如果指针或算术类型的值为0,转换结果是false, 否则转换结果是true。
● 转换成常量: 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。 也就是说, 如果T 是一种类型, 我们就能将指向T的指针或引用分别转换成指向 const T 的指针或引用。
类类型定义的转换: 类类型能定义由编译器自动执行的转换, 不过编译器每次只能执行一种类类型的转换例如:
string s,t="a value"; //字符串字面值转换成string类型
while(cin>>S); // while 的条件部分把cin 转换成布尔值
条件(cin>>S)
读入cin的内容并将cin 作为其求值结果。 条件部分本来需要一个布尔类型的值, 但是这里实际检查的是istream
类型的值。cin 自动地转换成布尔值。 所得的布尔值到底是什么由输入流的状态决定, 如果最后一次读入成功, 转换得到的布尔值是true, 相反, 如果最后一次读入不成功, 转换得到的布尔值是false
显式转换
● 语法为:
cast_name (expression)
type: 是要转换的目标类型, expression: 是要转换的值。
如果type 是引用类型,则结果是左值。
cast_name 是: static_cast dynamic_cast const_cast 和 reinterpret_cast 中的一种。 dynamic_cast 支持运行时类型识别。 cast_name 指的是 执行的是哪种转换。
static_cast
● 任何具有明确定义的类型转换,只要不包含底层const(指的是指针所指的对象是一个常量),都可以使用 static_cast.
● 注意: 当需要把一个较大的算术类型赋值给较小的类型时, static_cast 非常有用。
const_cast
● const_cast 只能改变运算对象的底层const:
const char *pc;
char p=const_cast<char>(pc); 正确,但是通过p写值是未定义的行为,因为指针pc没有初始化,是未定义的值
上述的对于将常量对象转换成非常量对象,称其为 “去掉 const 性质”。 一旦我们去掉了某个对象的const 性质,该对象就是个变量了,不再是常量, 上面的指针pc 指向的对象就是可以变的。我们可以为 *pc 这样 赋值, 来改变指针指向对象的值。