c++ - 重载比较运算符 - 运算符重载的基本规则和习惯用法是什么?




重载函数运算符 (5)

C ++中运算符重载的一般语法

您不能在C ++中为内置类型更改运算符的含义,只能为用户定义的类型重载运算符1 。 也就是说,至少有一个操作数必须是用户定义的类型。 与其他重载函数一样,运算符只能为一组参数重载一次。

并不是所有的操作符都可以用C ++重载。 无法超载的运营商包括: . :: sizeof typeid .*和C ++中唯一的三元运算符, ?:

在C ++中可以重载的操作符包括:

  • 算术运算符: + - * / %+= -= *= /= %= (所有二进制中缀); + - (一元前缀); ++ -- (一元前缀和后缀)
  • 位操作: & | ^ << >>&= |= ^= <<= >>= (所有二进制中缀); ~ (一元前缀)
  • 布尔代数: == != < > || >= || && (全部二进制中缀); ! (一元前缀)
  • 内存管理: new new[] delete delete[]
  • 隐式转换运算符
  • miscellany: = [] -> ->* , (all binary infix); * & (所有一元前缀) () (函数调用,n元中缀)

然而,你可以重载所有这些的事实并不意味着你应该这样做。 请参阅运算符重载的基本规则。

在C ++中,运算符以具有特殊名称函数的形式被重载。 与其他函数一样,重载运算符通常可以作为其左操作数类型 的成员函数或作为非成员函数来实现 。 无论你是自由选择还是使用其中一种,都取决于几个标准。 2应用于对象x的一元运算符@ 3被调用为[email protected](x)[email protected]() 。 应用于对象xy的二进制中缀运算符@被称为[email protected](x,y)[email protected](y)4

作为非成员函数实现的操作符有时是操作数类型的朋友。

1 术语“用户定义”可能会有些误导。 C ++区分了内置类型和用户定义类型。 前者属于例如int,char和double; 到后者属于所有结构,类,联合和枚举类型,包括来自标准库的类型,即使它们不是由用户定义的类型。

2 本FAQ 的后面部分将对此进行介绍。

3 @不是C ++中的有效运算符,因此我将它用作占位符。

4 C ++中唯一的三元运算符不能被重载,并且唯一的n元运算符必须始终作为成员函数来实现。

继续以C ++运算符重载的三个基本规则

注意:答案是按照特定的顺序给出的,但是由于许多用户根据投票分类答案,而不是按照给定的时间排列答案,因此下面是答案索引,按其最有意义的顺序排列:

(注意:这意味着要成为Stack Overflow的C ++常见问题解答的入口,如果您想批评在此表单中提供常见问题解答的想法,那么启动所有这些工作的meta上的贴子将成为这样做的地方。该问题在C ++聊天室中进行监控,常见问题解决方案首先出现在C ++聊天室中,因此您的答案很可能会被提出这一想法的人阅读。)


C ++中运算符重载的三个基本规则

当涉及到C ++中的运算符重载时, 应遵循三条基本规则 。 与所有这些规则一样,确实有例外。 有时人们偏离了他们,结果并不是错误的代码,但是这种积极的偏差却很少。 至少,我看到的100个这样的偏差中有99个是不合理的。 但是,它也可能是1000个中的999个。所以你最好遵守以下规则。

  1. 每当操作员的含义不明确且无可争议时,不应超载。 相反,提供一个精心挑选名称的功能。
    基本上,超载运营商的首要原则就是: 不要这样做 。 这看起来很奇怪,因为有很多关于操作符重载的知识,所以很多文章,书籍章节和其他文章都涉及这些。 但尽管有这些看似明显的证据, 但只有极少数情况下运营商超载是适当的 。 原因在于实际上很难理解运算符应用背后的语义,除非在应用程序域中使用运算符是众所周知且无可争议的。 与普遍的看法相反,这种情况几乎不存在。

  2. 始终坚持运营商众所周知的语义。
    C ++对重载运算符的语义没有限制。 您的编译器会高兴地接受实现了二元运算符的代码,从其右操作数中减去。 但是,这样的操作员的用户绝不会怀疑表达式a + bb减去b 。 当然,这假设应用程序域中操作符的语义是无可争议的。

  3. 始终提供一整套相关的操作。
    运营商彼此之间以及与其他运营相关。 如果你的类型支持a + b ,用户也期望能够调用a += b 。 如果它支持前缀增量++a ,他们也会期望a++能工作。 如果他们可以检查a < b ,他们肯定会期望也能够检查a > b 。 如果他们可以复制构建您的类型,他们希望分配工作。

