第7篇:C++重载操作符

在C ++中,我们可以使操作符(operator)为用户定义的类在调用层可以像一般运算表达式一样参与运算。 这意味着C ++能够为操作符提供数据类型的特殊含义,这种能力称为操作符重载(operator overloading)。例如,我们可以在string之类的类中重载操作符“ +”,以便仅使用+即可连接两个字符串。

重载“+”操作符

下面我们用一个类似超市的结算小程序来作为本小节的示例
Good类接口

#ifndef GOOD_HH
#define GOOD_HH
#include <string>
class Good
{
    std::string d_name;
    double d_price;

public:
    Good();
    Good(std::string, double);
    double price() const;
    void set_price(double price);
    std::string name() const;
    bool operator<(const Good &obj) const;
    //两个Good实例 1+Good实例2
    Good operator+(Good const &good);
};
#endif

Customer接口

#ifndef CUSTOMER_HH
#define CUSTOMER_HH
#include "../header/good.hh"
#include <iostream>
#include <map>
#include <string>

class Customer
{
    std::string d_fullname; //用户名
    double d_pay;           //应付金额
    double d_bonus;         //奖励积分
    double d_count;         //购买数量
    double d_discnt;        //让利总计
    std::map<Good, double> cart;

public:
    Customer(std::string, double);
    //购买
    void buy(const Good, double);
    //支付
    void payment();
    //结帐信息

    void display_info();

private:
    //折扣
    void discount(const Good &, double);
};
#endif

类实现
下面的重点就是 Good Good::operator+(Good const &obj)的实现,“+”操作符的重载函数内部封装了两个Good类中的d_name字符串的拼接操作,以及两个Good对象的d_price的加法赋值。

Good Good::operator+(Good const &obj)
{
    this->d_price += obj.d_price * 0.95;
    this->d_name = this->d_name + "和" + obj.d_name;
}

调用代码

int main(int argc, char const *argv[])
{
    Good a{"香蕉", 5.7};
    Good b{"奇异果", 13.4};
    Good c{"榴莲", 13.5};
    Good d{"苹果", 5.7};
    Good e{"奶蕉", 7.7};

    Good r = a + e;

    Customer p1{"Lisa", 800};

    p1.buy(a, 23);
    p1.buy(c, 12);
    p1.buy(d, 32);
    p1.buy(r, 45);

    p1.payment();

    p1.display_info();
    return 0;
}

程序输出


因此,我们知道重载操作符实质上就是重载函数,函数内部封装了运算对象(即:对象内部各种数据成员)的对应运算操作。

重载operator[]

除非你是你自己实现类似动态数组的顺序存储结构,才需要重载索引操作符,使用标准库的的顺序容器,重载索引操作符显得有些画蛇添足。

以下是有关[]重载的一些特殊情况

  1. 当我们自己实现的顺序存储结构,需要检查索引越界问题,[]的重载可能很有用
  2. 我们必须在函数中通过引用返回,因为像“ arr [i]”之类的表达式可以用作左值

关于重载下标运算符的具体示例可以看我这篇文章,里面说的很详细
《C++ 数据结构--动态顺序表的实现》

重载operator ++

重载增量运算符(operator)和减量运算符(operator--)会带来一个小问题:每个运算符都有两个版本,因为它们可以用作后缀运算符(例如x)或用作前缀运算符(例如x)。

用作后缀运算符时,该值的对象作为右值,临时const对象返回,且后递增变量本身从视图中消失。 用作前缀运算符时,变量会递增,其值将作为左值返回,并且可以通过修改前缀运算符的返回值来再次更改。 尽管在运算符重载时不需要这些特性,但强烈建议在任何重载的增量或减量运算符中实现这些特性。

假设我们围绕size_t值类型定义一个包装器类。 这样的类可以提供以下(部分显示)接口:

class Customer
{
    size_t d_bonus;         //奖励积分

public:
    Customer(std::string, size_t);
    Customer &operator++();
}

类的最后一个成员声明前缀重载的增量运算符。 返回的左值是Customer&。 该成员很容易实现:

Customer &Customer::operator++()
{
    ++d_bonus;
    return *this;
}

要定义后缀运算符,需要定义该运算符的重载版本,并期望使用(虚拟)int参数。 这可能被认为是错误的,或者是函数重载的可接受的应用程序。 无论您对此有何看法,都可以得出以下结论:

  • 没有参数的重载增量和减量运算符是前缀运算符,应返回对当前对象的引用。
  • 具有int参数的重载增量和减量运算符是后缀运算符,并且应在使用后缀运算符的位置返回一个值,该值是对象的副本。

后缀增量运算符在Customer类的接口中声明如下:

    //后缀增量操作符重载
    Customer operator++(int);

实现如下,请注意,运算符的参数并未使用。 在实现和声明中消除前缀和后缀运算符的歧义只是实现的一部分。

Customer Customer::operator++(int)
{
    Customer tmp{*this};
    ++d_bonus;
    return tmp;
}

在上面的示例中,增加当前对象的语句提供了空位保证,因为它仅涉及对原始类型的操作。 如果初始副本构造抛出异常,则原始对象不会被修改,如果return语句抛出异常,则对象已被安全地修改。 但是增加一个对象本身可能会引发异常。 在这种情况下,如何实现增量运算符? 再次,swap是我们最好的选择。 当数据成员增量执行增量操作可能抛出时,以下是前缀和后缀运算符提供了有力的保证:

重载new操作符和delete操作符

