C++表达式知识概要总结

这一章好多细节好多好多好多啊
然后就看的就很慢


我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的,但是运算对象的个数、运算符的优先级和结合律都是无法改变的。

关于左值和右值:

在c语言中的经典分辨方法:
左值是一个可以出现在赋值的左侧或右侧的表达式,而右值是一个只能出现在赋值的右侧的表达式。
在c++中也可以这么分辨。当然还有更细节的区别。

关于复合表达式

优先级相同时。按照从左往右的顺序组合运算对象。
括号无视优先级和结合律,这是跟普通数字一毛一样的

注意注意注意!!

对于一条形如f()+g()*h()+j()的表达式中;
有一个很大的问题会存在:虽然f、g、h、j这四个的返回值的运算已经由结合律和优先级确定了,但对于这四个函数谁该先返回值却没有确定,所以当这四个函数中若存在公共的变量,那将是一个错误的表达式,将产生未定义的行为。

在表达式求值之前,小整数类型的运算对象会被提升成较大的整数类型,所有运算对象最终会转换成同一类型。

关于一元运算的’-’,这个的话经常用,但是有一个很特殊的,就是对布尔型用的时候。

bool a = true;
bool b = -a;//注意这里的b返回的还是true,而不是false;

前面可能提到了布尔值不应该参与运算,但是我没注意(lll¬ω¬)
对大多数运算符来说,布尔类型的运算对象将会被提升为int类型。在上面的例子中,b会变成-1,而-1并不是0,所以返回的还是true。

关于取余运算符

众所周知,两个整数才能取余;

C++语言的早期版本允许结果为负值的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接切除小数部分)。

扫描二维码关注公众号,回复: 10301216 查看本文章

又是众所周知:表达式(m/n)*n+m%n的求值就是m,因此,m%n的值的符号只与m有关。不然会很麻烦。
我是这么理解的:对于式子左边的式子(m/n)*n,这个由于出现了两个n,所以这个式子的正负将只有m决定,n是负的那两个n则是正的,反之更显然,所以后面的式子只能与m有关,不然就要分类讨论了。

关于逻辑与关系符运算

对于逻辑与或运算符,编译器会采取短路求值,即左边的能确定值则不会继续进行右边的,
根据短路求值的这种策略,左侧运算的对象可以专门为了确保右侧运算对象求值过程的正确性和安全性而存在。

回到范围for语句来说,

for(auto &c : text)

对于这里的引用,不得不再提一嘴,前面说了必须用引用的一些情况:

  • 多维数组循环体的前几层都需要用引用。
  • 需要改变text中的值的时候,必须要用引用。

上述的是必须使用引用的情况,但还有些情况用引用是最好的选择,比如当text是存放着string的vector时,并且存放的string都很大,这时若不用引用的话就需要做大量的拷贝工作,而用引用的话则不需要,直接绑定上就好了。岂不香哉。

关于关系运算符

学到这,我终于算是解决了为什么刚开始接触c时写这样的语句是不对的了:

if(a < b < c)

当时就背下来这种是错误的,其实完全不算错的,编译可以通过,我想要的其实是连小于的效果,但这种并不是连小于的效果,只能属于是自己的逻辑错误。
这个语句是先比较a和b,比价完了之后,把比较得出的布尔值在和c比较。。。。

在比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
为什么?
比如你这么写:

if(val == true) // 只有当val等于1时,条件才为1;

上面提到了,true这个值在这个式子中会变成1,因此这个if的意义就是拿val和1进行比较,而不是判断val是否为false,即是否为0了,意思就完全不一样了。

关系运算符的优先级比较陌生,所以我要把这个搬上来,需要的时候尽管查

在这里插入图片描述
小练习 :

if(a != b<c) //***

关于赋值运算

赋值满足右结合律

int i;
double d;
i = d = 3.5;
d = i = 3.5;

观察上面代码右结合律就很明显了。
如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象类型;

还需要注意的是,赋值运算符的优先级很低,比逻辑运算和关系运算符低。
因此很多情况都需要对赋值运算加上括号。

关于复合操作,

a += b;
a = a + b;

这俩式子一毛一样,唯一的区别就是左侧运算对象的求值次数,复合操作只求一次,正常操作需要多求一次赋值运算,对性能稍微有那么一点影响,几乎可以忽略不计,知道即可。

关于递增递减的更详细

前面讲到过,vector和string的迭代器是可以进行各种运算的,但是有很多迭代器是不支持算术运算的,只支持递增(++)和递减(–)操作。

i++; //得到递增之后的值
++i;//得到递增之前的值

除了返回的值不同之外,这俩其实还有一个区别

  • 前置版本把值加1后直接返回改变了的运算对象。
  • 后置版本需要将原始值储存下来以便于返回未修改的内容

所以后置版本就多了储存原始值的操作,如果不需要什么特殊的功能,以后一律就写前置版本吧,这样少了额外的消耗。

注意 后置递增运算符的优先级高于解引用运算符
*it++
对于 前置递增运算符也是一样:*++it; ++*it;

注意注意
如果一条式子改变了某个运算对象的值,另一条表达式又要使用该值的话,运算对象的求值顺序就很关键了,而且编译器会用任意的思路处理他们,所以一定要避免这样的式子,就是这样的:

*beg = toupper(*beg++);
关于成员访问运算符

ptr->mem就相当于(*ptr).mem;
写个例子

string s1 = "string", *p=&s1;
auto n = s1.size();
n = (*p).size();
n = p->size();

同样的点运算符的优先据也高于解引用运算符的优先级
所以不能这么写

n=*p.size();
关于条件运算符

值得注意的是:条件运算符的优先级非常之低。举个例子一目了然

cout << ((grade < 60) ? "fail" : "pass");

//正确。输入想要的

cout << (grade < 60) ? "fail" : "pass";

// 不=正确,因为会输出1或0,即只输出(grade<60)的结果

cout << grade < 60 ? "fail" : "pass";

//完全错误,并且会报错。因为不能把cout和60作比较。

关于位运算符

如果运算对象是带符号的且它的值为负,那么位运算如何处理运算对象的“符号位”依赖于机器。因此这是一种未定义的行为,并没有明确的规定。

一般来说,在进行位运算时如果运算对象是小整形的,则它的值会被自动提升成较大的整数类型。

unsigned char b = 0233;
decltype(b<<8) i;
cout << sizeof(i) <<endl;  // 这里会输出4,也就是证明了被自动提升成较大的整数类型了
b <<= 8;
decltype(b) j;
cout << sizeof(j) <<endl;//这里会输出1.

右侧的运算对象一定不能为负

左移运算符就会在左侧插入值为0的二进制位,而右移运算符就取决于左侧对象类型了,若是有符号的,那就要取决于具体环境了。
。。。这句说了跟没说一样。。。

异或运算符,有且只有一个1的时候才是1,别的都是0;

关于位运算符的优先级

比算术运算符低,但是比关系运算符、条件运算符 优先级高;很不好记,最好都加上括号。

关于sizeof

注意 sizeof不需要真的解引用指针也能知道他所指的对像的类型;

还有以下几点:

  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于数组中的所有元素各执行一次sizeof的值的综合。而且并且不会把数组转换成指针来处理。
  • 对string或vector对象执行sizeof运算只返回该类型固定部分的大小,并不会计算对象中的元素占用了多少空间;
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要有效。

所以要想知道数组中的元素的个数,可以先sizeof整个数组,然后除以sizeof单个元素就OK了

constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int a[sz];//sz是常量表达式,正确;

最后看一个例子

int x[10];
int *p = x;
cout << sizeof(x)/sizeof(*x) << endl; // 输出10
cout << sizeof(p)/sizeof(*p) << endl; // 输出1

最后一个句子输出1,而不是10,为什么?

上面记了,sizeof一个指针返回的实际上是指针本身所占的空间大小。所以不要混淆了。我第一反应就混淆了(⊙﹏⊙)

关于逗号运算符

首先对左侧的表达式求值,然后将求值结果丢掉,然后依次执行,真正的结果是最右侧的表达式的值。

