上篇笔记总结了C++类的基本结构和使用,并尝试着写了一个实现简单计算的类,但这个类仅仅是为了表明语法规则,并不能很好的体现类存在的意义。另外,类的设计也不会像上篇笔记那样死板和简单,它还有很多复杂的规则来是我们的设计变得方便(但不一定变得简单,很可能是变得复杂)。本篇笔记将总结一些类设计中的“新方法”,并同时设计一个“有意义”的类。
一、设计一个矢量类
这里,我首先提出一个软件开发需求。这个需求是要实现一个二维矢量的运算。二维矢量的计算的核心思想是同时考虑大小和方向,它有两种表示方式,一种表示为极坐标方式,即大小和方向;一种是直角坐标方式,即将矢量分解为直角坐标下的x和y分量。因为C++内置类型中并没有矢量这个物理量,而且C++的标准库中也没有相关的库文件。针对这个需求,最好的C++解决方案便是写一个矢量类(Vector)。该类实现矢量运算,如相加、相减、扩大等。本篇笔记将围绕实现这个类总结一些类设计的方法。
二、运算符重载
就像之前在《函数探幽》中介绍函数重载一样,C++还可以对运算符进行重载。其目的就是将运算符的功能扩展到不限于基本类型的情况。比如,本篇笔记将实现的矢量类的相加,就是要将两个自定义类的对象相加。重载运算符使得用户不用关心内部如何实现的,这也是OOP的主要目标之一。
运算符重载的格式如下;将函数名换为operator运算符即可
operator运算符(形参){函数体}
以重载Vector类的加号为例,其声明和实现分别如下;
Vector operator+(const Vector & b) const;
Vector Vector::operator+(const Vector & b) const
{
return Vector(x + b.x, y + b.y);
}
这使得我们在应用时可以直接将两个矢量相加(用+号运算符):
Vector A,B,C;
C = A+B;
这里我们要理解一个问题就是,编译器实际上是将上述“+”翻译为:
C= A. operator+(B);
也就是说它有加数和被加数的区别,A作为被加数,是调用运算符重载函数的对象,而B作为加数是函数的形参。对于加法来说,A和B可以互换位置,但是如果是减法,则不可以,因为这违背了物理意义。另外,这还带来一个问题,就是扩大的算法,是将向量扩大某值,是一个double类型,重载了*运算符,A*n和n*A的表面意义应该一样,但是在具体调用的时候会被翻译为:
n. operator*(B);
这显然行不通。后面将介绍通过另一种方式解决这个问题。
再分析一个问题,如果要实现A+B+C,这种重载可以实现吗?
答案是肯定的,这就是将函数返回值设计为Vector的另一作用,A+B的运算返回值是Vector类型的副本,我们假设为tmp,后面执行tmp+C,也即:
tmp. operator+(C);
这显然是可以的。
三、运算符重载限制:
1. 重载后的运算符必须至少有一个操作数是用户定义的类型,这是为了防止程序员为标准类型重载运算符,可以确保程序正确运行。
2. 不能修改运算符的优先级,不能违反运算符原本的运算规则。例如在重载加号时,不能提供两个形参(友元除外),例如下面的这种重载方式就是不被允许的;
Time operator+(const Time &t1,const Time &t2) const;
因为加法是一种双目运算符。在重载为类的成员函数后,加法运算的第一项应该是调用函数的一个对象。所以在运算符重载时,参数表中的参数数目应该是重载运算符的操作数减1。
3. 不能创造新的运算符,例如不能定义operator**()运算符来求幂;
4. 不能重载以下运算符;
“.”成员运算符
“.*”成员指针运算符
“::”作用域运算符
“?”条件运算符
“siezof”运算符。
5. 很多运算符可以通过成员或者非成员函数进行重载,但是以下四种只能通过成员函数进行重载;
“=”赋值运算符;
“()“函数调用运算符;
“[ ]”下标运算符;
“->”通过指针方位类成员的运算符。
6. 自增运算符(++)与自减运算符(--)
由于自增和自减运算符是单目运算符,在重载时应该是没有参数的,但是又有前置与后置之分,例如++i与i++。为了区分,C++做了规定;
Class operator++() //前置
Class operator++(int) //后置
四、友元函数
我们已经知道,类的私有成员被限制访问,公有接口是唯一可访问途径,但是有时候这种限制太严格,在某些场景使用并不方便。我们来看上述提到的给矢量扩大的重载函数,实现A*n可以将运算符重载函数写为:
Vector Vector::operator*(double n) const
{
return Vector(n * x, n * y);
}
但是如果要实现n*A,则上述函数不能满足要求,需要下面的运算符重载函数原型:而且因为在成员函数里已经对*运算符重载过了,下面这个函数只能定义为非成员函数,
Vector operator*(double n, const Vector & a);
{
return Vector(n * x, n * y)
}
这种情况下,非成员函数内没有权限访问类的成员变量x和y。C++提供了友元来解决这种问题。C++中友元有三种,分别是友元函数,友元类,友元成员函数。这里单独介绍友元函数,它使得函数成为类的友元,将拥有和类的成员函数一样的权限去访问类的成员变量。
创建友元函数的第一步是声明,友元函数的声明放在类的声明中,并且在前面加上friend关键字。例如;
friend Vector operator*(double n, const Vector & a);
这意味着:
1.虽然友元在类中声明,但是它并不是成员函数,所以不能使用成员运算符来调用它,
2.但是它却跟成员运算符有相同的访问权限,即可以通过友元访问类的私有成员。
第二步是定义友元,友元可以直接在声明的时候进行定义,即内联的定义。也可以定义在类外,定义在类外时,不需要加类作用域运算符,也不需要有friend关键字。这样上述的非成员函数重载接口就可以访问x和y了。于是n*A将被翻译为:operator*(n, A);
因此,可以总结为:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
五. 常用的友元:重载<<运算符
我们希望为Vector类重载<<运算符,以实现直接打印某些信息。<<经常被重载,按照通常所见:
int n=0;
cout<<n;
我们希望可以直接:
Vector A;
cout<<A;
但我们知道cout是ostream的一个对象。要想让Vector类知道ostream类的对象,且该对象位于第一个操作数,按照上述总结,需要通过友元来实现重载<<运算符。我们可以如此设计友元函数:
void operator<<(ostream & os, const Vector & v)
则cout<<A被翻译为:operator<<(cout, v)。
这里分析一个问题,按照友元函数的概念,这里用到了ostream的对象和Vector的对象,则该函数有必要同时成为ostream和Vector类的友元函数。但是实际上,由于用的参数是cout,且我们一直用的是cout的整体,并没有访问ostream类的私有成员,于是不必是ostream的友元。为了保证传进来的不是cout的副本,所以这里的参数用了&引用。
下面来看另一种常见的情况:
cout<<A<<B;
也即在实现连续输出,上述的友元函数不能满足,因为cout<<A的返回值为void,将无法执行<<B的操作。通常的
cout<<”hello”<<”world”<<endl;
能够执行是因为cout的返回值不是void而是返回cout本身。于是可以按此改造友元函数:
std::ostream & operator<<(std::ostream & os, const Vector & v)
{
os << "(x,y) = (" << v.x << ", " << v.y << ")";
return os;
}
注意返回值是ostream &,这实现了将传入的cout返回去。
该友元函数不仅可以用以显示在控制台上的cout,同样可以应用于写入文件的ofstream.因为ofstream是ostream的子类。不用修改接口,可直接再调用的时候:
Vector A,B;
ofstream fout;
fout.open(“./Test.txt”);
fout<<A<<B;
附源码:
//vect.h 矢量类声明 实现矢量运算和显示
#ifndef VECTOR_H_ //预编译宏 避免重复引用
#define VECTOR_H_
#include <iostream> //cout等对象的支持库
class Vector //类声明
{
public:
enum Mode {RECT, POL}; //枚举表达方式为直角坐标还是极坐标
//私有成员
private:
double x; // 直角坐标系下的x值
double y; // 直角坐标系下的y值
double mag; // 极坐标下的矢量长度
double ang; // 极坐标下的矢量角度
Mode mode; // 表达方式 或RECT或POL
// 私有接口函数
void set_mag(); //计算长度
void set_ang(); //计算角度
void set_x(); //计算x
void set_y(); //计算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;} // 得到x
double yval() const {return y;} // 得到y
double magval() const {return mag;} // 得到mag
double angval() const {return ang;} // 得到ang
void set_polar_mode(); // 设置表达方式为极坐标
void set_rect_mode(); // 设置表达方式为直角坐标
// 运算符重载
Vector operator+(const Vector & b) const; //矢量加法
Vector operator-(const Vector & b) const; //矢量减法
Vector operator-() const; //矢量反向,求负
Vector operator*(double n) const; //矢量扩大
// 友元函数
friend Vector operator*(double n, const Vector & a);//矢量扩大
friend std::ostream & operator<<(std::ostream & os, const Vector & v);//矢量打印
};
#endif
// vect.cpp 矢量类方法定义
#include <cmath>
#include "vect.h"
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;
// 弧度转度
const double Rad_to_deg = 45.0 / atan(1.0);//180/PI
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);
}
Vector::Vector()
{
x = y = mag = ang = 0.0;
mode = RECT;
}
//默认表达方式为RECT,直角表达
Vector::Vector(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
void Vector:: reset(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
Vector::~Vector()
{
}
void Vector::set_polar_mode()
{
mode = POL;
}
void Vector::set_rect_mode()
{
mode = RECT;
}
Vector Vector::operator+(const Vector & b) const
{
return Vector(x + b.x, y + b.y);
}
Vector Vector::operator-(const Vector & b) const
{
return Vector(x - b.x, y - b.y);
}
Vector Vector::operator-() const
{
return Vector(-x, -y);
}
Vector Vector::operator*(double n) const
{
return Vector(n * x, n * y);
}
Vector operator*(double n, const Vector & a)
{
return a * n;//这里调用了上述重载的*
}
std::ostream & operator<<(std::ostream & os, const Vector & v)
{
if (v.mode == Vector::RECT)
os << "(x,y) = (" << v.x << ", " << v.y << ")";
else if (v.mode == Vector::POL)
{
os << "(m,a) = (" << v.mag << ", "
<< v.ang * Rad_to_deg << ")";
}
else
os << "Vector object mode is invalid";
return os;
}
// main.cpp 测试Vector类功能
#include <iostream>
#include "vect.h"
int main()
{
using namespace std;
//用默认构造函数创建
Vector A;
A.reset(3, 4);
//用显式的构造函数直接创建,使用默认表达方式
Vector B(6, 8);
//用new的方式创建,使用指定的表达方式
Vector *C= new Vector(5, 53.1, Vector::POL);
//测试+
Vector Sum = A+B;
//测试-
Vector Sub = B-A;
//测试*
double n = 2.0;
Vector Mul = (*C)*n;
Vector Mul1 = n*(*C);
//测试求负
Vector Ops = -(*C);
//测试打印
cout<<A<<" "<<B<<" "<<*C<<endl;
//查看计算结果
cout<<Sum<<" "<<Sub<<" "<<Mul<<" "<<Mul1<<" "<<Ops<<endl;
Sum.set_polar_mode();
Sub.set_polar_mode();
Mul.set_polar_mode();
Mul1.set_polar_mode();
Ops.set_polar_mode();
cout<<Sum<<" "<<Sub<<" "<<Mul<<" "<<Mul1<<" "<<Ops<<endl;
//清理内存
delete C;
//窗口停留
cin.get();
return 0;
}