智能指针中C++重载'->'符号是怎么实现的
例如下面的代码:
class StrPtr{
public:
StrPtr() : _ptr(nullptr){}
//拷贝构造函数等省略...
std::string* operator->()
{
return _ptr;
}
private:
std::string *_ptr;
};
std::string* operator->()
/*
这句代码的格式不是类似于前导运算符吗?类似于std::string operator*(){//...}不正是*ptr?但是重载的->运算符却是ptr->这样使用的,请问是为什么?
而且std::string* operator->()返回的是指针,为什么可以直接在后面访问类成员,[比如说ptr->size()]?
我的疑问是这是如何实现的,这和我对运算符重载的直接理解有所差异。
*/
解答:
这句代码的格式不是类似于前导运算符吗?类似于
std::string operator*(){//...}
不正是*ptr
?但是重载的->
运算符却是ptr->
这样使用的,请问是为什么?
因为运算符的结合律不同,dereference operator(*
)是 right associative 的,而 member access operator(.
和->
)是 left associative 的。这是不同符号的结合律不同的例子,C++ 里符号相同的时候也会有用法不同导致结合律不同的例子。例如:
operator | left associative | right associative
| lhs op rhs / lhs op | op rhs
----------+-----------------------------+---------------------------
++ -- | postfix increment/decrement | prefix increment/decrement
+ - | binary add/subtract | unary plus/minus
* | binary multiply | dereference
& | bitwise and | address-of
() | function call | type conversion
当你重载一个左结合律的操作符时(如+ - * / ()
等),往往这个操作符是个二元操作符,对于lhs op rhs
,就会调用lhs.operator op(rhs)
或operator op(lhs, rhs)
,我们只需要按照这个函数签名来重载操作符就行了。
但是,如果这个操作符是一元操作符怎么办?这就要分情况讨论了:
-
如果这个符号有一种以上的用法(比如
++
或--
),对于lhs op
(左结合)和op rhs
(右结合),我们得想个办法区分不同的用法啊,所以就会出现用于占位的函数参数(int
):-
对于
lhs op
调用lhs.operator op(int)
-
对于
op rhs
调用rhs.operator op()
-
-
如果这个符号只有一种用法(比如
->
),对于lhs op
,那就不需要用于占位的函数参数了,可以直接写成类似右结合的函数签名,即-
对于
lhs op
调用lhs.operator op()
-
所以会让人有点糊涂为啥函数签名差不多,用法却不同。
而且
std::string* operator->()
返回的是指针,为什么可以直接在后面访问类成员,[比如说ptr->size()
]?
这是由 C++ 标准规定的,对于ptr->mem
根据ptr
类型的不同,操作符->
的解释也不同:
-
当
ptr
的类型是内置指针类型时,等价于(*ptr).mem
-
当
ptr
的类型是类时,等价于ptr.operator->()->mem
你会发现这是一个递归的解释,对于ptr->mem
会递归成:
(*(ptr.operator->().operator->().….operator->())).mem
操作符->
是一元的,而操作符.
是二元的。操作符->
最终是通过操作符.
来访问成员的,而.
这个操作符是不允许重载的,只能由编译器实现。
举个例子,使用题主定义的类StrPtr
:
string s = "abc";
StrPtr ptr(&s);
string *sp = &s;
ptr->size();
// 等价于 ptr.operator->()->size();
// 等价于 _ptr->size(); 这跟 sp->size(); 不就一样了吗
// 等价于 (*_ptr).size(); 这跟 (*sp).size(); 不就一样了吗
最后一句题外话,C++ 变量名不要用下划线作为起始,用下划线做起始是保留给编译器使用的。可以使用m_somemember
或somemember_
作为私有成员名称。