关于类型转换
int i=3.541+3.5+1;//运算后i的值为8

这个就是隐式转换,自动执行,先统一类型,后计算。比如这里就会先统一成double然后计算。

下列情况下,会进行隐式转换,编译器会自动地转换运算对象的类型。

  • 大多表达式中,比int小的整形值首先提升为较大的整形值。
  • 在条件中,非bool转换成bool。
  • 赋值语句中,右侧对象转换成左侧对象的类型。
  • 函数调用时也会发生。

整型提升会首先执行,即把小整数类型转换成较大整数类型。

比较麻烦的是既有无符号类型的运算对象,又有有符号类型的运算对象时。
这时候有两种情况:

  • 当无符号类型不小于带符号类型时,即无符号>=有符号时,那么带符号的运算对象转换成无符号的,此时,若int值刚好为负值,那就需要该负值对无符号类型值的大小进行取余。
  • 当无符号类型小于带符号类型时,此时的转换结果依赖于机器。具体还很迷。
    据说要想理解算数转换,办法之一就是研究大量的例子。。。

举一个例子

3.14L+'a'  //这里,'a'先提升至int,然后该int值再转换成long double
其他隐式类型转换

数组换成指针

int a[10];
int *ip=a;

而当decltype数组类型时,返回的就是数组类型而不是指针类型

int a[10];
decltype (a) b;
cout << sizeof b;

输出四十。

众所周知,可以将非常量类型转换成常量类型,但是反过来并不行,毕竟底层const时不能删除的;//后来发现可以强制去掉底层const。

关于while(cin>>s)

这里实际检查的是istream类型的值,IO库里定义了从istream向布尔转换的规则。所以可行。

关于强制类型转换

规则如下:

cast-name<type>(expression);

type是转换的目标类型,expression是要转换的值。
其中cast-name可以是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种,dynamic_cast支持运行时类型识别。
cast-name指定了执行的是哪种转换。

关于static_cast

只要不包含底层const,都可以使用statci_cast;

double s = static_cast<double>(j) / i;
  • 当把一个较大的算术类型赋值给较小的类型时,static_cast非常之有用,他会告诉读者和编译器:我们知道并且不在乎潜在的精度损失。因此会把一些可能因为精度的警告信息⚠给关闭。

  • static_cast对于编译器无法自动执行的类型转换也非常有用。比如我们可以使用static_cast找回存在与void*指针。

      void* p=&d;
      double *dp = static_cast<double*>p;
    

必须类型一样不然会产生未定义的结果。

关于const_cast

const_cast只能改变运算对象的底层const, 并且const_cast只改变常量属性,不能改别的东西。
其他形式的命名强制类型转换改变表达式的常量属性都会引发报错。

const char *pc;
char *p = const_cast<char*>(pc); //正确,但是通过p来改变pc 的值时改不得的。。。。

并不知道改变底层const有什么用。。指针指向的值本身也不会改变,而且会进入到很奇葩的现象:
指针的解引用的值跟指针指向的常量的值并不相同。。。

关于reinterpret_cast

假设有如下的转换:

int *ip;
char *pc = reinterpret_cast<char*>(ip);

必须牢记,pc所指的真实对象还是int而不是char。。。
据说这个函数非常之危险,我现在觉得它并没有什么用处,那我就先不用它了。
目前阶段,只要知道reinterpret_cst本质依赖于机器。日后工作要是想安全的使用reinterpret_cast必须要好好先了解目前所用的编译器实现的转换过程。

感觉关于强制类型转换,书上写的也蒙蒙的,反正这本书读到现在,就这里有点模糊,所以以后尽量避免强制类型的转换,书上也这么说。

关于旧式的强制类型转换

只要加括号就行了,这个我之前就会了,就不记了。
也是现在才知道,原来我一直会的强制类型转换是旧式的。。。。
据说旧式的不够清晰明了。确实有点。

最后附上运算符优先级表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第四章细节真是多,结束。

发布了75 篇原创文章 · 获赞 26 · 访问量 7664

猜你喜欢

转载自blog.csdn.net/qq_40962234/article/details/104594099