在重载时,你不能更改 C++ 中内置类型的运算符的含义,只能对自定义类型[1]的运算符进行重载。也就是,运算符两边的操作数至少有一个是自定义的类型。与其他重载函数一样,运算符只能对一组特定类型参数重载一次。
当然,并不是所有的运算符都可以被重载。例如,.
::
sizeof
typeid
,还有唯一的一个三元运算符 ?:
,都是不可以被重载的。
可以被重载的运算符如下:
+
-
*
/
%
+=
-=
*=
/=
%=
;一元前缀运算符 +
-
;一元前缀后缀运算符 ++
--
。&
|
^
<<
>>
&=
|=
^=
<<=
>>=
;一元前缀位操作运算符 ~
。==
!=
<
>
<=
>=
||
&&
;一元前缀布尔操作符 !
。new
new[]
delete
delete[]
。=
[]
->
->*
,
;其它一元前缀运算符 *
&
;还有 n 元的函数调用运算符 ()
。运算符重载是一种特殊的函数。和其它函数一样,运算符重载既可作为成员函数,也可作为非成员函数。
[1] 内置类型和自定义类型的区别,举个例子,前者有 int
char
double
等,后者有 struct
class
enum
union
等,这其中也包括标准库中定义的那些 struct
class
…。
C++ 重载有三个基本规则,
+
号重载成-
号的语义,但这种做法会给别人带来歧义,不建议这么做。+
,那么也应该重载+=
;如果你重载了前置自加++
,那么也应该重载后置自加++
。赋值运算符 =、数组下标运算符 []、成员访问符 -> 和 函数调用运算符 (),只能作为成员函数,因为 C++ 语法就是这么要求的。
其它的运算符可以定义为成员函数,也可以定义为非成员函数。但是有一些你不得不定义成非成员函数,因为它们的左操作数是不可修改的。比如输入输出运算符(<< 和 >>),它们的左操作数是标准流对象(stream),我们无法对其进行修改。
那么这么多运算符,如何选择是作为成员函数还是非成员函数呢?主要基于以下几点准则:
当然,也有一些例外。如果你有一个枚举,
enum Month {Jan, Feb, ..., Nov, Dec}
你想为它重载递加和递减运算符,但是你是无法实现它们为成员函数的,因为在 C++ 中,枚举类型压根就没有成员函数这一说。还有,对于嵌套在类模板中的类模板,operator<() 作为内联成员函数会更方便去读写成员变量,但这种情况不是经常能遇到。
重载运算符的大部分代码都是固定的。这并不奇怪,因为运算符就是语法糖而已,它们完全可以由普通函数完成。但是,确保这些运算符重载的代码执行正确是非常重要的。因为,如果你的代码有 bug,不能编译倒是小事,运行后出现一些奇奇怪怪的 bug 才真的要人命。
赋值运算符 operator=
是一个经常被提及的运算符,需要修改左操作数,应该将其实现为成员函数,可参考 copy-and-swap。下面就简单贴下它的代码,
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
因为左操作数是流对象,且需要访问右操作数的私有成员,所以实现为友元函数最佳。(译注:原回答并没有提及友元,不过我这里还是贴出它的友元实现。)
class T
{
...
friend std::ostream &operator<<(std::ostream &os, const T &obj)
{
// write obj to stream
return os;
}
friend std::istream &operator>>(std::istream &is, T &obj)
{
// read obj from stream
if (/* no valid object of T found in stream */)
is.setstate(std::ios::failbit);
return is;
}
};
函数调用运算符使得可以像调用普通函数一样去调用一个类实例,必须实现为成员函数。它的参数既可以是多个也可以是 0 个。可以看下面的例子,
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
int operator()(int i) {
// ...
}
};
可以这样使用,
foo f;
int a = f("hello");
int b = f(10);
以下的运算符不会修改左右操作数,应实现为非成员函数,
inline bool operator==(const X& lhs, const X& rhs) { /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs) { return !operator==(lhs,rhs); }
inline bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs) { return operator< (rhs,lhs); }
inline bool operator<=(const X& lhs, const X& rhs) { return !operator> (lhs,rhs); }
inline bool operator>=(const X& lhs, const X& rhs) { return !operator< (lhs,rhs); }
译注:在比较时可能还是需要访问其私有成员。如果有
getXXX()
这一类的函数,那么设其为非成员函数就没什么问题;如果没有,设置为友元最佳,这样就可以直接访问私有成员。
||
&&
的用法和上面的一样,但是应用场景很难遇到需要重载这两个的。
最后,一元前缀布尔操作符 !
应该实现为成员函数。
一元前自加和后自加运算符,按照前面所说的基本规则,应该实现为成员函数,
class X
{
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
对于二元算术运算符,不要忘记第三点基本准则:运算符之间往往是有关联的,如果你重载了+
,那么也应该重载+=
;如果你重载了前置自加++
,那么也应该重载后置自加++
。
class X
{
X& operator+=(const X& rhs) // 修改了左操作数
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs) // 未做修改
{
lhs += rhs;
return lhs;
}
数组下标运算符是一个二元运算符,必须需要实现为成员函数。
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
如果 value_type 是一个内建(built-in)类型,直接返回它的拷贝会比常量引用更好,
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const; // if value_type == int/double/char/...
// ...
};
为了定义自己的迭代器或智能指针,你就需要自己重载运算符 *
和 ->
,
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
对于运算符operator->*()
,可参考 https://stackoverflow.com/questions/8777845/overloading-member-access-operators-c。
类型转换运算符可以使两种不同的类型的变量互相转换,有显示转换和隐式转换两种。
隐式转换(C++98/C++03 和 C++11)
隐式转换运算符使编译器可以将用户定义类型的值隐式转换(例如 int 和 long 之间的转换)。以下是一个带有隐式转换运算符的类,
class my_string {
public:
operator const char*() const { return data_; } // This is the conversion operator
private:
const char* data_;
};
隐式转换运算符(看着就像是带有一个参数的构造函数)是用户定义的转换。
void f(const char*);
my_string str;
f(str); // same as f( str.operator const char*() )
看着用起来挺舒服的,但有时候也会出现问题,见下面的代码:因为 my_string()
返回的是一个左值,所以下面的代码只会调用第二个重载。
void f(my_string&);
void f(const char*);
f(my_string());
即使是资深的 C++ 程序员有时候也会在这方面犯错。这个时候显示转换就显得很有必要。
显示转换(C++11)
下面是一个显示转换的示例,
class my_string {
public:
explicit operator const char*() const { return data_; }
private:
const char* data_;
};
注意关键字explicit
。如果你现在再像上面那样去调用就会报错,
prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note: no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note: no known conversion for argument 1 from ‘my_string’ to ‘const char*’
要想正确的使用显示转换,就需要使用static_cast
或 C 风格的类型转换或构造函数T(value)
来作一次转换。就像下面这样,
void f(const char*);
my_string str;
f((const char*)str); // C-style cast
当调用 new 表达式(比如new T(arg)
)的时候,实际上做了两步,
operator new
申请内存同样地,当调用 delete 表达式(比如delete p
),实际上也做了两步,
operator delete
释放内存区C++ 允许我们重载operator new
和operator delete
,以实现我们自己的目的。但是我不推荐去重载它们,除非你有一些性能和内存的需求(译注:问题追踪也是一个需要用到重载的需求)。在一些高性能算法中,它们往往会对其重载以获得对内存的高利用。
C++ 标准库提供的 operator new 和 operator delete 函数是,
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void*) throw();
前面两个作用于一个对象,后面两个用于于一组对象。
如果你提供上述函数的自己的版本,那么你的版本会替换掉标准库中的版本,实际调用的时候会调用你的版本。(译注:重载并不是替换,但对 operator new 和 operator delete 比较特殊,以下我们还是称之为重载)
如果重载了 operator new,那么也应该重载 operator delete;同样,如果重载了 operator new[],那么也应该重载 operator delete[]。
new 运算符负责在堆(heap)中找到足以能够满足要求的内存块。定位 new 运算符是 new 运算符的变体,能够指定要使用的内存位置。
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
C++ 标准库提供的定位 new 和定位 delete 函数是,
void* operator new(std::size_t,void* p) throw(std::bad_alloc);
void operator delete(void* p,void*) throw();
void* operator new[](std::size_t,void* p) throw(std::bad_alloc);
void operator delete[](void* p,void*) throw();
注意,在上面的示例代码中,operator delete 不会被调用,除非 X 的构造函数发生异常(参见:https://en.cppreference.com/w/cpp/memory/new/operator_delete)。
关于定位 new 和 delete 运算符,可参考:
很多时候你需要对内存管理进行一些微调。统计表明,类的示例的频繁的构建和销毁,常规的默认的内存管理处理效率低下,所以需要针对特定类做特定的 new 和 delete。
class my_class {
public:
// ...
void* operator new();
void operator delete(void*,std::size_t);
void* operator new[](size_t);
void operator delete[](void*,std::size_t);
// ...
};
new 和 delete 重载时的行为类似于静态成员函数。对于 my_class 的对象,std::size_t 参数始终为 sizeof(my_class)。
上面已经说过了,重载全局 new 和 delete,其实是替换标准库中的运算符。但是,我们很少需要去重载全局 new 和 delete。