分数类+重载为成员函数和友元函数出现二义性_简易矩阵类C++实现

a54535477b27c3dc6c6a1baf510a2003.png

文章的标题图里面显示的matrix类是openCV里面的matrix类的源代码,这个类在openCV的神经网络库里面发挥了至关重要的作用。不过今天我们要做的任务不是吃透openCV的matrix代码,而是直接着手去实现一个Matrix类,并且封装一些必要的函数,重载一些必要的运算符。这些操作只需要基础的C++知识即可。

首先是Matrix成员的设计,我们设计Matrix的时候要考虑到几点:Matrix的必需参数,Matrix的数据存储区,以及Matrix支持的数据类型。

很明显,Matrix包括行数row,列数col,一个支持任意数据类型的**num二维指针。为了支持任意数据类型,使用template来实现。

首先写出大体框架,考虑到我们需要一些基础功能,尤其是运算功能和数据寻找功能,所以我们大致确定需要重载的各种运算符。

template<typename __T>
class Matrix
{
private:
	int row;
	int col;
	__T **num;
public:
	Matrix(const int,const int);
	Matrix(const Matrix<__T>&);
	~Matrix();
	Matrix  operator+ (const Matrix<__T>&);
	Matrix  operator- (const Matrix<__T>&);
	Matrix  operator* (const Matrix<__T>&);
	Matrix& operator= (const Matrix<__T>&);
	__T*    operator[](const int);
	Matrix  Hadamard  (const Matrix<__T>&);
	Matrix  Transpose ();
	template<typename T> friend std::ostream& operator<<(std::ostream&,const Matrix<T>&);
	template<typename T> friend std::istream& operator>>(std::istream&,const Matrix<T>&);
};

template<typename __T>
Matrix<__T>::Matrix(const int __row,const int __col)
{
	row=__row;
	col=__col;
	if(row>0 && col>0)
	{
		num=new __T* [row];
		for(int i=0;i<row;++i)
			num[i]=new __T[col];
	}
	else
	{
		row=0;
		col=0;
		num=NULL;
	}
	return;
}

template<typename __T>
Matrix<__T>::Matrix(const Matrix<__T>& __temp)
{
	row=__temp.row;
	col=__temp.col;
	if(row>0 && col>0)
	{
		num=new __T* [row];
		for(int i=0;i<row;++i)
			num[i]=new __T[col];
		for(int i=0;i<row;++i)
			for(int j=0;j<col;++j)
				num[i][j]=__temp.num[i][j];
	}
	else
	{
		row=0;
		col=0;
		num=NULL;
	}
	return;
}

template<typename __T>
Matrix<__T>::~Matrix()
{
	if(num)
	{
		for(int i=0;i<row;++i)
			delete[] num[i];
		delete []num;
	}
	return;
}

首先是构造函数和拷贝构造函数,为了照顾定义对象数组时可能出现的没有初始化参数的情况,构造函数中默认num为NULL。拷贝构造函数则是单纯的复制数据创造新对象。析构函数很简单,就是释放二维指针指向的所有空间,前提是num不为NULL。

接下来是实现最简单的两个运算符,+和-。要使得+和-运算符可工作,首先两边的对象必须row和col值都相同才行,这个条件在Hadamard乘积函数里也有提及。

+和-的运作就是对应的矩阵里的数进行+和-。不过为了防止真的出现不符合条件的运算符调用,我还特地加入了throw机制来提醒出错,throw出的字符串可以被catch到。

template<typename __T>
Matrix<__T> Matrix<__T>::operator+(const Matrix<__T>& B)
{
	if(this->row==B.row&&this->col==B.col)
	{
		for(int i=0;i<row;++i)
			for(int j=0;j<col;++j)
				this->num[i][j]+=B.num[i][j];
		return *this;
	}
	else
	{
		Matrix<__T> NullMatrix(0,0);
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
		return NullMatrix;
	}
}

template<typename __T>
Matrix<__T> Matrix<__T>::operator-(const Matrix<__T>& B)
{
	if(this->row==B.row&&this->col==B.col)
	{
		for(int i=0;i<row;++i)
			for(int j=0;j<col;++j)
				this->num[i][j]-=B.num[i][j];
		return *this;
	}
	else
	{
		Matrix<__T> NullMatrix(0,0);
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
		return NullMatrix;
	}
}

接下来是=运算符的重载。=运算符重载的传参必须是const型,否则编译器报错。

template<typename __T>
Matrix<__T>& Matrix<__T>::operator=(const Matrix<__T>& B)
{
	if(num)
	{
		for(int i=0;i<row;++i)
			delete[] num[i];
		delete num;
	}
	row=B.row;
	col=B.col;
	if(row>0 && col>0)
	{
		num=new __T* [row];
		for(int i=0;i<row;++i)
			num[i]=new __T[col];
		for(int i=0;i<row;++i)
			for(int j=0;j<col;++j)
				num[i][j]=B.num[i][j];
	}
	else
	{
		row=0;
		col=0;
		num=NULL;
	}
	return *this;
}

由于是直接对左侧*this操作的,所以在复制之前先确认num是否已经指向一块内存,如果指向了,那么就释放这个内存重新申请。

如果operator=是void类型返回,那么这个运算符是不能完成这个语句的:

a=b=c;

所以使用Matrix &,因为使用引用一方面可以使得上面这个语句行得通,而且效率也比较高。

重载[]可以使用代理类(感谢评论区小伙伴的提醒,一年前忘了实现这个),考虑到简易写法,先在这里实现一个不使用代理类的直接方法,但是这个方法在实际使用中如果使用不当,可能会导致野指针的出现,所以之后会继续增加代理类写法:

template<typename __T>
__T* Matrix<__T>::operator[](const int addr)
{
	return addr>=this->row? NULL:this->num[addr];
}

接下来是流运算符<<和>>的重载。流运算符的重载都是通过friend友元函数重载的,写法大致如下:

类内部声明:

template<typename T> friend std::ostream& operator<<(std::ostream&,const Matrix<T>&);
template<typename T> friend std::istream& operator>>(std::istream&,const Matrix<T>&);

外部:

template<typename T>
std::ostream& operator<<(std::ostream& strm,const Matrix<T>& aim)
{
	for(int i=0;i<aim.row;++i)
	{
		for(int j=0;j<aim.col;++j)
			strm<<aim.num[i][j]<<((char)(j==aim.col-1)? 'n':' ');
	}
	return strm;
}

template<typename T>
std::istream& operator>>(std::istream& strm,const Matrix<T>& aim)
{
	for(int i=0;i<aim.row;++i)
		for(int j=0;j<aim.col;++j)
			strm>>aim.num[i][j];
	return strm;
}

外部友元的template函数的typename千万要避免和class的template里面的typename重名。

接下来三个功能都是重头戏,在神经网络的前向过程和反向传播中都缺少不了这些功能。

1.重载运算符*

2.矩阵转置Transpose

3.矩阵Hadamard乘积(在LSTM中有提及)

由于template的特殊性,而后面两个功能的传出类型是Matrix,所以也必须是一个template函数,故这次我也把这两个功能写成了友元函数。重载运算符*则直接写在类中。

运算符*的条件也很明显,两侧的row和col必须不为0,且左侧的col必须等于右侧的row,否则运算无法进行,会throw出一个错误提示字符串,可被catch。

过程按照矩阵乘法来即可,左侧第n行的向量与右侧第m列的向量点乘得到一个数值,放入新创建的temp.num[n][m]中。

template<typename __T>
Matrix<__T> Matrix<__T>::operator*(const Matrix<__T>& B)
{
	Matrix<__T> NullMatrix(0,0);
	if(!this->row || !this->col|| !B.row|| !B.col)
	{
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
	}
	else if(this->col!=B.row)
	{
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
	}
	else
	{
		Matrix<__T> Temp(this->row,B.col);
		__T trans;
		for(int i=0;i<Temp.row;++i)
			for(int j=0;j<Temp.col;++j)
			{
				trans=0;
				for(int k=0;k<this->col;++k)
					trans+=this->num[i][k]*B.num[k][j];
				Temp.num[i][j]=trans;
			}
		return Temp;
	}
	return NullMatrix;
}

