C++ Primer记录_第四章

本文详细讲解了C++中的表达式结构,包括基础概念、运算符优先级与结合律、求值顺序,以及算术、逻辑、赋值、位运算符等的用法。重点介绍了类型转换,从隐式到显式,以及各类转换的场景和注意事项。最后提供了运算符优先级表,帮助读者理解和运用这些概念。
摘要由CSDN通过智能技术生成

第四章 表达式

4.1 基础

  • 表达式由一个或多个运算对象组成,对表达式求值将得到一个结果
  • 字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
  • 把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。

4.1.1 基础概念

  • 一元运算符作用于一个运算对象。如取地址&、解引用*
  • 二元运算符作用于二个运算对象。如相等运算符==、乘法运算符*
  • 三元运算符作用于三个运算对象。如条件表达式?表达式1:表达式2
  • 有些符号既能作为一元运算符也可以作为二元运算符,取决于上下文,用法互不相干。
  • 对于复杂表达式要理解优先级结合律求值顺序
  • 在表达式求值中,运算对象常常由一种类型转换成另外一种类型。
  • 内置和复合类型的运算对象的运算符操作已被定义,自定义类型用户可以自行定义操作与含义,被称为重载运算符
  • 左值与右值:
    1. 左值可以位于赋值语句的左侧,右值则不能。
    2. 当一个对象被作为右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

4.1.2 优先级与结合律

  • 复合表达式是指含有两个或多个运算符的表达式。
  • 根据运算符的优先级,表达式3+4*5的值是23,不是35。
  • 更具运算符的结合律,表达式20-15-3的值2,不是8。
//这条表达式中的括号符合默认的优先级和结合律
((6 + ((3 * 4) / 2)) + 2)
  • 括号会无视优先级和结合律。
#include <iostream>
using namespace std;
int main()
{
    //不同括号组合导致不同的组合结果
    cout << (6 + 3) * (4 / 2 + 2) << endl;
    cout << ((6 + 3) * 4) / 2 + 2 << endl;
    cout << 6 + 3 * 4 / (2 + 2) << endl;
}
36
20
9
  • 优先级与结合律有何影响
int ia[] = {0, 2, 4, 6, 8}; //含有5个整数的数组
int last = *(ia + 4);       //把last初始化成8也就是ia[4]的值
last = *ia + 4;             //last=4,等价于ia[O] + 4

4.1.3 求值顺序

  • 优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。
  • 编译器可能先求i的值再求++i的值,此时输出1 1;也可能先求i值再求++i的值,输出结果是0 1;甚至编译器还可能做出完全不同的操作。
int i = O;
cout << i << " " << ++i << endl; //未定义的
  • 运算对象的求值顺序与优先级和结合律无关。

4.2 算术逻辑符

  • 算术运算符(左结合律)。
  • 按照运算符的优先级将其分组。一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。
运算符功能用法
+一元正号+expr
-一元负号-expr
-------------------------
*乘法expr * expr
/除法expr / expr
%求余expr % expr
-------------------------
+加法expr + expr
-减法expr - expr
  • 算术表达式有可能产生未定义的结果:数学性质本身(除数为0)、计算机的特点(溢出)。
int ival1 = 21 / 6; // ival1是3,结果进行了删节,余数被抛弃掉了
int ival2 = 21 / 7; // ival2是3,没有余数,结果是整数值
int ival = 42;
double dval = 3.14;
ival % 12;   //正确:结果是6
ival % dval; //错误:运算对象是浮点类型
  • 根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等。
21 % 6;    // 3
21 % 7;    // 0
- 21 % -8; //-5
21 % -5;   // 1
21 / 6;    // 3
21 / 7;    // 2
- 21 / -8; // 2
21 / -5;   //-4

4.3 逻辑和关系运算符

  • 关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。
  • 返回值都是布尔类型。
