表达式
零.前言
感觉前面那么详细的记录,导致浪费了很多时间,现在可能一篇文章记录一章的重点,所以可能会损失一些简单的信息。
一.基础
1.基础概念
表达式是由一个或多个运算对象
组成,对表达式求值,会得到一个结果
,字面值和变量是最简单的表达式
,用运算符
把一个或多个运算式组合起来,就可以生成比较复杂的表达式。
C++里有一元运算符
、二元运算符
、三元运算符
等多种运算符,如其名,比如&
、*
的作用对象是一个,所以是一元运算符,而==
、*
这些符号作用对象是两个,所以是二元运算符。函数调用也是一种特殊的运算符。
1.1 组合运算符和运算对象
对复杂的表达式,我们要理解运算符的优先级
和结合律
以及运算对象的求值顺序
。
1.2 运算对象转换
一般来说,只要能被转换成同一种类型,就能参与运算。且小整数类型(如short、char)会被提升
为较大的整数类型,比如int。
1.3 重载运算符
重载运算符,可以让用户自定义一些运算符的含义,让它能够作用于飞内置的类型。使用重载运算符的时候,运算对象的类型以及返回值类型都是重载运算符定义的(程序员自己写代码定义),运算符的优先级和结合律是无法=被改变的。
1.4 左值和右值
左右值得概念可能有些抽象,目前可以这样理解:
左值是用的对象的身份(在内存中的位置)
右值是用的对象的值(也就是它的内容)
2.优先级和结合律
复合表达式指含有两个以上运算符的表达式。优先级和结合律决定了表达式中的每个运算符对应的运算对象来自表达式的哪一部分。
一般来说,表达式的组合方式能够决定表达式的最终值。
且括号可以无视优先级和结合律
3.求值顺序
如果没有指定顺序的运算符来说,如果表达式指向并修改了同一个对象,则将会引发错误并产生未定义的行为。有四种运算符明确规定了运算对象的求值顺序:&&
、||
、?:
、,
比如‘&&’运算符,它会先求左侧对象的值,如果左侧对象为真了,就不会计算右侧对象的值。
所以我们在面对复合表达式的时候:
- 在拿不准的时候,用括号来强制把表达式转换成你的需求
- 如果改变了某个对象的值,就不要再其他地方使用这个运算对象
二.算数运算符
对于算数运算符+ - * / %
这些运算符号,都满足左结合律,他们的运算对象和求值结果都是右值。
且他们在运算的时候,可能会得到提升
:
bool b = true;
bool b2 = -b;
cout << b2;
// 输出是1
为啥?因为true是1,在进行-
运算的时候,b2变成了-1,但是-1不是0,再转换成bool的时候,就变成了真,也就是1。
算数运算的异常,我们常见的还有溢出,比如:
short a = 32767;
cout << ++a;
此时输出的a是-32768
对于取余,现在C++不管是正负,一律去除小数点后的部分
三.逻辑和关系运算符
这一节没啥太多说的,但记住一点,如果一个非bool值,最好不要与true
或false
比较
四.赋值运算符
赋值满足右结合律,且赋值的优先程度较低,最好在条件语句中的赋值,打上括号
比如:
int get_value(void)
{
int a;
cin >> a;
return a;
}
int main()
{
int i = 0;
while((i = get_value()) != 42)
{
cout << i << endl;
}
}
五.递增和递减运算符
有两种版本的递增和递减:
++a
或a++
这两者的区别是:++a
是对自身+1,并返回自身
而a++
是先返回一个a的值,然后再对a+1。
所以我们再for里面,最好使用++a,而非a++。使用a++就造成了一种浪费
六.成员访问运算符
简单来说,记住:
ptr->element
等价于(*ptr).element
七.条件运算符
表达式为:
cond ? expr1 : expr2;
如果cond成立,那么执行 expr1,否则执行expr2. 该语句可以嵌套:
string a;
int c;
c = 10;
a = (c < 60) ? "NO" : (c > 90) ? "Excellent" : "Yes";
cout << a << endl;
c = 70;
a = (c < 60) ? "NO" : (c > 90) ? "Excellent" : "Yes";
cout << a << endl;
c = 95;
a = (c < 60) ? "NO" : (c > 90) ? "Excellent" : "Yes";
cout << a << endl;
/*
NO
Yes
Excellent
*/
八.位运算符
C++里有对二进制的支持,有个类叫bitset
,他们也支持二进制
因为位运算会让每个位进行运算,所以推荐使用无符号的类型,以免发生奇怪的现象。
其他的就很常规,如果不懂,可以参考其他地方的内容。
九.sizeof运算符
使用sizeof
会得到一个size_t
的常量表达式,且有一下规则:
- 对
char
或类型为char
的表达式执行siezof
结果为1 - 对引用类型执行,会得到引用类型所占的空间大小
- 对指针执行,会得到指针所占的空间大小,而非其指向对象的大小
- 对解引用指针执行,会得到其指向对象的大小,且不需要该指针是否有效
- 对数组执行,会得到其所占空间的大小,所以如果需要得到一个数组的长度,可以
sizeof(arry) / sizeof(arry[1])
- 对
string
或者vector
执行,会得到其固定部分的大小,而非所有元素所占的空间
十.逗号运算符
对于逗号运算符来说,他会先对左侧的表达式求值,然后把左侧的值丢掉,对右侧表达式求值,并返回右侧的值。
int a = 10, b = 0;
int c;
c = ++a , ++b;
cout << a << ","<< b << ","<< c << ",";
//11,1,11,
十一.类型转换
1.隐式转换
在有些情况下,程序会自动将某些值进行转换,这个过程是自动且无需外部介入的。
- 在大多数表达式里,比
int
类型小的整型,会优先提升为较大的整数 - 在条件中,非布尔类型会换成布尔类型
- 初始化中,初始化转换成变量的类型;赋值语句中,右侧对象会转换成左侧运算对象的类型
- 如果算数运算或者关系运算的对象有多种类型,则会转换成同一种类型。
- 函数调用时,也会发生类型转换
当然,除了这些,还有些特殊的隐式转换,比如数组会转换成首元素的指针等等
2.算数转换
小整型会提升成大整型,叫做整型提升
如果一个是无符号的,一个有符号,且无符号大于带符号类型的,那么带符号的会转换成无符号的
反之,带符号的大于无符号的,则转换结果根据机器而定。
3.显示转换
早期的C语言的强制转换
type (expr)
(type) expr
C++则提供了更多的方法:
cast-name<type>(expr)
其中cast有四种static_cast
dynamic_cast
const_cast
reinterpret_cast
static_cast:只要不包含底层const,就可以用这个来转换,举两个例子:
{
int a = 5, b = 2;
double c = static_cast<double> (a) / b;
cout << c;
//2.5
double a = 3.1415;
void *p = &a;
double *dp = static_cast<double*> (p);
cout << *dp;
// 3.1415
const_cast
可以改变运算对象的底层const
,作用可以理解为“去掉const性质”。
就可以使用该转换,让对象从可读编程可读写。
但如果对象是一个常量,那么就又会产生未定义的后果
reinterpret_cast
这玩意儿就是从很底层的进行类型的强制转换。很危险。
dynamic_cast
后面会提到。
所以,为了安全,尽量少使用显示类型转换而多使用隐式转换
比如我就喜欢:
double a = 1.0 * c / d;(c d是int型)