其实c语言没有引用这个概念,导致很多语言里的细节其实根本就是矛盾的。光光怪初学者领悟不了,其实有些冤枉。
int get_value_at(int* , int);
int x[20];
x[1] = 2;
*(x + 1) = 2;
get_value_at(x, 1) = 2; /// Compiling error!
x[1]是什么类型?*(x + 1)又是什么类型?
c语言玩弄了一个很geeker的技巧,试图用结果去说明原理:
0)左值可放到等号左边,右值不可以。(这条规则被实际挑战的千疮百孔)
1)x[1]和*(x + 1)是一个左值
2)函数的非void返回值是个右值
3)x+1是一个右值
我们要讲道理,不能用行政命令指挥初学者的大脑:凭什么第一点的那几个操作的结果就这么特殊呢?
第一点确实是一个语言中的特例,这种特例在c语言中其实根本是由于语言的自完备性不足造成的:
1)如果把c语言看作是对汇编的封装,那么“变量”是对“一块连续内存的引用”这个概念的封装。
2)c语言封装了细节之后,又要允许人们去访问其中的细节(打破封装),问题就来了。
既然“变量”是对“一块连续内存的引用”,那我现在想访问这块连续内存中的“某一部分连续内存",这其实就是
x[1] = 2;
*(x + 1) = 2;
这一类操作。问题是这部分内存并没有一个变量来封装它们的引用(数组这块内存有变量封装,可惜数组的元素没有变量去封装),所以c语言又试图拿出左值,右值这些大杀器来说明这些场景。初学者不晕才怪。看我的解释:
3)x[1] = 2;可以看作是 int y; y=2; //这2个操作。但是要注意这个临时定义的变量和我们平时定义的变量略有不同:平时定义的变量包含分配内存这个操作(就是它要去封装的那块内存,“定义即分配”),而我们这里定义的y不分配内存,仅仅用来封装已经分配好的x[1]这块内存的引用。y是左值!
4)struct {int _0; int _1;} x; x._1 = 2; //这里的x._1和上面的x[1]其实也是一个操作:访问一个连续内存块(这里由数组换成了结构体)的部分内存(由通过下标访问改为了通过成员变量访问)的引用。
5)上面的y是左值还是右值取决于x是左值还是右值。部分服从整体,很好明白吧。
6)c++解除了对引用这个概念的封装,我们终于可以给3)中的y一个合法的语法身份并且完美的解释y的行为了:int &y = x[1]; y = 2; //y不会引起分配内存操作而且y只能固定的引用x[1]。
7)c++明确的把左值和右值划分开了:一个值要么是右值,要么是左值。左值可以取地址,右值不可以。引用类型既然引用的是内存,当然可以取地址,一定是左值,哪怕是做函数的返回值。当然最新的c++ 11草案又扩充了引用的概念,引用也可以是右值,暂且不提。
好了,好事多磨。终于到了本文想说的话了。像x[1],*(x+1)这些特殊的操作有哪些呢,他们有什么特征呢?
返回值是一个引用,是一个左值。
That's it! 我们用事实说话,观察stl中重载运算符的结果,归纳出这些特殊的运算符。
1.前置的++, --
2.=, +=, -=, *=, /= , &=, |=, ^=, <<=, >>=
3.[], *
4. , (comma, 逗号运算符,在boost的序列初始化等等一组件中可以见到它的重载)
5.不允许重载的运算符: . (dot, 点号运算符,访问结构体成员的 )
实际上->操作返回的依然是一个右值,一个指针的拷贝。
如果以后你重载运算符,一定要记得这些哦。