首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >运算符重载的基本规则和习惯用法是什

运算符重载的基本规则和习惯用法是什

作者头像
ClearSeve
发布2022-02-10 18:47:06
发布2022-02-10 18:47:06
96900
代码可运行
举报
文章被收录于专栏:ClearSeveClearSeve
运行总次数:0
代码可运行

运算符重载的通用语法

在重载时,你不能更改 C++ 中内置类型的运算符的含义,只能对自定义类型[1]的运算符进行重载。也就是,运算符两边的操作数至少有一个是自定义的类型。与其他重载函数一样,运算符只能对一组特定类型参数重载一次。

当然,并不是所有的运算符都可以被重载。例如,. :: sizeof typeid,还有唯一的一个三元运算符 ?:,都是不可以被重载的。

可以被重载的运算符如下:

  • 二元算术运算符 + - * / % += -= *= /= %=;一元前缀运算符 + -;一元前缀后缀运算符 ++ --
  • 二元位操作运算符 & | ^ << >> &= |= ^= <<= >>=;一元前缀位操作运算符 ~
  • 二元布尔操作运算符 == != < > <= >= || &&;一元前缀布尔操作符 !
  • 内存管理运算符 new new[] delete delete[]
  • 隐式转换运算符。
  • 其它二元运算符 = [] -> ->* ,;其它一元前缀运算符 * &;还有 n 元的函数调用运算符 ()

运算符重载是一种特殊的函数。和其它函数一样,运算符重载既可作为成员函数,也可作为非成员函数。

[1] 内置类型和自定义类型的区别,举个例子,前者有 int char double 等,后者有 struct class enum union 等,这其中也包括标准库中定义的那些 struct class …。

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

C++ 重载有三个基本规则,

  1. 如果一个运算符的含义不是很清楚的时候,它就不应该被重载。 如果非要这样的话,倒不如直接提供一个函数来实现你想要的功能。
  2. 始终重载运算符众所周知的语义。 C++ 对运算符重载的语义并没有限制,意思是你可以对+号重载成-号的语义,但这种做法会给别人带来歧义,不建议这么做。
  3. 始终提供一组相关的操作。 运算符之间往往是有关联的,如果你重载了+,那么也应该重载+=;如果你重载了前置自加++,那么也应该重载后置自加++

成员函数与非成员函数的选择

赋值运算符 =、数组下标运算符 []、成员访问符 -> 和 函数调用运算符 (),只能作为成员函数,因为 C++ 语法就是这么要求的。

其它的运算符可以定义为成员函数,也可以定义为非成员函数。但是有一些你不得不定义成非成员函数,因为它们的左操作数是不可修改的。比如输入输出运算符(<< 和 >>),它们的左操作数是标准流对象(stream),我们无法对其进行修改。

那么这么多运算符,如何选择是作为成员函数还是非成员函数呢?主要基于以下几点准则:

  1. 如果是一元运算符,就实现为成员函数。
  2. 如果是二元运算符,且不会修改其左右操作数,则实现为非成员函数。
  3. 如果是二元运算符,且会修改其左/右操作数(一般都是左),则实现为成员函数,因为一般你都需要访问其私有成员。

当然,也有一些例外。如果你有一个枚举,

代码语言:javascript
代码运行次数:0
运行
复制
enum Month {Jan, Feb, ..., Nov, Dec}

你想为它重载递加和递减运算符,但是你是无法实现它们为成员函数的,因为在 C++ 中,枚举类型压根就没有成员函数这一说。还有,对于嵌套在类模板中的类模板,operator<() 作为内联成员函数会更方便去读写成员变量,但这种情况不是经常能遇到。

普通运算符重载的用法

重载运算符的大部分代码都是固定的。这并不奇怪,因为运算符就是语法糖而已,它们完全可以由普通函数完成。但是,确保这些运算符重载的代码执行正确是非常重要的。因为,如果你的代码有 bug,不能编译倒是小事,运行后出现一些奇奇怪怪的 bug 才真的要人命。

赋值运算符

赋值运算符 operator= 是一个经常被提及的运算符,需要修改左操作数,应该将其实现为成员函数,可参考 copy-and-swap。下面就简单贴下它的代码,

代码语言:javascript
代码运行次数:0
运行
复制
X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

输入输出运算符

因为左操作数是流对象,且需要访问右操作数的私有成员,所以实现为友元函数最佳。(译注:原回答并没有提及友元,不过我这里还是贴出它的友元实现。)

代码语言:javascript
代码运行次数:0
运行
复制
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 个。可以看下面的例子,

代码语言:javascript
代码运行次数:0
运行
复制
class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
    int operator()(int i) {
        // ...
    }
};

可以这样使用,

代码语言:javascript
代码运行次数:0
运行
复制
foo f;
int a = f("hello");
int b = f(10);

比较运算符

