正文之前
好久没写了啊!!感觉自己都已经不爱简书了。不过其实我的《C++ Primer》已经看到400多页了。然而网络笔记还停留在120页,这个很骚啊,意味着我还有巨多的笔记坑要补!不过不要紧啊!因为我时间多,而且还耐得下心啊,写笔记么。就是种复习回顾的过程。不比看书的收获小的,而且往往收获的只是都是紧俏货,大家没那么容易看懂的那些!
正文
1、表达式的概念
表达式是由一个或者多个运算对象组成的式子,对表达式进行求值操作就会得到一个结果。通常来说,我们见到的字面值和变量就是最简单的表达式。
2、运算符
C++定义了一元运算符,二元运算符,以及一个唯一的三元运算符。其中一元的常见的包括区地址符 & 或者是 解引用符号*,至于二元的运算符那就多了去了!相等运算符== 乘法运算符 * 最后那个常见的三元运算符就是它了:a>b?a:b。当然,如果函数调用算是一种运算符,那么它的运算对象的数量是没有限制的,完全取决于它定义的时候限定了传入多少个值。另外,对于运算符还有几个很重要的东西 优先级,结合律,求值顺序.
3、左值与右值的概念
总结下来,当一个对象被用作右值的时候,那么用的是对象的值;当对象被用作左值的时候,用的时候它的身份(在内存中的位置。) 简单的例子:
int a=12;
int b=a;
在第二句的时候,b是左值,a是右值,b用的是b这个变量的身份,代表着可以接受一个值来填充b在内存中的位置。而对于a来说,在第二句中就是一个右值,我们取用的是a所指向的对象的值12,而不是它的地址。
以下是几个需要用到左值的地方:
- 赋值运算符需要一个左值来作为赋值对象;
- 取地址符作用于一个左值对象;返回一个指向对象的指针,这个指针倒是一个右值;
- 内置解引用,以及下标运算符,需要作用的对象是一个左值,取用这个内存的为孩子和总店个内容。
- 内置类型和迭代器的递增递减运算符。作用于左值对象。
4、优先级和结合律
优先级的问题,就是说当一个对象面对左边一个运算符,右边一个运算符,我是先跟左边的先结合呢还是右边的呢?这是个恼火的问题,所以就有了优先级这个说法。这个是人为规定的,当然也是符合自然规律的。不然在计算机中惩罚比加法优先级低的话,那根本没法进行数学运算对不?那么想必大家都知,括号是无视你的优先级的。括号自成小宇宙,必须等我括号里面的先进行运算,然后我给出一个结果到括号外,你们再来厮杀!!!(或者说是因为括号的优先级最高也行!)
5、求值顺序
有四种运算符明确的规定了其求值的顺序,这是在很多面试的题目里常常考到的。
- 逻辑与&& 运算符,明确指出先求左边再求右边。如果左边的就已经足够判定,anemia就直接忽略右边!所以有时候在右边放一个自增,你是没法确定到底会不会执行的。因为你还必须考虑到左值!!!
- || 逻辑或运算符,这个跟上面那位一个尿性了!所以,不说了!
- ?: 条件运算符,这个三元运算符,肯定要先求?左边的表达式,然后再求右边的!
- ,逗号运算符,这个很明显了吧??
下面是两个使用表达式以及运算符的忠告:
- 1.拿不住这个结合顺序,直接用括号强制来执行你想要的顺序。
- 2.如果改变了某个运算对象的值,那么请不要在表达式的其他地方继续使用这个对象。因为你没法确定哪个的先后。但是如果你已经可以确定了,那就好说,尤其是,如果一个运算对象的子表达式本身就是另外一个子表达式的运算对象的时候!!!举例: *++iter,此处iter是一个指针,那么我们的子表达式 ++iter 是 *(指针)的子表达式,所以自然而然的,先后顺序就出来了,就不会因其错误!
6、算术运算符
具体的优先级请参考上面的大图,上图是算术运算符的一些具体的操纵,接近我们的生活中的用法!但是由于计算机的精度有限,所以我们很多时候会出现溢出。因为每一种数据类型的表达范围是有限的。所以当你达到一个顶峰之后,如果继续向上增加,那么就会跳到末尾。比如说short这个数据类型在32位机器上的话,其表达范围是-32768~32767 也就是说如果一个short类型的变量,假如为x=32767.那么x+1=-32768 而不是32767 ,因为根本就没有32767 的表达方式,所以系统会自动多个溢出,新城“环绕”现象!另外,在新标准的C++中,除法的余数这个概念,更新之后为:余数与被除数的符号保持一致,也就是说 -7/2=-3···-1 而不是-7/2=-4···1这一点请记住,很容易错!
7、逻辑运算符和关系运算符
实在没啥力气讲清楚了!大家伙自己将就看吧!记住下面这两条短路求值的重点就OK:
- 逻辑与&& 运算符,明确指出先求左边再求右边。如果左边的就已经足够判定,anemia就直接忽略右边!所以有时候在右边放一个自增,你是没法确定到底会不会执行的。因为你还必须考虑到左值!!!
- || 逻辑或运算符,这个跟上面那位一个尿性了!所以,不说了!
具体的用处呢,我举个例子:
//假设有一个储存着若干string的vector 那么我们要遍历的话:就有如下的代码
for(const auto &r:text)
{
cout<<s<<endl;
if(s.empty() || s[s.size()-1]=='.')
cout<<endl;
else
cout<<" ";
}
上面的代码中,if后面跟着一个逻辑或表达式,那么我们先判断empty,如果经过判定,不是空的,我们才会在后面采用下标运算符进行string的判断,这样可以确保不会引发下标运算符的越界!!
8、赋值运算符
1) 赋值运算符的左侧对象必须是一个可以修改的左值(比如说const对象那就妥妥的不行的)。如果赋值运算符两侧的对象类型不同,那么只要右边的可以转换成左边的类型,那么最后返回的就是一个左边的类型的对象。
2) 赋值运算符满足的是右结合律,这一点与别的二元运算符大相径庭。下面举个栗子🌰说说。
int zval,yval;
zval=yval=0;
那么如上所见,上面的句子能够通过编译,但是第二句让我们不解。到底是怎么结合的顺序呢???根据右结合律,那么应该是先yval跟0结合,返回左侧运算对象,也就是说其顺序如下:
zval=(yval=0);
对于zval来说,它只知道自己的右边是一个yval被作为右值赋给了自己。并不知道括号里面究竟发生了什么!!!
知道了这个特点,我们就可对我们的语句进行一个改头换面的简化了:
//**初代版本**
int i = get_value(); //为了进入循环,不得不把第一次放在外面以免循环直接胎死腹中
while(i != 42)
{
i=get_value();
//other code using i to do sth like next line:
cout<<i<<endl;
}
// **改进版本**
while((i = get_value()) != 42)
{
//other code using i to do sth like next line:
cout<<i<<endl;
}
另外注意的就是,根据总的优先级图谱我们可以看到,赋值运算符的优先级其实不高,所以最好的就是加上一个括号,强制优先赋值运算符进行!! 确保程序不会出错,加括号是一个优秀的习惯哦~~
3) 请不要混淆相等运算符和赋值运算符。赋值是= 而相等是== ;二者的返回值是不同的, 一个是返回其左值的类型,一个是返回bool型的对象。所以请务必分清,这是一个很初级的错误!
9、自增自减运算符
为啥要单独的拿出来讲呢?因为这个东西很容易混淆不清,另外我想详细的讲一下。众所周知的,有前置和后置版本,++i 和 i++ 想必有点基础的同学都知道,一个是先加了再用,一个是用了再加。那么我们到底发生了什么呢???下面容我用代码为大家伙解释下:
为大家解释一个很偏门的概念。临时量。姑且命名为tmp,这个临时量是编译器自动生成,不会人为的定义。适用于一些已经进行了变化的变量,但是需要用到其原值的操作。
if(a++)
cout<<"Zhang Zhaobo is so good"<<endl;
上面这段代码发生了什么呢?大家都知道后置版本是先用后加,那么如何实现的?这就是tmp在作用了。具体的过程如下:
int tmp=a;
a=a+1;
if(tmp)
cout<<"Zhang Zhaobo is so good"<<endl;
这其实就是上面发生的动作,是完全等价的。而如果是前置版本,就没有tmp的定义与销毁过程,从性能方面来说节约很大的消耗。这也就是为什么:我们提倡在非必要的时候,统一使用前置版本++i而不是后置的自增,后置的版本比前置的开销大了很多。
另外对于自增自减运算符,我们还需要注意,在同一个表达式中,我们要注意求值顺序的概念。一般情况下,大部分的运算符都没有规定其运算对象的求值顺序,也就造成了其意义的不明确。但是在一般情况下是不会有影响的。不过如果在一个表达式里面存在同时取用一个变量的两个子表达式,那么最好祈祷不要发生无法预知的错误!!如下:
for(suto it = s.begin();it != s.end() && !isspace(it); ++it)
*it=toupper(*it);
上面的第二行。赋值运算符的左右两个子表达式中,都对it有求值的动作,我们知道反正是从右值赋值到左值,所以我们还可以顺利的把大写化后的值赋值回原处。那么下面这个呢?
for(suto beg = s.begin();beg != s.end() && !isspace(beg); ++beg)
*beg = toupper(*beg++);
你根本无法判断左边的beg到底是我们需要的beg 还是自增过的 beg+1;因为赋值运算符对于左右两边的求值顺序没有要求。所以很自然的,beg在一个表达式里面居然同时存在了两种可能。那肯定是要不得的。而前面那个没错的,是因为it自始至终都指向当前的对象。不会出现先后求值导致迭代器指向对象变化的情况!
10、成员访问运算符
.(点)运算符和 ->(箭头)运算符用于引用类、结构和共用体的成员。
点运算符应用于实际的对象。箭头运算符与一个指向对象的指针一起使用。简而言之,访问结构的成员时使用点运算符,而通过指针访问结构的成员时,则使用箭头运算符。
另外,解引用运算符的优先级基于点运算符,所以一般的时候 要这样:
*p.size() // Error
(*p).size() //Right
11、 位运算符
1) 其实位运算符的含义很明显,就是针对计算机的二进制进行操作,好比说左移运算符就相当于是整个整数x2 因为左移是基于二进制的,所以如果你要深入下,可以去我看我以前的这篇:
【计算机本科补全计划】计算机的算数运算 +-*/
上面这篇文章有很详细的说明计算机在运算的时候发生了什么的!
2) 对于有符号数来说,右移左移什么的存在一个符号位的问题,不同的编译器有不同的处理方式。不过大多数都是能够自行修正这个问题的。我就不详细说了。毕竟这个内容属于很高深了。不属于我们Primer的范畴~~
3) 移位运算符大家是不是有点眼熟?没错cout<<"zhangzhaobo is good"<<endl;
没错,这个也是移位运算符,不过它是一个I/O流的移位运算,也就是刷新缓冲区的操作。但是请注意,移位运算符满足左结合律,所以 如果有两个运算符在一个表达式里面,那么是完全按照左结合律来的。比如下面这句:
cout<<"zhang"<<" zhaobo"<<endl;
//等价于
cout<<"zhang zhaobo"<<endl;
对,左结合律计时从左到右的结合。
12 、 sizeof() 运算符
sizeof() 有两种形式,
sizeof(type);
sizeof expr
前面那种是返回type对应的类型的大小。在第二种形式中,sizeof返回expr这个表达式结果的类型大小。都是对应的类型的大小。且因为sizeof并没有实际的对对象进行运算,所以哪怕你传入的对象并没有实际意义都是可行的,比如说空指针这种。其针对的是类型而不是具体的对象。有以下几点需要注意
- 引用类型返回被引用对象的空间的大小
- 对指针执行sizeof运算返回的是指针所占的空间的大小
- 对指针解引用执行sizeof运算,返回其指向的对象的所占空间大小。
- 对指针解引用执行sizeof运算,不需要其指向对象有效,只要有类型即可。
- 对数组进行sizeof运算,返回的是数组的大小,也就是相当于对数组的所有元素执行一次sizeof 并且相加!
Type | Size | 数值范围 |
---|---|---|
无值型void | 0 byte | 无值域 |
布尔型bool | 1 byte | true false |
有符号短整型short [int] /signed short [int] | 2 byte | -32768~32767 |
无符号短整型unsigned short [int] | 2 byte | 0~65535 |
有符号整型int /signed [int] | 4 byte | -2147483648~2147483647 |
无符号整型unsigned [int] | 4 byte | 0~4294967295 |
有符号长整型long [int]/signed long [int] | 4 byte | -2147483648~2147483647 |
无符号长整型unsigned long [int] | 4 byte | 0~4294967295 |
long long | 8 byte | 0~18446744073709552000 |
有符号字符型char/signed char | 1 byte | -128~127 |
无符号字符型unsigned char | 1 byte | 0~255 |
宽字符型wchar_t (unsigned short.) | 2 byte | 0~65535 |
单精度浮点型float | 4 byte | -3.4E-38~3.4E+38 |
双精度浮点型double | 8 byte | 1.7E-308~1.7E+308 |
long double | 8 byte |
多嘴一句,我最大的惊讶来自于:char竟然可以提升为 int类型并且与之比较!!!
正文之后
好久没写了真是对不住怀有期待的读者了(不知道有木有??) 我会坚持下去的了。不过偶尔懈怠下也请原谅我了~ sorry 我后面会慢慢补上的!等以后有空了自己作个博客好好的整理下,目前的话只能在简书发咯~
下面这个东西好好玩!