继续进行会员与非会员之间的决定


转换运营商(也称为用户定义的转化)

在C ++中,您可以创建转换运算符,这些运算符允许编译器在类型和其他定义的类型之间进行转换。 有两种类型的转换运算符,即隐式和显式运算符。

隐式转换运算符(C ++ 98 / C ++ 03和C ++ 11)

隐式转换运算符允许编译器将用户定义类型的值隐式转换(如intlong之间的转换)为某种其他类型。

以下是一个带有隐式转换运算符的简单类:

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*() )

起初这看起来很有帮助,但问题在于,当不期望的时候,隐式转换甚至会启动。 在下面的代码中, void f(const char*)将被调用,因为my_string()不是lvalue ,所以第一个不匹配:

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风格转换或构造函数风格cast(即T(value) )。

但是,有一个例外:编译器允许隐式转换为bool 。 此外,编译器在转换为bool (允许编译器一次执行2个隐式转换,但最多只有1个用户定义的转换)后不允许执行另一个隐式转换。

由于编译器不会投射“过去” bool ,显式转换运算符现在不需要安全布尔成语 。 例如,C ++ 11之前的智能指针使用安全Bool惯用法来防止转换为整型。 在C ++ 11中,智能指针使用显式运算符,因为编译器在将类型显式转换为布尔型之后,不允许隐式转换为整型。

继续重载newdelete


重载newdelete

注意:这只涉及重载newdelete语法 ,而不涉及这样的重载操作符的实现 我认为重载newdelete的语义应该得到他们自己的常见问题解答 ,在运算符重载的话题中,我永远不能做正义。

基本

在C ++中,当你编写一个像new T(arg)这样的新表达式时,在计算这个表达式时会发生两件事:第一个operator new被调用来获得原始内存,然后调用T的相应构造函数将这个原始内存变成有效的对象。 同样,当你删除一个对象时,首先调用它的析构函数,然后内存返回给operator delete
C ++允许您调整这两种操作:内存管理和在分配的内存中构建/销毁对象。 后者是通过编写一个类的构造函数和析构函数完成的。 微调内存管理是通过编写你自己的operator newoperator delete

操作符重载的第一个基本规则 - 不要这样做 - 特别适用于重载newdelete 。 几乎导致这些操作符过载的唯一原因是性能问题内存限制 ,并且在许多情况下,其他操作(如对所用算法的更改)将提供比试图调整内存管理更高的成本/增益比

C ++标准库附带一组预定义的new操作符和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 delete ,该operator delete与调用分配内存以创建对象的operator new相匹配。如果执行了不提供匹配的operator delete ,默认的调用,这几乎总是错误的。
如果你重载newdelete ,你应该考虑重载数组变体。

安置new

C ++允许新的和删除操作符采取额外的参数。
所谓的placement new允许您在传递给某个地址的某个地址创建一个对象:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

标准库为此提供了新的和删除操作符的适当重载:

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(); 

请注意,在上面给出的用于放置new的示例代码中,除非X的构造函数抛出异常,否则不会调用operator delete

您也可以使用其他参数重载newdelete 。 与放置new的附加参数一样,这些参数也在关键字new后的括号内列出。 仅仅因为历史原因,这样的变体通常也被称为放置新的,即使他们的论点不是用于将对象放置在特定地址。

特定于类的新建和删除

最常见的情况是,您需要微调内存管理,因为测量显示特定类或一组相关类的实例经常创建和销毁,并且运行时系统的默认内存管理调整为general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

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);
    // ... 
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class , the std::size_t argument will always be sizeof(my_class) . However, these operators are also called for dynamically allocated objects of derived classes , in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.


Why can't operator<< function for streaming objects to std::cout or to a file be a member function?

Let's say you have:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Given that, you cannot use:

Foo f = {10, 20.0};
std::cout << f;

Since operator<< is overloaded as a member function of Foo , the LHS of the operator must be a Foo object. Which means, you will be required to use:

Foo f = {10, 20.0};
f << std::cout

which is very non-intuitive.

If you define it as a non-member function,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

You will be able to use:

Foo f = {10, 20.0};
std::cout << f;

which is very intuitive.





c++-faq