C++之常引用对象只能调用常成员函数、重载为成员函数和友元函数的区别

        近日写了一道题,遇到了新bug,使博主对C++的逻辑严密性有了更深的体会,特写此博客。本文标题是对两个bug的总结,即本文内容分为两部分。

        题目如下:

        设计一个CShape抽象类,类中包含纯虚函数

        从CShape类派生出三角形类CTriangle、矩形类CRectangle和椭圆类CEllipse

        使用一个公共接口计算三角形对象、矩形对象和椭圆对象的面积

        重载运算符>用于判断两个形状面积的大小,返回true或false

        阅读完题目,显然CShape类中至少要包含计算面积的纯虚函数,再在派生出的形状类中重写计算面积的函数即可。题目最后一个要求是重载比较运算符>来实现判断两个形状面积的大小,由于形状具有随机性,且C++中允许类型兼容(即子类对象可以当做父类使用,子类可以赋给父类引用或父类指针),所以重载该运算符应该在基类CShape中进行,这样就可以实现对任意两个派生类对象进行比较。最后,每个派生类中都要添加足够描述形状特征的数据成员,只要有几何基础知识就知道该怎么做,这里就不赘述。根据以上分析,我初步写的代码如下:

#include<iostream>
#include<math.h>

using namespace std;

#define Pi 3.14159

class CShape//抽象基类
{
public:
    virtual double area()=0;
    friend bool operator>(const CShape &a,const CShape &b)
    {
        return a.area()>b.area();
    }
};

class CTriangle:public CShape//三角形类
{
private:
    //三条边长,半周长
    double a,b,c,p;
public:
    CTriangle(double m,double n,double l)
    {
        a=m;
        b=n;
        c=l;
        p=(a+b+c)/2;
    }
    double area()
    {
        return sqrt(p*(p-a)*(p-b)*(p-c));
    }
};

class CRectangle:public CShape//矩形类
{
private:
    //长,宽
    double a,b;
public:
    CRectangle(double m,double n)
    {
        a=m;
        b=n;
    }
    double area()
    {
        return a*b;
    }
};

class CEllipse:public CShape//椭圆形类
{
private:
    //半长轴,半短轴
    double a,b;
public:
    CEllipse(double m,double n)
    {
        a=m;
        b=n;
    }
    double area()
    {
        return Pi*a*b;
    }
};

int main()
{
    //构造测试用例
    CTriangle a(3,4,5);
    CRectangle b(3,3);
    //结果应为false
    cout<< boolalpha << (a>b) <<endl;
    return 0;
}

看起来没有毛病,但是编译器居然报错了,报错信息为:对象含有与成员函数CShape::area不兼容的类型限定符,对象类型是const CShape

       乍一看不明白这句话的意思,但是很显然问题出在area函数和重载运算符的参数类型上。强调对象类型是const CShape是有什么用意呢?我们知道,只要加上const关键字,就不能进行修改值(内存内容)的相关操作。那么传入重载运算符的两个常对象,都不能在重载运算符函数体内进行修改自身属性的操作。但是我写的area并没有修改对象属性啊,怎么会这样呢?后来我才知道,原来C++为了提高代码安全性,以及更好地预防误操作修改不允许修改的量,规定了这么一条规则:常对象只能调用常成员函数。常成员函数和一般的成员函数有什么区别呢?常成员函数的写法是"返回值类型 函数名(函数参数)const",与一般的成员函数相比末尾多了const,这个const的意思就是在函数体内不允许有修改对象属性的操作,也是一种保证,保证这个成员函数不会修改对象属性。于是bug如何解决就豁然开朗了,在所有定义area函数的代码行末尾加const就行了。修改后代码如下:

#include<iostream>
#include<math.h>

using namespace std;

#define Pi 3.14159

class CShape
{
private:
public:
    virtual double area()const=0;//修改1
    friend bool operator>(const CShape &a,const CShape &b)
    {
        return a.area()>b.area();
    }
};

class CTriangle:public CShape
{
private:
    double a,b,c,p;
public:
    CTriangle(double m,double n,double l)
    {
        a=m;
        b=n;
        c=l;
        p=(a+b+c)/2;
    }
    double area()const//修改2
    {
        return sqrt(p*(p-a)*(p-b)*(p-c));
    }
};

class CRectangle:public CShape
{
private:
    double a,b;
public:
    CRectangle(double m,double n)
    {
        a=m;
        b=n;
    }
    double area()const//修改3
    {
        return a*b;
    }
};

class CEllipse:public CShape
{
private:
    double a,b;
public:
    CEllipse(double m,double n)
    {
        a=m;
        b=n;
    }
    double area()const//修改4
    {
        return Pi*a*b;
    }
};

int main()
{
    CTriangle a(3,4,5);
    CRectangle b(3,3);
    cout<< boolalpha << (a>b) <<endl;
    return 0;
}

代码正常运行,bug1解决。然后我又想,能不能把比较运算符重载为成员函数而不是友元函数。于是我对代码再次做改动。此处只附上做了改动的代码:

class CShape
{
private:
public:
    virtual double area()const=0;
    bool operator>(const CShape &a,const CShape &b)
    {
        return a.area()>b.area();
    }
};

编译器又报了错,报错信息为:此运算符函数的参数太多

乍一看我又愣住了,参数何以太多?比较运算符要比较两个对象,必然需要两个参数啊。然后我意识到一件事:我已经把这个运算符重载为成员函数。何谓成员函数?就是只有属于成员函数所在的类的对象才能调用的函数。——就是说,我必须要用本类对象才能调用成员函数。举个例子,当我们重载运算符>为成员函数,执行a>b这个语句时,实际情况是对象a在调用这个函数。所以,运算符>前面的对象是不用传参的,学术一点、专业一点的说法就是第一个参数通过this指针隐式传递!那么这个bug如何解决就显而易见,删去参数列表第一个参数,并把函数体内第一个参数出现的地方改为this指针就行了。修改后代码如下:

class CShape
{
private:
public:
    virtual double area()const=0;
    bool operator>(const CShape &b)
    {
        //两种写法都可以
        //return (*this).area()>b.area();
        return this->area()>b.area();
    }
};

代码正常运行,bug2解决。

最后,再总结一下重载运算符为友元函数和成员函数的区别:前者的本质是一个全局函数,后者的本质是只允许本类对象调用的成员函数;前者没有this指针,所以必须要传递所有参数,后者是成员函数,有this指针,不用也不应该传递第一个参数,同时要保证调用时第一个参数是本类对象;如果重载双目运算符,当运算符第一个参数不是本类对象时,只能重载为友元函数,如果希望实现双目运算符第一个参数既可以是本类对象,也可以不是本类对象,需要重载该运算符两次,一次重载为成员函数,一次重载为友元函数。比如:

//A泛指某个类,x泛指A类中某个数据成员
A operator+(float b)//实现支持a+1.2之类操作,且仅支持此类操作
{
     A a;
     a.x=(*this).x+b;
     return a;
}
//实现支持1.2+a之类操作,且仅支持此类操作
friend A operator+(float b,A a)
{
     A c;
     c.x=b+a.x;
     return c;
}

 

猜你喜欢

转载自blog.csdn.net/qq_44643644/article/details/106161202