以下的运算符不会修改左右操作数,应实现为非成员函数,

代码语言:javascript
代码运行次数:0
运行
复制
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()这一类的函数,那么设其为非成员函数就没什么问题;如果没有,设置为友元最佳,这样就可以直接访问私有成员。

|| && 的用法和上面的一样,但是应用场景很难遇到需要重载这两个的。

最后,一元前缀布尔操作符 !应该实现为成员函数。

算术运算符

一元前自加和后自加运算符,按照前面所说的基本规则,应该实现为成员函数,

代码语言:javascript
代码运行次数:0
运行
复制
class X
{
  X& operator++()
  {
    // do actual increment
    return *this;
  }

  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

对于二元算术运算符,不要忘记第三点基本准则:运算符之间往往是有关联的,如果你重载了+,那么也应该重载+=;如果你重载了前置自加++,那么也应该重载后置自加++

代码语言:javascript
代码运行次数:0
运行
复制
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;
}

数组下标

数组下标运算符是一个二元运算符,必须需要实现为成员函数。

代码语言:javascript
代码运行次数:0
运行
复制
class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

如果 value_type 是一个内建(built-in)类型,直接返回它的拷贝会比常量引用更好,

代码语言:javascript
代码运行次数:0
运行
复制
class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const; // if value_type == int/double/char/...
  // ...
};

仿指针操作

为了定义自己的迭代器或智能指针,你就需要自己重载运算符 *->

代码语言:javascript
代码运行次数:0
运行
复制
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 之间的转换)。以下是一个带有隐式转换运算符的类,

代码语言:javascript
代码运行次数:0
运行
复制
class my_string {
public:
  operator const char*() const { return data_; } // This is the conversion operator
private:
  const char* data_;
};

隐式转换运算符(看着就像是带有一个参数的构造函数)是用户定义的转换。

代码语言:javascript
代码运行次数:0
运行
复制
void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

看着用起来挺舒服的,但有时候也会出现问题,见下面的代码:因为 my_string() 返回的是一个左值,所以下面的代码只会调用第二个重载。

代码语言:javascript
代码运行次数:0
运行
复制
void f(my_string&);
void f(const char*);

f(my_string());

即使是资深的 C++ 程序员有时候也会在这方面犯错。这个时候显示转换就显得很有必要。

显示转换(C++11)

下面是一个显示转换的示例,

代码语言:javascript
代码运行次数:0
运行
复制
class my_string {
public:
  explicit operator const char*() const { return data_; }
private:
  const char* data_;
};

注意关键字explicit。如果你现在再像上面那样去调用就会报错,

代码语言:javascript
代码运行次数:0
运行
复制
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)来作一次转换。就像下面这样,

代码语言:javascript
代码运行次数:0
运行
复制
void f(const char*);

my_string str;
f((const char*)str); // C-style cast

operator new 和 operator delete

基础部分

当调用 new 表达式(比如new T(arg))的时候,实际上做了两步,

  1. 调用operator new申请内存
  2. 调用 T 的构造函数初始化内存区

同样地,当调用 delete 表达式(比如delete p),实际上也做了两步,

  1. 调用该对象的析构函数
  2. 调用operator delete释放内存区

C++ 允许我们重载operator newoperator delete,以实现我们自己的目的。但是我不推荐去重载它们,除非你有一些性能和内存的需求(译注:问题追踪也是一个需要用到重载的需求)。在一些高性能算法中,它们往往会对其重载以获得对内存的高利用。

C++ 标准库提供的 operator new 和 operator delete 函数是,

代码语言:javascript
代码运行次数:0
运行
复制
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(Placement new)

new 运算符负责在堆(heap)中找到足以能够满足要求的内存块。定位 new 运算符是 new 运算符的变体,能够指定要使用的内存位置。

代码语言:javascript
代码运行次数:0
运行
复制
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

C++ 标准库提供的定位 new 和定位 delete 函数是,

代码语言:javascript
代码运行次数:0
运行
复制
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

很多时候你需要对内存管理进行一些微调。统计表明,类的示例的频繁的构建和销毁,常规的默认的内存管理处理效率低下,所以需要针对特定类做特定的 new 和 delete。

代码语言:javascript
代码运行次数:0
运行
复制
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,其实是替换标准库中的运算符。但是,我们很少需要去重载全局 new 和 delete。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年1月24日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 运算符重载的通用语法
  • 运算符重载的三个基本规则
  • 成员函数与非成员函数的选择
  • 普通运算符重载的用法
    • 赋值运算符
    • 输入输出运算符
    • 函数调用运算符
    • 比较运算符
    • 算术运算符
    • 数组下标
    • 仿指针操作
    • 转换运算符
  • operator new 和 operator delete
    • 基础部分
    • 定位 new(Placement new)
    • 特定于类的 new 和 delete
    • 全局的 new 和 delete
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档