使用类(一)运算符重载和友元
理解了类和对象的通用原理后,我们的重点应该放在类设计技术。
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 重载限制
多数运算符可以重载,而且重载运算符不必是成员函数,也可以是常规函数。
实际上,重载运算符确实有约束。
- 可以重载的运算符如下图
这表格中,有些运算符是只能通过成员函数来重载的:
=
、()
、[]
、->
- 重载后的运算符至少有一个操作数是用户定义的类型,这防止用户为标准类型重载运算符。
- 使用运算符时仍然遵循原来的优先级法则。
- 不能创造新的运算符。
- 存在不可以重载的运算符,例如
.
、::
、?:
、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.min
和t.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;
}
友元函数的定义和常规函数一样,
- 它没有
friend
关键词.- 虽然友元函数的原型在类声明中,但是它不是类成员,自然也不需要作用域解析符
::
.- 友元函数的独特之处在于,它可以访问数据
t.min
和t.hour
总之,如果要给类重载运算符,并将非类项,作为其第一个操作数,可以用友元函数反转操作数顺序!
2.1 常用友元:重载<<
运算符
首先
<<
是个什么运算符?他可以是移位符号,也可以是ostream
类中的重定向符。
我们希望使用
Time a={13,45};
std::cout<<a;
上面这种方式直接显示对象,我们首先了解一下
<<
的特性。
ostream
是一个类,cout
,cerr
都是它的对象。
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};
会调用构造函数,它是将新的x
和y
传给构造函数的方式完成运算。 -
对已重载的运算符再次重载
-
既可以表示减法,又可以表示负数,所以这边对于-
的重载有两个。
接下来,看一个使用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
对象,从而调用友元函数重载运算符。