百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

运算符重载的基本规则和习语是什么

liebian365 2024-10-31 15:16 15 浏览 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. 如果是二元运算符,且会修改其左/右操作数(一般都是左),则实现为成员函数,因为一般你都需要访问其私有成员。

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

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 个。可以看下面的例子,
 
```c++
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

operator new 和 operator delete

基础部分

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

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

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

  1. 调用该对象的析构函数
  2. 调用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(Placement new)

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 运算符,可参考:

  • https://stackoverflow.com/questions/3675059/how-could-i-sensibly-overload-placement-operator-new
  • https://isocpp.org/wiki/faq/dtors#placement-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,其实是替换标准库中的运算符。但是,我们很少需要去重载全局 new 和 delete。



相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: