运算符介绍
cpp定义了一元运算符和二元运算符,服用与一个运算对象的运算符是一元运算符,如&和*,作用于两个运算对象的是二元运算符,如相等运算符==,还有一个三元运算符。函数调用也是一种特殊的运算符其对运算对象没有限制。
&和*运算符看上下文,不同的上下文下是完全不同的运算符。
cpp的表达式要不是右值要不是左值。在c语言中是这样子的:左值可以位于赋值语句的左侧,但是右值不行。
但是在cpp中:一个左值表达式的求职结果是一个对象或者是一个函数,然而以常量对象为代表的某些左值不能作为赋值语句的左侧运算对象。此外虽然某些表达式的求值结果是对象但他们是右值而并非左值。简单的归纳:当一个对象是右值的时候用的是对象的值(内容),左值的时候用的是对象的身份(在内存中的位置).
左值指的是一个对象或者一个函数在内存中是有固定的地址的,右值是一种表达式,结果是一个值而非值所在的位置,一般在内存中没有固定的位置。
一个重要的原则是需要用右值的时候可以用左值进行代替,但是需要用左值的时候不能用右值作为代替。列举一下熟悉的需要用左值的地方:
- 赋值运算符需要一个(非常量)左值作为其左侧运算对象。得到的结果也是一个左值。
- 取地址符需要一个左值作为运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。(需要被另外一个右值接收)
- 内置解引用运算符下表运算符的求值结果都是左值。(不需要被接收如
*p=2;
) - 内置类型和迭代器的递增递减运算符
使用decltype的时候左值右值也有些不同,对于指针类型如果表达式求值结果是左值decltype作用于该表达式得到一个引用类型。如假设p是一个int*类型,decltype(*p)是一个int&类型。decltype(&p)得到的就不是一个引用类型而是正常的int **类型。
优先级及结合律
复合表达式是含有多个运算符的表达式。求解这个就需要考虑到优先级来吧运算对象和运算符组合在一起进行运算。优先级与结合律决定了运算对象的组合方式。
求值顺序
优先级规定了运算对象的组合方式,但是美欧说明运算对象按照什么顺序求值。在大多数情况下不会明确的指出求值的顺序。对于如下的表达式:
int i = f1()*f2();
我们都知道函数值的计算肯定会在执行乘法之前被调用但是不知道哪个函数先调用。对于这种情况如果表达式都修改了同一个对象就会产生一些未定义的行为,产生意想不到的错误,比方说int i =0; cout <<i<<++i<<endl;
这时候可以是输出00也可能适合11,具体是看编译器是如何进行计算的。
算术运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xUtuuW6l-1641811798366)(/home/brother/.config/Typora/typora-user-images/image-20210824221504314.png)]
其中一元运算符 优先级最高,接下来是乘法和除法,最低是加法减法。
商一律往0取整。即直接切除小数部分,13%6=1;13/6=2;
对于除法运算,规定:(m/n)*n+m%n=m。因此21%5=1;
逻辑运算符
逻辑运算符作用于任何能转换成bool值的类型。逻辑运算符和关系运算符返回值都是bool类型。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0Yup34V-1641811798367)(/home/brother/.config/Typora/typora-user-images/image-20210824222845968.png)]
对于||和&&来说,当前仅当两个运算对象都为真时结果为真。因此会先求左边的值,如果左边的直接可以判断出来这个表达式的结果就不会去计算右边的值了。
赋值运算符
赋值运算符左侧运算对象必须是一个可修改的左值,其运算结果是左侧的对象。
//右边是给定的条件 int i=0,j=0,k=0; const int ci=i;
1024=k; //1024是一个字面值,字面值是右值
i+j=k; //i+j是算是表达式,会生成临时变量是右值
ci=k; //ci是左值但是是一个不可修改的常量左值
//c++允许使用花括号{}括起来的初始值列表,作为赋值语句的右侧运算对象。
k-{3.141};
vector<int> vi;
vi={0,1,2,3,4,5};
赋值运算符满足右结合律 这一点和其他的二元运算符都不一样。
i=j=k={0};
赋值语句的优先级比较低,有时候需要加()来满足我们的需求。
int i=0;
while( (i=get_value())!=42 ) //这里因为
continue;
复合赋值运算符:+=,-=,<<=都相当于a=a+b,a=a<<b
递增和递减运算符都有前置和后置版本
- 前置版本,先进行运算然后将改变后的对象作为求值的结果。会将对象本身作为左值返回。
- 后置版本也会将运算对象+1,但是求值之后返回的是改变之前的副本。会将对象+1前的副本作为右值返回。
*p++是一种很常用的使用方法,技能往前走也能返回当前的值
//其实后置++优先级高于解引用运算符
成员访问运算符
指针使用->访问成员,对象使用.访问成员。有一个常见的错误*p.mem要避免。
条件运算符
cond ? expr1:expr2;
和c语言不同的是条件运算符是可以返回左值的。(如果表达式友左值的话)这个左值用来被赋值。
条件运算符支持嵌套,因此可以在条件运算符的内部嵌套另外一个条件运算符。即一个条件运算符表达式可以作为另外一个条件运算符表达式的cond或者expr。
条件运算符的优先级非常低。因此当一条长的表达式中嵌套了条件运算符之后要注意必要时加上括号。如cout<<grad<60?"fail" :"pass"<<endl;这句话就会导致cout和60比大小就很奇怪
位运算符
位运算符对于符号位的处理没友明确的规定因此,强烈建议只使用无符号类型的数字来进行位运算操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2MC9Tiu-1641811798367)(/home/brother/.config/Typora/typora-user-images/image-20210825000431792.png)]
左移动运算符在右侧插入值为0的二进制位,并且会补上符号位.
右移运算符会在左侧插入符号位的副本,因为是以补码的形式补充的,比方说-1的二进制全是1组成,无论进行怎么样的右移并且补上符号位都全是1,name结果也全都是-1。
重载运算符
对运算符进行重载之后优先级变。
移位运算符的优先级不高不低,比算术运算符的优先级低,但是比关系运算符,赋值运算符和条件运算符的优先级高。
对于移位运算符满足左结合律。所以表达式:
cout<<"hi"<<"there"<<endl;等价于( (cout<<"hi")<<"there") )<<endl;
sizeof运算符
sizeof运算符返回一个表达式或者一个类型名字所占的字节数,sizeof运算符满足右结合律,其所得值是一个size_t类型的常量表达式。其运算符的对象友如下两种形式:s并且sizeof不实际计算运算对象的值而是采用推理的方式。
sizeof (type)
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的revennue成员对应的类型的大小
sizeof运算符的一些特点:
- 对引用类型得到被引用对象所占空间的大小
- 对指针类型得到指针本身所占空间的大小
- 对解引用指针执行得到指向所指的对象占用空间的大小,指针不需要有效。
- 对数组执行sizeof得到整个数组所占空间的大小,把整个大小除以每个数组元素占用的空间大小就可以得到整个数组的占用空间,sizeof不会将数组转换为指针来处理
- 对string对象或者vector对象只会得到这种类型的固定部分的大小,不会计算对象中的元素占用过了多少的空间
逗号运算符
逗号运算符是含有两个运算对象,按照从左往右的顺序依次求值。逻辑与、逻辑或以及条件运算符也都是从左往右的求值顺序。
逗号运算符首先对左侧的表达式求值然后将结果丢掉,其真正的执行结果是右侧表达式的值,如果右侧运算对象是左值,那么最终的求值结果也是左值。
逗号运算符常用在for循环当中。例如for (i=0;i<100;i++,j--){}
类型转换
如果两种类型可以相互转化那么我们就收他们是有关联的。
隐式转换
下列情况下会自动转换运算对象的类型。
- 大多数表达式中比int小的会提升为较大的整数类型
- 条件中非boo值转化为bool值
- 初始化中,初始值转化为变量的类型,赋值语句中右侧运算对象转化为左侧运算对象的类型
- 函数调用时也会发生
算数转换很多都会隐式转换。但是除了算数转换之外还有几种隐式转换类型。包括如下:
- 数组转换为指针:如
int ia[1]; int *ip=ia;//这里ia将会转换为指向数组首元素的指针
,但是当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof以及typeid等运算符对象时上述转换不会发生
nullptr可以转换为任意指针类型;指向任意非常量的指针能转换为void*;指向任意对象的指针能转换为cosnt void*;子类指针能转换为父类指针。 - 转换成bool类型:一般来说只要非空或者非0,转换为bool类型都是true
- 转换为常量:允许将指向非常量类型的指针(或引用)转换为指向相应的常量类型的指针(或引用)(自己感动自己的行为是合法的)
- 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
对于代码cppstring s,t="a value";//字面值可以转化为string类型
while(cin>>s);//(cin>>s)这句话意思是把cin读取结果传给s,并把cin作为求值结果。 cin有到bool类型的隐式转换(如果cin读取成功则为bool),所以可以用来判断有没有正常的用cin给s输入值
显式转换
有时需要显式的把对象类型转化为另外一种类型,就是用显式转换。
- 命名的强制转换:具有cast-name<type>(expression)的形式。其中type是转换的目标类型,expression是要转换的值。如果type是引用类型则结果为左值。cast-name是**static_cast、dynamic_cast、const_cast和reinterpret_cast的一种。**其中dynamic_cast支持运行时类型识别。
- static_cast:比较自然或者低风险的转换,如整型、字符型之间的转换,另外如果对象所属的类重载了强制类型转换符T(T是其他类型的名字),则static_cast也能进行对象到T类型的转换。其不能在不同类型的指针进行转换以及整型和指针的转换,因为这种属于高风险的转换。
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast.如double f=static_cast<double>(i);
当需要把大的算数类型转化为小的类型的时候编译器会警告,使用了强制类型转换就不会输出警告信息。
static_cast对编译器无法自动转换的类型也非常有用。如void *p = &i; int *j=static_cast<int *>(p);
- const_cast:只能改变运算对象的底层const,也只能用cosnt_cast来改变表达式的常量属性。const一般作用于引用和指针。
const char *pc; char *p=cosnt_cast<char *>(pc);
这样可以把const char *变量转化为一个char *变量,但是对*p进行写操作是未定义的行为。
const_cast常用在有函数重载的上下文中。 - reinterpret_cast:用于进行各种不同类型之间、不同引用之间以及指针和能容纳指针的整数类型之间的转换,转换时执行的是逐个bite复制的操作。这种转换很灵活但是需要程序员细心了。如可以把一个int*转化为string *但是无法保证之后的操作不出错。
- dynamic_cast:使用reintepret_cast可以将多态基类转化为派生类的指针,但是这种方式不检查安全性。dynamic_cast用来把基类指针转化为派生类指针或引用,而且能够检查转换的安全性,对于不安全的指针转换到返回NULL指针。
dynamic通过运行时检查来保证转换的安全性。其不能把非多态基类的指针或引用强制转化为子类的指针或引用-因为没办法保证安全性,这种工作只能用reinterpret_cast来完成。 - 早期的强制转换 :
type (expr);或者(type)expr;
- 运算符优先级表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUzoGMl7-1641811798367)(/home/brother/.config/Typora/typora-user-images/image-20210825223132562.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mt6hVLGX-1641811798368)(/home/brother/.config/Typora/typora-user-images/image-20210825223203262.png)]
- static_cast:比较自然或者低风险的转换,如整型、字符型之间的转换,另外如果对象所属的类重载了强制类型转换符T(T是其他类型的名字),则static_cast也能进行对象到T类型的转换。其不能在不同类型的指针进行转换以及整型和指针的转换,因为这种属于高风险的转换。