从C到C++___使用类(一)运算符重载和友元

本文详细介绍了C++中类的运算符重载和友元函数的概念及应用。通过一个时间类的例子,展示了如何重载+、-、*运算符,以及如何利用友元函数解决非成员项作为运算符的第一个操作数的问题。同时,文章提到了如何重载<<和>>运算符,使得对象可以直接与iostream流进行交互。最后,通过一个随机漫步程序展示了矢量类的使用,该类包含了对运算符的多重重载以及状态成员的使用。
摘要由CSDN通过智能技术生成

使用类(一)运算符重载和友元

理解了类和对象的通用原理后,我们的重点应该放在类设计技术。

1. 运算符重载

C++中的运算符本质上就是函数,函数可以重载,那么运算符也是可以重载的,C++允许将运算符重载扩充到用户定义的类型,例如可以使用+将两个对象加起来。
要重载运算符,要使用运算符函数。它的形式是:
operatorop(argument-list)
如果一个类重载了+a,b,c是三个对象。则我们可以使用:

c=a+b;
c=a.operator+(b);

上面这两种调用+,是等价的。

d=a+b+c;

上面代码也是可行的,因为+是从左到右结合的上面式子等价于

d=a.operator+(b+c);
进一步的
d=a.operator+(b.operator+(c));

1.1 例子:重载+-*的类

我们设计一个类,用来表示时间的数据类型,时间的加法减法乘法可以通过重载运算符的方式。

//使用类1.h
#ifndef TIME
#define TIME
class Time
{
    private:
        int hour;
        int min;
    public:
        Time();
        Time(int h,int m);
        void show() const;
        Time operator+(const Time &t) const;
        Time operator-(const Time &t) const;
        Time operator*(double b) const;

};
#endif
//使用类1.cpp
#include"使用类1.h"
#include<iostream>
Time::Time()
{
    hour=0;
    min=0;
}
Time::Time(int h,int m)
{
    hour=h;
    min=m;
}

void Time::show() const
{
    using namespace std;
    cout<<hour<<"小时 "<<min<<"分钟"<<endl;
}
Time Time::operator+(const Time &t) const
{
    Time sum;
    sum.min=t.min+this->min;
    sum.hour=t.hour+this->hour+sum.min/60;
    sum.min%=60;
    return sum;
}
Time Time::operator-(const Time &t) const
{
    Time ret;
    int tot1,tot2;
    tot1=60*this->hour+this->min;
    tot2=60*t.hour+t.min;
    ret.min=tot1-tot2;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}
Time Time::operator*(double b) const
{
    Time ret;
    double tot=this->hour*60+this->min;
    tot*=b;
    ret.min=(int)tot;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}