Hadamard乘积则比较好理解,条件是左右两侧的矩阵row和col必须分别相同,否则运算无法进行。其运算规则就是左侧的num[i][j]乘右侧的num[i][j]得到的数值填入新创建的Temp.num[i][j]中。

Transpose转置就是将传入的矩阵的row作为新矩阵的col,col作为新矩阵的row,然后新创建的Temp.num[i][j]=B.num[j][i],就是这么简单粗暴。

template<typename T>
friend Matrix<T> Hadamard(const Matrix<T> &A,const Matrix<T> &B);
template<typename T>
friend Matrix<T> Transpose(const Matrix<T> &B);
//in class Matrix

template<typename T>
Matrix<T> Hadamard(const Matrix<T> &A,const Matrix<T> &B)
{
	if(!A.row||!A.col||!B.row||!B.col)
	{
		Matrix<T> NullMatrix(0,0);
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
		return NullMatrix;
	}
	else if(A.row!=B.row||A.col!=B.col)
	{
		Matrix<T> NullMatrix(0,0);
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
		return NullMatrix;
	}
	else
	{
		Matrix<T> Temp(A.row,A.col);
		for(int i=0;i<A.row;++i)
			for(int j=0;j<A.col;++j)
				Temp.num[i][j]=A.num[i][j]*B.num[i][j];
		return Temp;
	}
}

template<typename T>
Matrix<T> Transpose(const Matrix<T> &B)
{
	Matrix<T> temp(B.col,B.row);
	for(int i=0;i<B.row;++i)
		for(int j=0;j<B.col;++j)
			temp.num[j][i]=B.num[i][j];
	return temp;
}
//out of the class Matrix

不过这种通过友元函数的写法有一个比较麻烦的地方,就是和流运算符结合的时候,编译器会犯难。

例如:

Matrix<double> a(2,3);
std::cout<<Transpose(a);

这时编译器会告诉你:

[Error] no match for 'operator<<' (operand types are 
'std::ostream {aka std::basic_ostream<char>}' and 'Matrix<double>')

但是如果你写成这样:

Matrix<double> a(2,3);
a=Transpose(a);
std::cout<<a;

那么就一点问题都没有了……Hadamard同理。

然而我们可以用正规的写法来避免这种新手会遇到的困难,这是一个不使用友元函数的方法,在这个方法中,两个函数的传参数量会有所改变:

template<typename __T>
Matrix<__T> Matrix<__T>::Hadamard(const Matrix<__T>& B)
{
	Matrix<__T> NullMatrix(0,0);
	if(!this->row || !this->col || !B.row || !B.col)
	{
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
	}
	else if(this->row!=B.row || this->col!=B.col)
	{
		std::string WarningInformation="No matching matrix";
		throw WarningInformation;
	}
	else
	{
		Matrix<__T> temp(this->row,this->col);
		for(int i=0;i<this->row;++i)
			for(int j=0;j<this->col;++j)
				temp.num[i][j]=this->num[i][j]*B.num[i][j];
		return temp;
	}
	return NullMatrix;
}

template<typename __T>
Matrix<__T> Matrix<__T>::Transpose()
{
	Matrix<__T> temp(this->col,this->row);
	for(int i=0;i<this->row;++i)
		for(int j=0;j<this->col;++j)
			temp.num[j][i]=this->num[i][j];
	return temp;
}

那么我们就完成了一个简易的Matrix实现。

至于如何catch到报错信息,参考下方代码:

Matrix<double> a(1,1);
Matrix<double> b(2,3);
Matrix<double> c;
try
{
    c=a+b;//Error
}
catch(std::string WarningInformation)
{
    std::cout<<WarningInformation<<std::endl;
}

然后你就会得到:

d131977064d68a6ee98d63f219c612c8.png

2020/5/3

没想到一年之后还有小伙伴能看到我的拙作,非常感激。从一年前发的文章来看,我的水平在一年之内还是有所提高,但是回头看以前写的代码,有时候看起来真的是惨不忍睹啊……

所以还望各位同行多多关照!

(上面的代码以及github源码已经更新到目前最新的版本)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值