结合律运算符功能用法
逻辑非!expr
----------------------------------
<小于expr < expr
<=小于等于expr <= expr
>大于expr > expr
>=大于等于expr >= expr
----------------------------------
==相等expr == expr
!=不相等expr != expr
----------------------------------
&&逻辑与expr && expr
----------------------------------
||逻辑或expr || expr
  • 逻辑与或当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值
  • 逻辑非运算符将运算对象的值取反后返回。
  • 关系运算符比较运算对象的大小关系并返回布尔值。
//哎哟!这个条件居然拿i<j的布尔值结果和K比较!
if (i < j < k) //若k大于1则为真!
//正确:当i小于j并且j小于k时条件为真
if (i < j && j < k) (/* ... */) 
  • 进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔宇面值true和false作为运算对象。

4.4 赋值运算符

  • 赋值运算符的左侧运算对象必须是一个可修改的左值。
int i = 0, j = 0, k = 0;             //初始化而非赋值
const int ci = i;                    //初始化而非赋值
1024 = k;                            //错误:宇面值是右值
i + j = k;                           //错误:算术表达式是右值
ci = k;                              //错误: ci是常量(不可修改的)左值
k = 0;                               //结果:类型是int, 值是0
k = 3.14159;                         //结果:类型是int, 值是3
k = {3.14};                          //错误:窄化转换,使用初始化列表让转换更严格,不允许损失精度
vector<int> vi;                      //初始为空
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // vi现在含有10个元素了,值从0到9
  • 赋值运算符满足右结合律。
int ival, jval;
ival = jval = 0; //正确:都被赋值为0
int ival, *pval; // ival的类型是int; pval是指向int的指针
ival = pval = 0; //错误:不能把指针的值赋给int
string sl, s2;
sl = s2 = "OK"; //字符串宇面值"OK"转换成string对象
  • 赋值运算优先级较低。
  • 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
//这是一种形式繁琐、容易出错的写法
int i = get_value(); //得到第一个值
while (i != 42)
{
    //其他处理....
    i = get_value(); //得到剩下的值
}
int i;
//更好的写法:条件部分表达得更加清晰
while ((i = get_value) != 42)
{
    //其他处理
}
* 切勿混淆相等运算符和赋值运算符。如`if (i = j)`、`if (i == j)`。
* 复合赋值运算符,`a op= b`等价于`a = a op b` 
int sum = 0;
//计算从1到10(包含10在内)的和
for (int val = 1; val <= 10; ++val)
{
    sum += val; //等价于sum=sum+val
}

4.5 递增和递减运算符

  • 递增运算符(++)和递减运算符(–)为对象的加1和减l操作提供了一种简洁的书写形式。
  • 递增和递减运算符有两种形式:前置版本和后置版本。
  • 这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
int i = 0, j;
j = ++i; // j=1,i=1:前置版本得到递增之后的值
j = i++; // j=1,i=2:后置版本得到递增之前的值
  • 在一条语句中混用解引用和递增运算符。
auto pbeg = v.begin();
//输出元素直至遇到笫一个负值为止
while (pbeg != v.end() && *beg >= 0)
{
    cout << *pbeg++ << endl; //输出当前值并将pbeg向前移动一个元素
}
  • 运算对象可按任意顺序求值。
  • 大多数运算符没有规定运算对象的求值顺序,一般情况下没有问题,但是如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序很关键了。
*beg = toupper(*beg++); //错误,该赋值语句未定义

4.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()
*p.size();          //错误:p是一个指针,它没有名为size的成员

4.7 条件运算符

  • 条件运算符cond? exprl : expr2;
  • cond是判断条件的表达式,exprlexpr2是两个类型相同或可能转换为某个公共 类型的表达式。
  • 首先求cond的值,如果条件为真对exprl求值并返回该值,否则对expr2求值并返回该值。
  • 条件运算符满足右结合律,意味着运算对象按照从右向左的顺序组合。
//条件部分判断成绩是否小于60,如果小于,表达式的结果是"fail",否则结果是"pass"。
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
cout << grade < 60 ? "fail" : "pass";     //错误:试图比较cout和60

