学习目标:
表达式:由一个或多个的运算对象和运算符组成。
学习内容:
一、基础
1. 基础概念
- 运算符
i. 一元运算符:作用于一个运算对象,如*、&等;
ii. 二元运算符:作用于两个运算对象,如+、==等;
iii. 三元运算符:作用于三个运算对象,如a>b ? a:b; - 表达式求值的结果依赖于:运算符的优先级、结合律以及运算对象的求值顺序。
- 运算符重载:当运算符作用于类对象时,用户可以自定义其含义。
- 左值和右值
i. 左值:可以取到地址的表达式;
ii. 右值:取不到地址的表达式;
2. 优先级和结合率
-
字面意思就是数学上的优先级和结合率。提要提一下的就是对指针的影响。
int a = {0, 2, 4, 6, 8}; int b = *(a + 4); // 8 int c = *a + 4; // 0 + 4
-
IO相关的运算符满足左结合率;
3. 求值顺序
- 有四种运算符明确规定了对象的求值顺序
i. &&,与运算符,规定先求左侧运算对象的值,只有当左侧对象为真时才会执行右侧对象;
ii. ||,或运算符,规定先求左侧运算对象的值,只有当左侧对象为假时才会执行右侧对象;
iii. ?a:b,条件运算符,条件成立,执行a,条件不成立,执行b;
iv. a,b 逗号运算符,从左到右执行,先执行a,再执行b,最后返回b的结果;
二、算术运算符
-
所有的算术运算符
优先级:一元 > 乘除余 > 加减 所有都满足左结合律:优先级相同时,从左往右计算 -
需要注意的一些点
i. 布尔类型在做一元算术运算符时,会先将布尔类型提升为int类型,再运算,运算完再降为布尔类型(如果是0,bool=0,只要不是0,bool=1);bool a = true; // a -> 1, -a = -1, -1 -> true bool b = -a; // b = true
ii. 整数除法,一律向下取整,只保留整数部分;小数除法还是保留小数部分;
int a = 7; int b = 2; cout << a / b << endl; // 3 double c = 3.14; int d = 2; cout << c / d << endl; // 1.57
iii. 求余,运算对象必须是两个整数,否则会报错;
double a = 3.14; int b = 2; cout << a % b << endl; // 报错
iv. 计算溢出问题(当前计算的结果超出该类型的范围),导致程序结果崩溃;
short a = 32767; a += 1; cout << a << endl; // -32768
v. 除法:(-m)/ n = m / (-n) = -(m/n);
vi. 求余:m % (-n) = m % n,(-m)% n = - (m%n)
三、逻辑和关系运算符
-
所有的逻辑和关系运算符
-
一些需要注意的点
i. 上述运算符优先级从高到低排列,如果搞不清优先级,最好是写的时候自己打上括号;
ii. 逻辑与:当且仅当左侧运算对象为真时才会对右侧运算对象求值;
iii. 逻辑非:当且仅当左侧运算对象为假时才会对右侧运算对象求值;
iv. 关系运算符会比较左右运算对象的大小关系后再返回bool值;if(a < b < c) // 错 a<b返回bool值 bool再和c比较 明显不是我们的意思 if((a<b) && (b<c)) // 对 if(a<b && b<c) // 对
v. bool值转换为其他算术类型时:true=1,false=0
四、赋值运算符
-
赋值运算符 =;
-
初始化不是赋值;
int a = 0, b = 0, c = 0; // 初始化 错 const int d = a; // 初始化 错
-
赋值运算符的左侧运算对象必须是一个可修改的左值;
1024 = c; // 左侧字面量一个右值 错 a + b = c; // 左侧算法表达式是一个右值 错 d = c; // 左侧是一个不可修改的左值 错
-
赋值是允许左右两侧类型不一致的,因为可以类型转换;
c = 0; // 对 类型一致 直接赋值 c = 3.14; // 对 类型不一致 先类型转->3 再赋值
-
c++11可以使用初始化列表进行赋值,但是这种赋值方式更加严格,类型必须一致;
c = {3.14}; // 类型不一致 不能用列表初始化 c = {3}; // 对 vector<int> v; v = {0,1,2,3}; // 对 初始化列表赋值用在vector中比较多
-
赋值运算满足右结合律,所以允许多重赋值a=b=0,相当于先把0赋值给b,返回b,再把b赋值给a,返回a;
a = b = 0; // 对
但是多重赋值要注意类型,很容易出错:
int a, *b; a = b = 0; // 错 不能把指针类型b赋值给a
-
赋值运算符的优先级低于关系运算符,所以一般在条件语句中使用赋值运算符都要用括号括起来;
int i; if((i=get_value()) != 42) // 对 if(i=get_value() != 42) // 错 先执行关系运算符 会将判断结果bool值赋给int类型i
-
不要把赋值运算符=和相等运算符==搞混了;
int a = 0; int b = 0; if(a = 2){ // 把2赋值给a,并返回a,if(a) 不等于0 所以是true 执行cout cout << "a = 2" << endl; } if(b == 2){ // b==2 不成立 返回false 不执行cout cout << "b == 2" << endl; } // 输出a = 2
-
复合赋值运算符:+=、-=、*=、/=、%=、<<==、>>=、&=、^=、|=; 几乎等价于 a = a op b; 唯一区别是复合赋值运算符的左值只求一次值,而a = a op b需要对左值a求两次值,但是这点区别对程序性能几乎忽略不计,所以一半都用复合运算符,增加程序的可读性。
五、递增和递减运算符
-
递增运算符++、递减运算符–
-
前置递增和后置递增
i. 前置递增: ++a,首先将a自增,再返回自增后的结果;
ii. 后置递增: a++,首先创建一个a的副本,保留原始值,再将a自增,最后返回a的副本,即返回原始值;int a = 0, b; b = ++a; // a=1 b=1 b = a++; // a=2 b=1
iii. 一般情况都使用前置递增,因为后置递增需要保留原始值的副本,浪费资源;
-
混用解引用和后置递增符(后置递增重点使用方法):后置递增运算符优先级高于解引用 输出vector中所有大于0的元素,直到遇到负数为止
vector<int> v = {0, 1, -2, 3}; auto beg = v.begin(); while(beg != v.end() && *beg >= 0){ // beg++: beg先自增 向后移动 再返回beg // *beg++: 返回*(原始位置副本) cout << *beg++ << endl; }
注意:*beg++ 等价于下面两种形式,但是一半都使用 *beg++,更简洁,这种后置递增方式很常见,需要熟悉这种写法;
*beg++; // <==> *(beg++); // <==> *beg;beg++;
-
递增运算符可以改变运算对象的值,所以要注意一条语句前后都使用对象且会改变的情况: 运算对象的求值顺序没有规定,所以不知道*beg = toupper(*beg++);的求值顺序。 如果先求左侧的值 ==> *beg = toupper(*beg); 如果先求右侧的值 ==> *(beg+1) = toupper(*beg); 所以,程序也不知道怎么执行,崩溃了,此时我们说这条赋值语句是未定义的。
vector<char> v = {'a', 'b', 'c', 'd'}; auto beg = v.begin(); while(beg != v.end() && !isspace(*beg)){ *beg = toupper(*beg++); // 错误 程序崩溃 }
六、成员访问运算符
-
类型:点运算符.和箭头运算符->,用于访问类对象的一个成员; p->mem 等价于 (*p).mem
string a = "Hello World"; string *p = &a; cout << a.size() << endl; // string对象a的size成员 点运算符 cout << (*p).size() << endl; // p指针所指向的对象的size成员 cout << *p.size() << endl; // 错 解引用优先级低于点运算符 先执行p.size() 报错 cout << p->size() << endl; // 箭头运算符
-
箭头运算符作用于一个指针类型的运算对象,结果是左值(对象是左值,成员是变量也是左值,那么p->mem的结果也是左值);点运算符如果成员所属对象是左值,那么结果是左值,如果成员所属对象是右值,那么结果是右值;
七、条件运算符
-
条件运算符是唯一的三元运算符,形式:cond ? exp1 : exp2 执行顺序:先求出条件cond的值,如果是True执行exp1,并返回exp1的值,否则执行exp2,并返回exp2的值;
// 先判断条件grade是否大于60,如果是返回pass 否则返回fail string final_grade = (grade > 60) ? "pass" : "fail";
-
条件运算符满足右结合律,当嵌套条件运算符时,右边的条件运算可以作为左边条件运算的分支;但是嵌套条件运算符会降低程序的可读性,所以一般最多嵌套2-3层;
// 先判断是否大于90分,如果大于输出high pass 否则再执行右边分支 // 判断是否大于60分,如果是返回pass 否则返回fail string final_grade = (grade > 90) ? "high pass" : (grade > 60) ? "pass" : "fail";
-
条件运算符的优先级比较低,所以在输入表达式时最好使用括号括起来;
cout << ((grade > 60) ? "pass" : "fail"); // fail // 等价于 cout << (grade > 60); cout ? "pass" : "fail"; cout << (grade > 60) ? "pass" : "fail"; // 0 // 等价于 cout << grade; cout > 60 错 cout << grade > 60 ? "pass" : "fail"; // 错
八、位运算符
-
位运算符作用于整数类型的运算符对象,把运算对象看成二进制位的集合。位运算符提供检查和设置二进制为的功能;
-
所有位运算符总结
-
位操作通常处理无符号类型整数;
-
移位运算符(<<、>>),移之后填充0
unsigned char bits = 0233; // 2^3=8 所以二进制从右往左数每3位表示八进制的一位 // 8进制 -> 2进制24个0+10011011 -> 16个0+10011011+8个0 bits << 8; // 24个0+10011011 -> 27个0+10011 bits >> 3; // 24个0+10011011 -> 32个0 bits << 31;
-
移位运算符都满足左结合律;
-
优先级顺序:算法运算符 > 移位运算符 > 关系/赋值/条件运算符;
cout << 42 + 10; // 对 算术运算符 > 移位运算符 cout << (42 + 10); // 对 cout << 42 > 10; // 错 等价于 cout << 42; cout > 10;错误
-
位求反运算符~,所有位置0->1,1->0
unsigned char b1 = 0145; // 24个0+01101000 -> 24个1+1001011 ~b1;
-
位与&、位或|、异或^运算 位与&:两个运算对象对应位置都是1,则该位置返回1,否则返回0; 位或|:两个运算对象对应位置有一个为1,则该位置返回1,否则返回0; 异或^:两个运算对象对应位置必须一个为0一个为1,则该位置返回1,否则返回0;
unsigned char b1 = 0145; // 01100101 unsigned char b2 = 0257; // 10101111 b1 & b2; // 00100101 b1 | b2; // 11101111 b1 ^ b2; // 11001010
九、sizeof运算符
-
sizeof运算符返回一条表达式或者一个类型名字所占的字节数byte;
-
形式:sizeof(type)或sizeof expr;第二种返回表达式结果类型的大小;sizeof运算符并不计算其运算对象的大小,只关注其类型;
i. 对引用执行sizeof运算符返回被引用对象所占空间的大小;
ii. 对指针执行sizeof运算返回指针本身所占空间的大小;
iii. 对解引用指针执行sizeof运算符返回指针所指的对象所占空间的大小,指针可以无效;
iv. 对数组执行sizeof运算符是算数组中所有对象所占空间的和,对解引用数组执行sizeof运算符是算数组第一个元素所占的空间;Sale_data data, *p; sizeof(Sale_data); // 返回存储Sale_data类型的对象所占用的空间大小 sizeof data; // 类对象data的类型的大小 sizeof p; // 指针对象p所占的空间的大小 // 等价于sizeof (*p) p可以是一个未初始化的指针,因为这里根本不需要解引用,只需要知道类型即可知道内存大小 sizeof *p; // 指针p所指向的内容的类型的大小 sizeof data.revenue; // 通过对象:Sale_data类的revenue成员对象的类型的大小 sizeof Sale_data::revenue; // 通过类名:Sale_data类的revenue成员对象的类型的大小
-
sizeof常用与求数组中元素的个数,string和vector中的元素个数通常不用sizeof求;
int a[] = {1, 2, 3}; constexpr size_t sz = sizeof(a) / sizeof(*a); // 3
十、逗号运算符
-
逗号运算符,常用在for语句中,按照从左到右的顺序依次求值。
vector<int> vec = {1, 2, 3, 4}; int cnt = vec.size(); for(int idx = 0; idx != vec.size(); ++idx, --cnt){ vec[idx] = cnt; } // vec = {4, 3, 2, 1}
-
逗号运算符两侧分别一个表达式,先计算左侧表达式,再将表达式结果丢弃掉,再计算右侧表达式,并将结果作为最终逗号运算符的结果,返回;
// 如果x>y,x自增,y自增,且返回y;反之x自减,且返回x x > y ? ++x, ++y : --x
十一、类型转换
1. 隐式转换-算术转换
-
整型提升:小整数类型转换为较大的整数类型: bool、char、short等小整型,转换为int、long等大整型;
-
int->float->double。
bool flag; short sval; int ival; long lval; float fval; char cval; unsigned short usval; unsigned int uival; unsigned long ulval; double dval; 3.14159L + 'a'; // 'a' -> int -> long double dval + ival; // int -> double dval + fval; // float -> double ival = dval; // double -> int flag = dval; // dval = 0 flag = false else flag = true cval + fval; // char -> int -> float sval + cval; // short -> int, char -> int cval + lval; // char -> int -> long // 下面三种不用管,一般不会把无符号数和有符号数相加 ival + ulval; usval + ival; uival + lval;
2. 其他隐式转换
-
数组转换为指针:大多数用到数组的表达式中,数组自动会转换为数组首元素的指针;
int a[10]; int *p = a; // 数组a转换为数组首元素的指针
-
算术类型或指针可以向布尔类型自动转换;
char *cp = get_string(); if(cp) // 如果指针cp是空指针,返回false,否则返回true if(*cp) // 如果*cp是空字符,返回false,否则返回true
-
字面串字面值转换成string类型;
string s, t = "hello world";
-
while(cin >> a) // 返回cin 再转换为bool类型
while(cin >> a) // 返回cin 再转换为bool类型
3. 显式转换
尽量慎用显示转换(强制类型转换)。
形式:const_type<type>(expression)
其中,expression是需要转换的值,type是需要转换的目标类型,const_type是static_cast、dynamic_cast、const_cast、reinterpret_cast四种。static_cast、dynamic_cast用的最多,const_cast在重载函数中用的多,reinterpret_cast不怎么用。
-
static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast;
int i, j; double c = static_cast<double>(j) / i; // 把int型强制转换为double void* p = &d; // 先把非常量对象的地址保存入void*中,再加那个void*转换为初始的指针类型; double *dp = static_cast<double*>(p); const char *cp; char *p = static_cast<char*>(cp); // 错 static_cast无法转换底层const
-
dynamic_cast: 支持运行时动态类型识别
-
const_cast:只能改变运算对象的底层const,一般可用于去除const性质。
const char *pc; // 底层const char *p = cosnt_cast<char*>(pc); // 把const char* -> char* string s = const_cast<string>(pc); // 错 const_cast只能改变底层cast void *pv; const string *ps; pv = static_cast<void*>(const_cast<string*>(ps)); // const string* -> string* -> void*
-
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。用的少,也注意慎用。