当运算符new重载时,它必须定义一个void *返回类型,并且其第一个参数的类型必须为size_t。 默认运算符new仅定义一个参数,但是重载版本可以定义多个参数。 第一个没有明确指定,但是从重载了new运算符的类的对象的大小推导得出。 在本节中,将讨论重载运算符new。

new和delete操作符的作用域

  • 如果使用某个类的成员函数来重载这些运算符,则意味着这些运算符仅针对该特定类才被重载
  • 如果重载是在类外完成的(即它不是类的成员函数),则只要您使用这些运算符(在类内或类外),都将调用重载的“ new”和“ delete”。 这是全局超载。

new运算符的函数原型

void *operator new(size_t n);

重载的new运算符接收的大小为size_t类型,该大小指定要分配的内存字节数。 重载的new的返回类型必须为void *。重载的函数返回一个指向分配的内存块开头的指针。

delete运算符的函数原型

void operator delete(void*);

该函数接收一个必须删除的void *类型的参数。 函数不应该返回任何东西。
注意:默认情况下,重载的new和delete运算符函数都是静态成员。 因此,他们无权访问此指针。
类接口

class Good
{
    std::string d_name;
    double d_price;

public:
    Good(std::string, double);
    //重载new操作符原型
    void *operator new(size_t n);
    //重载delete操作符
    void operator delete(void *);

    void display();
};

类实现

void *Good::operator new(size_t n)
{
    std::cout << "重载new操作符" << std::endl;
    void *p = ::new Good();
    return p;
}

void Good::operator delete(void *p)
{
    std::cout << "重载delete操作符" << std::endl;
    free(p);
    p = NULL;
}

void Good::display()
{
    std::cout << "品名:" << d_name << std::endl;
    std::cout << "价格:" << d_price << "RMB" << std::endl;
}

调用代码

#include "source/good.cpp"
#include <iostream>

int main(int argc, char const *argv[])
{
    Good *g = new Good("香焦", 10);
    g->display();

    delete g;
    return 0;
}
示例输出

在上面的新重载函数中,是通过new运算符分配了动态内存,但是它是全局的new运算符,否则它内部将递归调用因为new的内容将一次又一次地重载,就像下面的代码

void *p=new Good(); //错误的示例

正确的示例,通过::作用域操作符,从全局调用new操作符

void *p=::new Good();

在全局作用域重载new和delete操作符

void *operator new(size_t n){
     std::cout<<"全局作用域重载new操作符"<<std::endl;
     void *p=malloc(n);
     return p;
}

void operator delete(void *p){
     std::cout<<"全局作用域重载delete操作符"<<std::endl;
      free(p);
      p=NULL;
}

int main(){
      int n=5;
      int *p=new int[5];
      
    for(int i=0;i<n;i++){
        p[i]=i;
    }

   for(size_t i=0;i<n;i++){
        std::cout<<p[i]<<std::endl;
  }

 delete p;
}

在上面的代码中,在new操作符的重载函数中,我们无法使用::new int [5]分配内存,因为它将以递归方式进行。 我们只需要使用malloc分配内存。
输出

全局作用域重载new操作符
0 1 2 3 4
全局作用域重载delete操作符

重载new/delete操作符的原因

  • 重载的new运算符函数可以接受参数; 因此,一个类可以具有多个重载的new运算符的能力。 这使程序员在自定义对象的内存分配方面具有更大的灵活性。 例如:
    void  *operator new(size_t n, 
    
  • 重载的new或delete运算符还为类的对象提供了垃圾回收。
  • 可以在重载的新运算符函数中添加异常处理例程。
  • 提供了重载版本的new和delete操作符能够做一些编译器默认版本的new和delete不能做的自定义操作。 例如,您可能会编写一个自定义运算符delete,以用0覆盖释放的内存,以提高应用程序数据的安全性。
  • 可以在新函数中使用realloc函数动态重新分配内存。
  • 重载的新运算符还使程序员能够从程序中榨取一些额外的性能。 例如,在一个类中,为了加快新节点的分配速度,维护了一个已删除节点的列表,以便在分配新节点时可以重用它们的内存。在这种情况下,重载的delete运算符会将节点添加到列表中 删除的节点和重载的new运算符将从列表中分配内存,而不是从堆中分配内存以加速内存分配。 当删除的节点列表为空时,可以使用堆中的内存。

operator函数和普通函数有什么区别?

  • operator函数与普通函数相同
  • 唯一的区别是,操作符的名称始终是操作符operator关键字,后跟operator关键字的符号,并且在使用相应的操作符时会调用operator函数。
  • 几乎所有的操作符可以重载,但以下C++内置的操作符号是无法重载的。

重载操作符的注意事项

  • 为了使操作符重载起作用,至少一个操作数必须是用户定义的类对象。
  • 赋值操作符:编译器会为每个类自动创建一个默认的赋值操作符。 默认的赋值操作符确实将右侧的所有成员分配到左侧,并且在大多数情况下都可以正常工作(此行为与复制构造函数相同)。 有关更多详细信息,请参见此内容。
  • 转换操作符:我们还可以编写可用于将一种类型转换为另一种类型的转换操作符。

操作员重载规则

  • 仅内置操作符可以重载。 无法创建新的操作符。
  • 操作符的优先级和关联性不能更改。
  • 重载的操作符不能具有默认参数,但函数调用operator() 可以具有默认参数。
  • 不能仅对内置类型重载操作符。 必须至少使用一个操作数定义类型
  • 必须将赋值(=),下标([]),函数调用(“()”)和成员选择(->)操作符定义为成员函数
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349