//使用类1main.cpp
#include"使用类1.h"
#include<iostream>
int main()
{
    using namespace std;
    Time a,b,c;
    a={13,10};
    b={8,45};
    cout<<"a: ";
    a.show();
    cout<<"b: ";
    b.show();
    c=a+b;
    cout<<"a+b: ";
    c.show();
    c=a-b;
    cout<<"a-b: ";
    c.show();
    c=a*1.5;
    cout<<"a*1.5: ";
    c.show();
    c=b-a*0.5+a-b;
    cout<<"b-a*0.5+a-b: ";
    c.show();

    
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 使用类1 .\使用类1.cpp .\使用类1main.cpp
PS D:\study\c++\path_to_c++> .\使用类1.exe
a: 13小时 10分钟
b: 8小时 45分钟
a+b: 21小时 55分钟
a-b: 4小时 25分钟
a*1.5: 19小时 45分钟
b-a*0.5+a-b: 6小时 35分钟

1.2 重载限制

多数运算符可以重载,而且重载运算符不必是成员函数,也可以是常规函数。

实际上,重载运算符确实有约束

  1. 可以重载的运算符如下图
    pic
    这表格中,有些运算符是只能通过成员函数来重载的:
    =()[]->
  2. 重载后的运算符至少有一个操作数是用户定义的类型,这防止用户为标准类型重载运算符。
  3. 使用运算符时仍然遵循原来的优先级法则。
  4. 不能创造新的运算符。
  5. 存在不可以重载的运算符,例如.::?:sizeof;

2. 友元函数

1.1中那个时间类,有一个缺陷:在重载*的时候,它只能识别c=a*0.5;但是不能识别c=0.5*a;这是因为我们使用类成员函数的方式重载符号的时候,调用对象只能放在运算符左边,a.operator*(0.5)a*0.5等价,而0.5*a确无法找到匹配的函数。
如何解决这个问题?
一种思路是,采用非成员函数重载运算符

Time operator*(double n,const Time& t)
{
    return t*n;
}

好了,perfect!由于乘法交换律,很轻松解决这个问题。但是很多时候运算并不满足交换律,非成员函数有个致命的问题:无法访问对象的私有成员,也就是说,它无法使用数据t.mint.hour.
有没有一种可以访问对象私有成员的非成员函数?----友元函数

  • 友元是什么?

    C++控制类对象的私有部分的访问,通常我们只能用公有类方法访问私有成员,但是C++还提供的一种访问私有成员的方法----友元
    总共有三种友元:
    友元函数
    友元类
    友元成员函数

  • 友元函数的原型:

friend Time operator*(double n,const Time& t);
就是非成员函数前面加个关键词friend,但是友元函数的原型是放在类声明中的

  • 友元函数的定义

Time operator*(double n,const Time& t)
{
    Time ret;
    double tot=t.hour*60+t.min;
    tot*=n;
    ret.min=(int)tot;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}

友元函数的定义和常规函数一样,

  1. 它没有friend关键词.
  2. 虽然友元函数的原型在类声明中,但是它不是类成员,自然也不需要作用域解析符::.
  3. 友元函数的独特之处在于,它可以访问数据t.mint.hour

总之,如果要给类重载运算符,并将非类项,作为其第一个操作数,可以用友元函数反转操作数顺序!

2.1 常用友元:重载<<运算符

首先<<是个什么运算符?他可以是移位符号,也可以是ostream类中的重定向符。
我们希望使用

Time a={13,45};
std::cout<<a;

上面这种方式直接显示对象,我们首先了解一下<<的特性。
ostream是一个类,coutcerr都是它的对象。
ostream类重载了<<,而且它对每一种标准类型它都重载了一遍<<,使其变成一个很智能的运算符,<<是一个二元运算符,他左边是一个ostream对象,右边是要输出的数据,它的返回值是一个指向ostream对象的引用。

int x=5;
double y=8;
cout<<x<<y;

cout<<x<<y写成显式调用成员函数的形式就是:
(cout.operator<<(x)).operator<<(y)
先是调用类成员函数operator<<(int),显示x的值,然后它的返回值就是cout对象,然后再次调用类成员函数operate<<(double),执行cout.operator<<(y)显示y的值。
那我们就能轻松推断出,ostream类中operator<<()函数的一种重载定义应该是:

ostream & ostream::operator<<(某标准类型){
    ...
    return *this;
}

它的返回值就是cout,就是调用对象。所以返回类型一定是对ostream对象的引用。

  • 如何在我们定义的类中重载<<?

    绝对不能用成员函数来重载,因为用成员函数重载,那调用对象一定要在运算符左边,然而运算符左边是cout对象。所以我们采用友元函数的形式重载<<
    这个友元函数,接受两个参数,左边是ostream &类型,右边是const类的引用(不采用引用传参也行),返回类型也是ostream &

    那么这个友元函数应当这么写:

std::ostream & operator<<(std::ostream& os,const Time& t)
{
    os<<t.hour<<"小时 "<<t.min<<"分钟"<<std::endl;
    return os;
}

第一个参数和返回类型一定要是引用参数,因为它就是cout,第二个cons Time &主要是为了高效,也可以按值传递,不过对于类来说,我们一般都是采用引用传递或者const引用传递。
cout<<a<<b时,相当于调用operator<<(operator<<(cout,a),b)函数。

2.2 重载>>运算符

同样的,我们希望通过cin>>a;这种方式,输入对象a的值。
那么肯定也是用友元函数了

std::istream & operator>>(std::istream &is,Time &t)
{
    std::cout<<"输入小时和分钟\n";
    is>>t.hour>>t.min;
    return is;
}

那么我们更新一下我们的Time类:

//使用类2.h
#ifndef TIME
#define TIME
#include<iostream>
class Time
{
    private:
        int hour;
        int min;
    public:
        Time();
        Time(int h,int m);
        Time operator+(const Time &t) const;
        Time operator-(const Time &t) const;
        Time operator*(double b) const;
        friend Time operator*(double n,const Time& t);
        friend std::ostream & operator<<(std::ostream &os,const Time &t);
        friend std::istream & operator>>(std::istream &is,Time &t);

};
#endif
//使用类2.cpp
#include"使用类2.h"
#include<iostream>
Time::Time()
{
    hour=0;
    min=0;
}
Time::Time(int h,int m)
{
    hour=h;
    min=m;
}

Time Time::operator+(const Time &t) const
{
    Time sum;
    sum.min=t.min+this->min;
    sum.hour=t.hour+this->hour+sum.min/60;
    sum.min%=60;
    return sum;
}
Time Time::operator-(const Time &t) const
{
    Time ret;
    int tot1,tot2;
    tot1=60*this->hour+this->min;
    tot2=60*t.hour+t.min;
    ret.min=tot1-tot2;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}
Time Time::operator*(double b) const
{
    Time ret;
    double tot=this->hour*60+this->min;
    tot*=b;
    ret.min=(int)tot;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}
std::ostream & operator<<(std::ostream& os,const Time& t)
{
    os<<t.hour<<"小时 "<<t.min<<"分钟"<<std::endl;
    return os;
}
std::istream & operator>>(std::istream &is,Time &t)
{
    std::cout<<"输入小时和分钟\n";
    is>>t.hour>>t.min;
    return is;
}
Time operator*(double n,const Time& t)
{
    Time ret;
    double tot=t.hour*60+t.min;
    tot*=n;
    ret.min=(int)tot;
    ret.hour=ret.min/60;
    ret.min%=60;
    return ret;
}
//使用类2main.cpp
//使用类2main.cpp
#include"使用类2.h"
#include<iostream>
int main()
{   
    using namespace std;
    Time a={13,10};
    cout<<"a: "<<a;
    cin>>a;
    cout<<"a: "<<a;
    Time c=a*0.5;
    Time d=0.5*a;
    cout<<"a*0.5: "<<c;
    cout<<"0.5*a: "<<d;
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 使用类2 .\使用类2.cpp .\使用类2main.cpp
PS D:\study\c++\path_to_c++> .\使用类2.exe
a: 13小时 10分钟
输入小时和分钟
7 15
a: 7小时 15分钟
a*0.5: 3小时 37分钟
0.5*a: 3小时 37分钟

2.3 重载运算符:作为成员函数还是非成员函数

例如对于+的重载
你既可以采用成员函数

Time operator+(const Time& t);

这样子,一个参数通过this指针隐式传入,另一个参数,显式传入。

也可以采用友元函数

friend Time operator+(const Time& t1,const Time& t2); 

这样子,两个参数都是显式传入的
这两种形式没有太大区别,不过采用友元函数的形式,应该会更好(类的类型转化时)。

3.再谈重载:矢量类

这个矢量类不是标准模板库(STL)中的vector模板类
而是在工程中,我们常说的矢量:有长度和方向的量。

//矢量类.h
#ifndef _VECTOR_H_
#define _VECTOR_H_
#include<iostream>
namespace VECTOR
{
    class Vector
    {
        public:
            enum Mode {RECT,POL};
        private:
            double x;
            double y;
            double mag;//length of vector
            double ang;//directionof vector in degrees
            Mode mode;

            void set_mag();
            void set_ang();
            void set_x();
            void set_y();
        public:
            Vector();
            Vector(double n1,double n2,Mode form=RECT);
            void reset(double n1,double n2,Mode form=RECT);
            ~Vector();
            double xval() const{return x;}
            double yval() const{return y;}
            double magval() const{return mag;}
            double angval() const{return ang;}
            void polar_mode();
            void rect_mode();
        //operator overloading
            Vector operator+(const Vector& b)const;
            Vector operator-(const Vector& b)const;
            Vector operator-()const;
            Vector operator*(double n)const;
        //friends
            friend Vector operator*(double n,const Vector& a);
            friend std::ostream & operator<<(std::ostream &os,const Vector &a );
    };
} // namespace VECTOR

#endif
//矢量类.cpp
#include"矢量类.h"
#include<cmath>
namespace VECTOR
{
    using std::sqrt;
    using std::sin;
    using std::cos;
    using std::atan2;
    using std::atan;
    using std::cout;

    const double Rad_to_deg=45.0/atan(1.0);
    void Vector::set_mag()
    {
        mag=sqrt(x*x+y*y);
    }
    void Vector::set_ang()
    {
        if(x==0.0 && y==0.0)
            ang=0.0;
        else
            ang=atan2(y,x);
    }
    void Vector::set_x()
    {
        x=mag*cos(ang);
    }
    void Vector::set_y()
    {
        y=mag*sin(ang);
    }

//pubic methods
    Vector::Vector()
    {
        x=y=mag=ang=0.0;
        mode=RECT;
    }
    Vector::Vector(double n1,double n2,Mode form)
    {
        mode=form;
        switch (mode)
        {
        case RECT:
            x=n1;
            y=n2;
            set_mag();
            set_ang();
            break;
        case POL:
            mag=n1;
            ang=n2;
            set_x();
            set_y();
            break;
        default:
            cout<<"the 3rd argument is incorrect! in Vector() --\n";
            cout<<"Vector set to 0\n";
            x=y=ang=mag=0;
            mode=RECT;
            break;
        }
    }
    void Vector::reset(double n1,double n2,Mode form)
    {
        mode=form;
        switch (mode)
        {
        case RECT:
            x=n1;
            y=n2;
            set_mag();
            set_ang();
            break;
        case POL:
            mag=n1;
            ang=n2;
            set_x();
            set_y();
            break;
        default:
            cout<<"the 3rd argument is incorrect! in Vector() --\n";
            cout<<"Vector set to 0\n";
            x=y=ang=mag=0;
            mode=RECT;
            break;
        }       
    }
    Vector::~Vector()
    {
    }
    void Vector::polar_mode()
    {
        mode=POL;
    }
    void Vector::rect_mode()
    {
        mode=RECT;
    }
//operator overloading
    Vector  Vector::operator+(const Vector& b)const
    {
        return {x+b.x,y+b.y};
    }
    Vector  Vector::operator-(const Vector& b)const
    {
        return {x-b.x,y-b.y};
    }
    //reverse sign of Vector
    Vector  Vector::operator-()const
    {
        return {-x,-y};
    }
    Vector  Vector::operator*(double n)const
    {
        return {n*x,n*y};
    }
//friends
    Vector operator*(double n,const Vector& a)
    {
        return a*n;
    }
    std::ostream & operator<<(std::ostream &os,const Vector &a )
    {
       switch (a.mode)
       {
       case Vector::RECT:
        os<<" (x,y) = ("<<a.x<<", "<<a.y<<")";
        break;
       case Vector::POL:
        os<<" (m,a) = ("<<a.mag<<", "<<a.ang*Rad_to_deg<<")";
        break;
       default:
        os<<"Vector object mode is invalid";
        break;
       }
       return os;
    }
} // namespace VECTOR

Vector类有一些特性。

  • 使用状态成员

    Vector类中的mode成员是表示状态,它会控制其他函数,而且为了得到类作用域中的常数,它采用的是枚举类来表示状态。

  • 重载算术运算符

        Vector  Vector::operator+(const Vector& b)const
      {
          return {x+b.x,y+b.y};
      }
    

    return {x+b.x,y+b.y};会调用构造函数,它是将新的xy传给构造函数的方式完成运算。

  • 对已重载的运算符再次重载

    -既可以表示减法,又可以表示负数,所以这边对于-的重载有两个。

接下来,看一个使用Vector类的随机漫步程序:

//矢量类_随机漫步.cpp
#include<iostream>
#include<fstream>
#include<cstdlib>
#include<ctime>
#include"矢量类.h"
int main(){
    using VECTOR::Vector;
    using namespace std;
    srand(time(0));
    double direction;
    Vector step;
    Vector result{0.0,0.0};
    ofstream fout;
    fout.open("thewalk.txt");
    unsigned long steps=0;
    double target;
    double dstep;
    cout<<"Enter target distance (q to quit): ";
    while (cin>>target)
    {
        cout<<"Enter step length: ";
        if(!(cin>>dstep))
            break;
        while (result.magval()<target)
        {
            /* code */
            //cout<<result<<endl;
            direction=rand()%360;//random integer in [0,359]
            step.reset(dstep,direction,Vector::POL);
            result=result+step;
            steps++;
        }
        cout<<"After "<<steps<<" steps,the subjuct has the following location:\n";
        cout<<result<<endl;
        result.polar_mode();
        cout<<"Or:\n"<<result<<endl;
        cout<<"Average outward distance per step = "<<result.magval()/steps<<endl;
        fout<<endl<<"the target distance: "<<target<<endl;
        fout<<"the step length: "<<dstep<<endl;
        fout<<"After "<<steps<<" steps,the subjuct has the following location:\n";
        fout<<"Average outward distance per step = "<<result.magval()/steps<<endl;
        steps=0;
        result.reset(0.0,0.0);
         cout<<"Enter target distance (q to quit): ";
        
    }
    cout<<"Bye!\n";
    cin.clear();
    while (cin.get()!='\n')
    {
        continue;
    }
    fout.close();
    return 0;
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 矢量类_随机漫步 .\矢量类.cpp .\矢量类_随机漫步.cpp
PS D:\study\c++\path_to_c++> .\矢量类_随机漫步.exe
Enter target distance (q to quit): 300
Enter step length: 2
After 38973 steps,the subjuct has the following location:
 (x,y) = (297.689, -37.6848)
Or:
 (m,a) = (300.065, -7.21477)
Average outward distance per step = 0.00769929
Enter target distance (q to quit): 1000
Enter step length: 0.5
After 558557 steps,the subjuct has the following location:
 (x,y) = (845.075, 535.457)
Or:
 (m,a) = (1000.43, 32.3592)
Average outward distance per step = 0.0017911
Enter target distance (q to quit): q
Bye!

谈谈随机数,标准ANSI C库中有一个rand()函数,他会返回一个随机整数,但是这些随机数,实际上是,伪随机数,因为rand()函数是根据种子来生成随机数的,种子不变随机数序列就不变。然而srand()函数允许覆盖默认的种子值。time(0)会返回当前时间,srand(time(0))就会按照当前时间设置种子。
还有一点就是fout<<endl<<"the target distance: "<<target<<endl;这里的<<运算符也会被重载,这是因为类继承属性,可以让ostream引用指向oftream对象,从而调用友元函数重载运算符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值