一、基础
1.基本概念
一元运算符:作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*);
二元运算符:作用两个对象的运算符
三元运算符:作用域三个运算对象
一些符号既能作为一元运算符,也能作为二元运算符,比如*。
重载运算符:
IO库的>>和<<运算符以及string对象,vector对象和迭代器使用的运算符都是重载的运算符。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的:但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值(重要):
C++的表达式要不然是右值,要不然就是左值。这两个名词是从C语言继承过来的,原来是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。
在C++语言中,二者的区别就要复杂很多:
一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。
可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
知乎看到的经典总结:左值右值的形式区分(或者称语法区分)是能否用取地址&运算符;语义区分(即其本质涵义)在于表达式代表的是持久对象还是临时对象。
使用关键字decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量),得到一个引用类型。
如:假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&p,另一方面,由于取地址运算符生成右值,所以decltype(&p)的结果是int**,也就是说是一个指向整型指针的指针。
一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
关于在等号左右两边的判断(左值可以出现在等号左边或右边,右值只能出现在右边)是不完善的,例如有些左值是无法出现在赋值符左边的,典型的就是常变量对象:如const int i = 1,变量 i 除了初始化的过程其他情况是不允许再赋值的,但是 i 却毫无疑问地是左值,可以使用取地址符。
不和对象相关的字面值常量(只有内置类型具有)一般来说是右值,但是有一个例外,就是字符串字面值,可以取地址,是左值,但是也不能位于等号左边。
相对的是,有时候右值是可以位于等号左边的,尽管新标准并不提倡这一点。例如,string s1, s2; s1 + s2 = "wow!"。
s1 + s2是右值,但是位于赋值符号的左边。为了维持向后兼容性,新标准库仍然允许向右值赋值。
2.优先级与结合律
括号无视优先级与结合律
3.求值顺序
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一对象,将会引发错误并产生未定义的行为:
int i=0;
cout<<i<<" "<<++i<<endl; //未定义的,无法确定先求i还是++i的值
二、算术运算符
运算符 | 功能 | 用法 |
+ | 一元正号 | +expr |
- | 一元负号 | -expr |
* | 乘法 | expr*expr |
/ | 除法 | expr/expr |
% | 求余 | expr%expr |
+ | 加法 | expr+expr |
- | 减法 | expr-expr |
上述运算符的优先级由高到低 |
整数相除还是整数,也就是说,如果商由小数部分,直接弃除。
运算符%俗称“取余”或“取模”运算符,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须为整数类型:
int ival=42;
double dval=3.14;
ival%12; //正确:结果是6
ival%dval; //错误:运算对象是浮点类型
C++新标准规定商一律向0取整(即直接切除小数部分)。
取余运算中,除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。
三.逻辑和关系运算符
结合律 | 运算符 |
右 | ! |
左 | < |
左 | <= |
左 | > |
左 | >= |
左 | == |
左 | != |
左 | && 逻辑与 |
左 | || 逻辑非 |
四.赋值运算符
赋值运算符的左侧对象必须为一个可修改的左值:
int i=0,j=0,k=0; //初始化而非赋值
const int ci=i; //初始化而非赋值
1024=k; //错误:字面值是右值
i+j=k; //错误:算术表达式是右值
ci=k; //错误:ci是常量左值
如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型
赋值运算符满足右结合律:
int ival,jval;
ival=jval=0; //正确
string s1,s2;
s1=s2="OK"; //正确
int ival,*pval;
ival=pval=0; //错误:不能把指针的值赋给int
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
切勿混淆相等运算符和赋值运算符。
复合赋值运算符:
+= -= *= /= %= //算术运算符
<<= >>= &= ^= |= //位运算符
任意一种复合运算符都完全等价于:
a=a op b
五.递增与递减运算符
递增运算符(++)、递减运算符(--)
前置版本:首先将运算对象加1(减1),然后将改变后的对象作为求值结果
后置版本:首先将运算对象加1(减1),但是求值结果是运算对象改变之前的那个值的副本
这两种运算符必须作用于左值运算对象。前置版本将运算对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
建议:除非必须,否则不用递增递减运算符的后置版本。因为前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回了改变了的运算对象。与之相比,后置版本则需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
在一条语句中混用解引用和递增运算符:
auto pbeg=v.begin();
while(pbeg!=v.end()&& *pbeg>=0)
cout<<*pbeg++<<endl; //输出当前值并将pbeg向前移动一个元素
后置递增运算符的优先级高于解引用运算符,因此*pbeg++等价于*(pbeg++).
六、成员访问运算符
点运算符和箭头运算符都可以用于访问成员。表达式ptr->mem等价于(*ptr).mem
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
七、条件运算符
cond?expr1:expr2
首先求cond的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值:
string finalgrade=(grade<60)?"fail":"pass"; //判断成绩是否合格
当条件运算符的两个表达式都是左值或者能转换成同一左值类型时,运算的结果是左值,否则运算的结果是右值。
嵌套条件运算符:
//将成绩分成三档:优秀、合格、不合格
string 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
第二条表达式等价于:
cout<<(grade<60); //输出1或者0
cout?"fail":"pass"; //根据cout的值是true还是false产生对应的字面值
第三条表达式等价于:
cout<<grade; //小于运算符的优先级低于移位运算符,所以先输出grade
cout<<60?"fail":"pass"; //然后比较cout和60
八、位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
运算符 | 功能 | 用法 |
~ | 位求反 | ~expr |
<< | 左移 | expr1<<expr2 |
>> | 右移 | expr1>>expr2 |
& | 位与 | expr&expr |
^ | 位异或 | expr^expr |
| | 位或 | expr | expr |
关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型
有一种常见的错误是把位运算符和逻辑运算符搞混了,比如位与(&)和逻辑与(&&)、位或(|)与逻辑或(||)、位求反(~)与逻辑非(!)。
九、sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t类型(包含在头文件cstddcf中)
运算符的运算对象有两种形式:
sizeof (type)
sizeof expr
在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的一点是,sizeof并不实际计算其运算对象的值:
Sales_data data,*p;
sizeof(Sales_data); //存储Sales_data类型的对象所占的空间大小
sizeof data; //data的类型的大小,即sizeof(Sales_data)
sizeof p; //指针所占的空间大小
sizeof *p; //等价于sizeof(*p),p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue; //Sales_data的revenue成员对应类型的大小
sizeof Sales_data::revenue; //另一种获取revenue大小的方式
因为sizeof不会实际上求运算对象的值,所以即使p是一个无效的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正的使用。sizeof并不需要真的解引用指针也能知道它所指对象的类型。
sizeof运算符的结果部分地依赖其作用的类型:
- 对char或者类型为char的表达式执行sizeof运算,结果得1.
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小
- 对指针执行sizeof运算得到指针本身所占空间的大小
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或者vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:
constexpr size_t sz=sizeof(ia)/sizeof(*ia); //返回ia的元素数量
int arr2[sz];
十、逗号运算符
按照从左到右的顺序依次求值即可。
十一、类型转换
隐式转换是自动执行的,无需程序员介入。
1、算术转换
算术转换定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。
整形提升负责把小整数类型转换成较大的整数类型。
无符号类型的运算对象:
如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
像往常一样,首先要执行整型提升。如果结果的类型匹配,无需进行进一步的转换。如果两个提升后的运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的对象转换成较大的类型。
2、其他隐式类型转换
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int ia[10];
int *ip=ia;
当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。
指针的转换:常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。
转换成布尔类型:如果指针或算术类型的值为0,转换结果是false,否则转换结果是true。
转换成常量:如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用;相反的转换则并不存在,因为它试图删除掉底层const:
int i;
const int &j=i; //非常量转换成const int的引用
const int *p=&i; //非常量的地址转换成const的地址
int &r=j,*q=p; //错误:不允许const转换成非常量
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如果同时提出多个转换请求,这些请求将被拒绝。
string s,t="a value"; //字符串字面值转换成string类型
while(cin>>s) //while的条件部分把cin转换成布尔型
3.显式转换
命名的强制类转换具有如下形式:
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static-cast、dynamic_cast、const_cast、reinterpret_cast中的一种。
static-cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如:
//进行强制类型转换以便执行浮点数除法
double slope=static_cast<double>(j)/i;
当需要把一个较大的算术类型赋给较小的类型时,static_cast非常有用。本来编译器会发出警告,当执行显式转换后,警告消息就会被关闭了。
static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用其找回存在于void*的指针:
void *p=&d;
double *dp=static_cast<double*> p;//正确:将void*转换回初始的指针类型
强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
const_cast
const_cast只能改变运算对象的底层const,可以将常量对象转换成非常量的对象:
const char *pc;
char *p=const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为
一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
只有const_cast能够改变表达式的常量属性,使用其他形式的命名强制类型转换改变类型的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:
const char *cp;
char *p=static_cast<char*>(cp); //错误:static_cast不能转换掉const性质
static_cast<string>(cp); //正确:字符串字面值转换成string类型
const_cast<string>(cp); //错误:const_cast只改变常量属性
建议:强制类型转换总是充满风险,应避免强制类型转换