目录
一,表达式分词
如何理解a---b?是a - --b还是a-- - b?
这里就涉及到编译原理中的分词原则,分词是从左往右最长匹配的贪心原则,
根据贪心原则,a--b就是a-- b,是无法编译的,无法理解为a - -b,因为先分词后才能按照高级语言的语法来编译。
而a---b就是a-- - b
有了这个原则,表达式想表达的意思就没有歧义了。
比如---b这个表达式,就是-- -b,这也是无法编译的。
分词之后,求解表达式的顺序,取决于运算符的优先级和结合性。
二,运算符
1,优先级
运算符的优先级,和数学中的运算优先级意思一样,比如乘法、除法的优先级比加法、减法的优先级高。
然而,--的优先级比-高,但是a-- -b是先计算a-b,再计算a--,这怎么理解呢?
我的理解是,运算符的优先级和结合性,其实是规定了如何添加括号,而无论如何添加括号,后置++和--依旧是在表达式计算完成之后才能计算。
顺带一提,a--+-+-b这个表达式如何理解?
答案是a-- + - + -b,加减号放在变量之前,也可以表示正负号,不难发现,这2个不需要什么规则做明显的区分,这个是没有歧义的。
和数学是一致是,3-5可以理解为3和-5这2个数的和,因为负数的定义就是如此,而且数学中也有3- -5和3- +5这种表达,是完全一致的,无论是数学还是C/C++中,这个都是没有歧义的。
2,结合性
如果两个运算符的优先级一样,比如 加法和加法,或者,加法和减法,那么就需要根据结合性确定运算顺序。
比如加减法的优先级相同,具有左结合性,a+b-c就是先算a+b,a-b+c就是先算a-b
优先级相同的运算符,要么全是左结合性,要么全是右结合性。
实际上,右结合性只有其中的三个等级,即三类运算符:一元运算符、三元运算符、赋值运算符。
3,运算顺序
运算符的优先级和结合性,相当于是规定了如何添加括号,而不是规定运算顺序。
比如,(a+b)*(c+d),是先计算a+b还是先计算c+d呢?这个运算顺序不影响结果,但是(a=b)+(b=a)呢?这个运算顺序就影响结果了。
同级表达式的运算顺序,是未定义的行为,具体顺序由编译器决定。
不难发现,同级表达式的计算顺序会影响结果的,有3类:
(1)func1+func2,2个函数的执行顺序可能会有影响
(2)含赋值运算符的表达式,比如(a=b)+(b=a)
(3)含自增运算符++或者自减运算符--的表达式
4,自增和自减
(1)分词
分词是从左往右最长匹配的贪心原则,a---b就是a-- - b
(2)运算顺序
超脱于优先级和结合性的规律之外,自增一定是先于整个表达式,自减一定是后于整个表达式
(3)左值、右值
++a和--a是左值,a++和a--是右值
所以,++(--a)合法,(++a)-- 合法,++(a--) 不合法,(a++)-- 不合法
所以,++--a合法,++a-- 不合法,a++-- 不合法
(4)函数传参中的自增自减
比如func(a++,++a),应该是未定义的顺序。
(5)cin和cout中的自增自减
int a=1;
cout<<a<<a++;
输出:21
怎么理解这个式子呢?
cout是一个对象,它重载了左移运算符<<,返回的是this指针指向的本身这个cout对象
因为重载是不改变优先级和结合性的,所以上述代码相当于
int a=1;
(cout<<a)<<(a++);
这应该也是未定义行为,但是编译器拓展支持了。
我猜测,这个可能是先把a++作为参数传进去了,再把a作为参数传进去了。
三,运算符重载
1,运算符重载
运算符重载的方法是在把 “operator加上操作符” 类似于函数来重载。
重载之后还是可以像普通函数一样调用,也可以简写,直接用操作符就行。
示例:
#include<iostream>
using namespace std;
class A
{
public:
int operator+(int b)
{
return a + b + 1;
}
A(int a)
{
this->a = a;
}
private:
int a;
};
int main()
{
cout << A(2).operator+(3);
cout << endl << A(4) + 5;
return 0;
}
输出结果:
6
10
2,重载限制
(1)c++允许重载大部分运算符,但Scope resolution、Member access、Pointer-to-member后面接的是变量名而不是变量值,所以无法重载,还有三元运算符及一些关键字运算符也不能重载,其他的一元运算符和所有二元运算符都可以重载。
(2)不允许创建新的运算符。
(3)不能改变运算符操作的操作对象数,比如二元运算符重载之后还是二元运算符。
(4)不能改变运算符的优先级和结合性。
(5)不能改变语法规则。
(6)非成员运算符要求类类型或者枚举类型的参数,即运算符的操作对象中至少有一个是用户自定义类型。
(7)由以上规则推导出的规则,比如重载运算符的函数不能有默认参数,否则就会违反(3)
3,普通函数和类成员函数
运算符重载也区分普通函数和类成员函数。
大部分运算符可以重载为两种形式,只有几个运算符是只能重载成成员函数,包括Function call()
以加法为例,基本类型的加法是已经定义的,用户自定义类的加法可以自定义重载。
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
{
this->a = a;
}
private:
int a;
};
class B
{
public:
int operator+(A a)
{
return b;
}
B(int b)
{
this->b = b;
}
private:
int b;
};
int main()
{
//cout << A(2) + B(3);
cout << B(3) + A(2);
return 0;
}
因为结合性指明了 + 只能是左边对象的成员函数,不能是右边对象的成员函数。
int operator+ (A a, B b)
{
return 5;
}
int main()
{
cout << A(2) + B(3);
return 0;
}
加了普通函数,就可以了。
普通函数和成员函数之间的重载:
int operator+ (B b, A a)
{
return 10;
}
int main()
{
//cout << B(3) + A(2);
return 0;
}
2个函数都可以调用,优先级一样,编译失败。
PS:
因为是类似于函数重载,所以返回类型是不限制的。
4,语法规则
运算符重载不能改变语法规则,包括操作符和操作对象的位置关系、运算符函数的参数的数量和顺序。
(4.1)一元运算符
类成员函数没有参数,普通函数有一个参数。
类成员函数:
class A
{
public:
int operator- ()
{
return -a;
}
private:
int a=5;
};
int main()
{
A a;
cout << -a;
return 0;
}
普通函数:
class A
{
int a=5;
};
int operator- (A a)
{
return 0;
}
int main()
{
A a;
cout << -a;
return 0;
}
自增自减运算符是个例外,只有自增自减运算符存在2种语法,前缀式和后缀式的语法不同。
前缀自增:
class A
{
public:
void operator++ ()
{
cout << 123;
}
};
int main()
{
A a;
++a;
return 0;
}
后缀自增:
class A
{
public:
void operator++ (int)
{
cout << 123;
}
};
int main()
{
A a;
a++;
return 0;
}
这里的int参数纯粹是用来表示这是后缀++
(4.2)二元运算符重载
只有二元运算符的重载涉及参数位置的问题。
二元运算符的2个操作对象,都是在运算符的一左一右,对于非成员函数,按照左右顺序对应,对于类成员函数,当前对象对应左边,函数参数对应右边。
对于左结合性的,上面有+的例子。
对于右结合性的,以+=为例:
class A
{
};
class B
{
public:
void operator+= (A a)
{
cout << 123;
}
};
int main()
{
A a;
B b;
// a += b;
b += a;
return 0;
}
所以不管结合性,二元运算符都是调用左边对象的成员函数,或调用普通函数:
class A
{
};
class B
{
};
void operator+= (A a, B b)
{
cout << 123;
}
int main()
{
A a;
B b;
a += b;
// b += a;
return 0;
}
四,几个特殊的运算符
- ::是优先级最高的,也是唯一比括号()还高的。
- ,是优先级最低的。
- ()也可以重载,即仿函数。
- ++和--的特殊性上面有提到。
- <<和>>也有点特殊,有位运算和流操作符两种语义。
注意,++和--作为前缀和后缀是两种语法,也是两种语义,<<和>>表示位运算和流操作符虽然是两种语义,但是是同一个语法,换句话说,前缀和后缀是2个不同的运算符,而表示位运算和流操作符的是同一个运算符,只是重载的功能不同。