《C++ primer plus》精炼(OOP部分)——对象和类(4)

“学习是人类进步的阶梯,也是个人成功的基石。” - 罗伯特·肯尼迪

文章目录

  • 友元函数
    • 利用友元函数重载<<运算符
    • 重载部分示例:矢量类

友元函数

先看看在上一章中我们作为例子的代码:

class Student{
	string name;
	int grade;
	int operator+(Student s)
	{
		return this->grade+s.grade;
	}
	
	int operator+(int a)
	{
		return this->grade+a;
	}
}

void test(Student a,Student b)
{
	a.operator+(b);
	int c=a+b;
	int d=a+c;
}

注意最后一行:

int d=a+c;

这一行的意义在上文已经说明,但是这是“+”运算符,也就是说,如果我编写以下的代码,在逻辑上依旧是正确的:

int d=c+a;

虽然在逻辑上正确,但在语法上并不能通过编译,因为编译器会认为是int类型的c调用了重载的函数。为了解决这个问题,我们可以用多种方法:

  1. 编写一层非成员函数接口。观察以下代码:
int operator+(int a,Student b)//非成员函数也可以重载运算符
{
	return b+a;
}

当成员函数进行运算符重载时,编译器默认调用这个函数的对象是这个函数的第一个操作数;非成员函数进行运算符重载时,编译器严格按照传参顺序决定操作数顺序。加入这个函数后,上面的代码就可以被编译器解释为传入int类型的c和Student类型的a作为参数,调用非成员函数,这就是接口的思想。
2. 事实上,C++的友元函数语法可以解决这个问题。友元函数是在类声明内定义的非成员函数,但是可以访问类的保护和私有成员,可以理解为具有成员函数视野的非成员函数。当想把一个函数声明为友元函数时,在函数声明开头加friend关键字。观察以下代码:

class Student{
	string name;
	int grade;
	int operator+(Student s)
	{
		return this->grade+s.grade;
	}
	
	int operator+(int a)
	{
		return this->grade+a;
	}
	//友元函数在类内声明
	friend int operator+(int a,Student b)//因为本质上是非成员函数,因此编译器按照传参顺序决定操作数顺序
	{
		return a+b.grade;//也不能用this指针,因为对象不能用.运算符调用友元函数
	}
	friend int operator+(Student a,Student b);
}

int operator+(Student a,Student b)//因为不是成员函数,所以在类外定义时不使用::运算符,而且也不用额外加friend关键字
{
	return a.grade+b.grade;
}

利用友元函数重载<<运算符

观察以下代码:

cout<<i;

这行代码使用了<<运算符,第一个操作数是ostream(输出流)类的对象cout,第二个操作数是一个不定类型的变量i。从上一篇我们知道大部分运算符都能进行重载,因此我们可以重载能用Student类对象作为操作数的函数。
首先,我们排除在类内重载<<运算符的可能性,因为这样就需要以Student类对象作为第一操作数,需要用以下代码来使用:

i<<cout;//i是一个Studnet类对象

代码的可读性很低,也很不习惯。因此,我们用友元函数来实现重载。
接下来,我们确定这个函数的参数和返回值。参数很好确定,第一个参数是ostream类对象,第二个参数是Student类对象。至于返回值,我们先看对于内置类型,<<运算符是怎么运作的:

cout<<a<<b<<c;//a,b,c都是int类型

对于上面这个语句的行为,我们可以理解为:

((cout<<a)<<b)<<c
  1. 第一个运算符调用函数,以ostream类对象为第一个参数,int类对象为第二个参数,输出a
  2. 第二个运算符应该也调用相同的函数,这意味着参数也相同,第一个参数也应该为ostream对象,所以第一个运算符调用的函数应该返回一个ostream类的对象

拓展到Student类型的重载,道理也是一样的,应该返回一个ostream类对象。以下是该友元函数的一种实现方式:

ostream operator*(ostream& os,Student s)
{
	return os<<s.name<<' '<<s.grade;//输出学生的名字和年级
}

重载部分示例:矢量类