4.8 位运算符

  • 位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
运算符功能用法
~位求反~expr
--------------------------
<<左移expr1 << expr2
>>右移expr1 >> expr2
--------------------------
&位与expr & expr
--------------------------
^位异或expr ^ expr
--------------------------
|位或expr
  • 移位运算符
// 0233是八进制的宇面值
unsigned char bits = 0233;
bits << 8;  //bits提升成int类型,然后向左移动8位
bits << 31; //向左移动31位,左边超出边界的位丢弃掉了
bits >> 3;  //向右移动3位,最右边的3位丢弃掉了
  • 位求反运算符(~)将运算对象逐位求反后生成一个新值,将1置为0、 将0置为1。
  • 与(&)、或(|)、异或(^)运算符在两个运算对象上逐位执行相应的逻辑操作。

4.9 sizeof运算符

  • sizeof运算符返回一条表达式或一个类型名字所占的字节数,满足右结合律。
  • 两种形式
    1. sizeof (type) 类型名字所占的大小。
    2. sizeof expr 返回的是表达式结果类型的大小。
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大小的方式
  • 对char或者类型为char的表达式执行sizeof运算,结果得1。
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

4.10 逗号运算符

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

4.11 类型转换

  • 如果两种类型可以相互转换,那么它们就是关联的。
  • 自动执行的,无须程序员的介入被称作隐式转换
int ival = 3.541 + 3; //编译器可能会警告该运算损失了粘度
  • 何时发生隐式类型转换?
    1. 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
    2. 在条件中,非布尔值转换成布尔类型。
    3. 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
    4. 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一类型。
    5. 函数调用时也会发生类型转换。

4.11.1 算术转换

  • 算术转换的含义是把一种算术类型转换成另外一种算术类型。
  • 整型提升负责把小整数类型转换成较大的整数类型。
  • 无符号类型的运算对象较复杂,要想理解算术转换就是研究大量的例子。
bool flag;
short sval;
int ival;
long ival;
float fval;
char cval;
unsigned short usval;
unsigned int uival;
unsigned long ulval;
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所占空间的大小进行转换

4.11.2 其他隐式类型转换

  • 数组转换成指针。
int ia[10];   //含有10个整数的数组
int *ip = ia; // ia转换成指向数组首元素的指针
  • 包括常量整数值0或者字面值nullptr能转换成任意指针类型。
  • 指向任意非常量的指针能转换成void*。
  • 指向任意对象的指针能转换成const void*。
  • 存在一种从算术或指针类型向布尔类型的自动转换机制。
char *cp = get_string();
if (cp) //如果指针cp不是0,条件为真
{
    /* code */
}
while (*cp) //如果*cp不是空字符,条件为真
{
    /* code */
}
  • 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是。
int i;
const int &j = i;   //非常量转换成const int的引用
const int *p = &i;  //非常量的地址转换成const的地址
int &r = j, *q = p; //错误:不允许const转换成非常量
  • 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。

4.11.3 显示转换

  • 显式地将对象强制转换成另外一种类型称作强制类型转换
  • 一个命名的强制类型转换具有如cast-name<type>(expression)
  • type是转换的目标类型、expression是要转换的值、type是引用类型。
  • static_cast:任何具有明确定义的类型转换,只要不包含底层const。
//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;
void *p = &d; //正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型
double *dp = static_cast<double*>(p);
  • const_cast只能改变运算对象的底层const。
const char *pc;
char *p = const_cast<char *>(pc); //正确:但是通过p写值是未定义的行为
const char *cp;
char *q = static_cast<char *>(cp); //错误:static_cast不能转换掉const性质
static_cast<string>(cp);           //正确:字符串字面值转换成string类型
const_cast<string>(cp);            //错误:const_cast只改变常量属性
  • reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
  • 使用reinterpret_cast是非常危险的,其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。
int *ip;
char *pc = reinterpret_cast<char *>(ip);
  • 尽量避免强制类型转换。

4.12 运算符优先级表

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Flame老唐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值