美文网首页IT狗工作室
第7篇:C++重载操作符

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

作者: 铁甲万能狗 | 来源:发表于2020-10-13 09:11 被阅读0次

    在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() 可以具有默认参数。
    • 不能仅对内置类型重载操作符。 必须至少使用一个操作数定义类型
    • 必须将赋值(=),下标([]),函数调用(“()”)和成员选择(->)操作符定义为成员函数

    相关文章

      网友评论

        本文标题:第7篇:C++重载操作符

        本文链接:https://www.haomeiwen.com/subject/zhmyuctx.html