作为一个例子,我们构建一个表示矢量的类。矢量是一个有长度的方向的量。我们可以用直角坐标和极坐标两种方式来表示。下面是这个类的声明:

namespace VECTOR {
	class Vector
	{
	public:
		enum Mode { RECT, POL };
		//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();
		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();//将模式设为POL
		void rect_mode();//将模式设为RECT
		//运算符重载
		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, Vector& a);
		friend std::ostream& operator<<(std::ostream& os, const Vector& v);
	};
}//end namespace VECTOR
  1. 使用VECTOR命名空间
  2. 使用enum枚举,带有RECL(直角坐标)和POL(极坐标)两个枚举量,表示这个对象用哪种方式描述矢量。
  3. private下有四种私有方法,用于直接控制类的私有变量,只有公共接口能使用这四个方法
  4. 注意第二种构造函数和reset函数的参数:
Vector(double n1, double n2, Mode form = RECT);
void reset(double n1, double n2, Mode form = RECT);

这两个函数的最后一个参数,即表示模式,有默认值,这代表在使用这个函数的时候不一定需要输入三个参数,编译器会自动采取默认参数值,但如果有恰当的参数值传入,则编译器使用新的参数值:

Vector v1(30,40)//此时编译器采用默认参数RECT
Vector v1(30,40,VECTOR::POL)//编译器采用传入的参数POL,注意POL在命名空间VECTOR中,要用::运算符
  1. 内联函数,忘记的可以看前几章

接下来是这个类的实现:

//定义表示一弧度的度数的一个变量
	const double Rad_to_deg = 45.0 / atan(1.0);
	//私有方法
	//从输入的x和y计算长度
	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(x, y);
	}
	//极坐标下设置x值
	void Vector::set_x()
	{
		x = mag * cos(mag);
	}
	//极坐标下设置y值
	void Vector::set_y()
	{
		y = mag * sin(ang);
	}
	//公共方法
	Vector::Vector()
	{
		x = y = mag = ang = 0.0;
		mode = 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;
			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;
			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::polar_mode()
	{
		mode = POL;
	}
	void Vector::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);
	}
	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 invaild";
		return os;
	}
	//display rectangular coordinates if mode is RECT
	//else display polar coordinates if mode is POL
	Vector operator*(double n, const Vector &a)//友元函数
	{
		return a * n;
	}
  1. C++中的函数在角度方面使用弧度制,但在我们构造的类中使用角度制,因此需要定义一个从弧度到角度的变量。
  2. 其中一些陌生的函数进行一些数学运算,头文件为< cmath >,具体如下:
  • atan函数:接收一个正切值(直线的斜率)输出直线与x轴的夹角(-90~90度)
  • sqrt函数:输入一个数,输出这个数的平方根
  • atan2函数:接收一个点的横纵坐标,输出横纵坐标与原点的连线延伸出的直线与x轴正方向的夹角(-180~180度)
  • cos、sin函数:输入一个角度,输出这个角度的余弦/正弦值
  1. 带有参数的构造函数和reset函数会根据模式的不同,进行不同行为的初始化。
  2. 注意运算符重载和友元函数的逻辑

下面是一个使用矢量类的实例,建议自己复制下来或者敲一遍运行一下(不要忘记给声明和定义加上头文件):

//randwalk.cpp -- using the Vector class
//compile with the vect.cpp file
#include
#include//rand(),srand()函数的头文件
#include//time()函数的头文件
#include "vector.h"
int main(void)
{
	using namespace std;
	using VECTOR::Vector;
	srand(time(0));//随机种子生成器
	double direction;
	Vector step;
	Vector result(0.0, 0.0);
	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)
		{
			direction = rand() % 360;
			step.reset(dstep, direction, Vector::POL);
			result = result + step;
			steps++;
		}
		cout << "After " << steps << " steps,the subject 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;
		steps = 0;
		result.reset(0.0, 0.0);
		cout << "Enter target diatance(q to quit):";
	}
	cout << "Bye!\n";
	cin.clear();
	while (cin.get() != '\n')
		continue;
	return 0;
}

请添加图片描述
我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!

你可能感兴趣的:(C++类和对象,c++,开发语言)