和我一起写矩阵类(四)
大连理工大学软件学院WAKU(转载请保留署名)
这次来写矩阵的运算。先从最简单的开始。
1. 获得矩阵的行和列
template <class T>
int CMatrix<T>::GetM()
{
return m;
}
template <class T>
int CMatrix<T>::GetN()
{
return n;
}
这个。。。一点技术含量都没有不多说了。
2.下标运算符
矩阵的主要数据都是放到二维数组里,在二维数组里我们可以用array[0][0]这种非常直观的形式表示数组里的元素。我们的矩阵要想和用二维数组一样舒服就得重载[]运算符,代码出奇的简单:
template <class T>
T*& CMatrix<T>::operator[](int i)
{
return p[i];
}
天啊!T*&这是什么东西啊?呵呵,觉得是天书就说明你C++没学好,要打PP哦~~
T*是T类型的指针,这没什么疑义吧?&不是取地址符,而是“引用”。引用说白了就是给一个变量起个外号,比如:
int a = 3;
int& b = a;
cout<<b<<endl;
a = 4;
cout<<b<<endl;
程序输出3和4,这里b就是a的外号,除了名字外,a和b是一样的。
既然引用是一个外号,那么所有变量出现的地方都可以换成该变量的引用,函数返回的变量也不例外。现在请跟随我的思路推导一个结论:变量可以赋值->引用是变量的外号,那么也可以给引用赋值->函数可以返回引用,所以也可以给函数的返回值赋值。
比如:
int a = 0;
int& function(){return a;}
void main()
{
function() = 3;
cout<<a<<endl;
}
a是一个全局变量,function函数返回a的引用,那么function() = 3就等价于a = 3,输出3就顺理成章了。
函数放到等号的左边真是要多别扭就有多别扭,不过回到我们的矩阵类上的[]运算符重载你就会发现这种技术是那么亲切。
运算符重载也是一个函数,如果已经定义了名为matrix的矩阵,matrix[0]就相当于调用matrix.operator[](0)这个函数。T*&返回了对p[i]的引用,前面说过p[i]是一个行指针,我们可以用p[0][0]来使用二维数组里的一个元素。如果返回的是p[0]的引用;那么matrix[0][0]就相等于p[0][0],这不就是数组里的第一个元素吗?而且你想让matrix[0][0]出现在等号的哪边都可以,和p[0][0]的用法完全一样。
3.转置
把行变成列,把列变成行的一种运算。
template <class T>
void CMatrix<T>::Transpose()
{
int i, j;
T t;
if (m == n) //方阵
{
for (i = 0; i < m; i++)
{
for (j = i; j < m; j++)
{
t = p[i][j];
p[i][j] = p[j][i];
p[j][i] = t;
}
}
}
else //非方阵
{
CMatrix<T> t(n, m);
for (i = 0; i < m; i++)
{
for (j = 0; j < n; j++)
{
t[j][i] = p[i][j];
}
}
*this = t;
}
}
虽然非方阵的代码完全适合方阵矩阵的转置,不过效率差了很多,多敲几行代码换来效率的提升是非常划算的。所以我分两种情况来实现转置。
对于方阵转置,由于行和列是相等的,所以只需做数值上的交换即可。两个for循环遍历了矩阵的上三角阵,交换p[i][j]和p[j][i]完成转置。由于我们已经重载了[]运算符,所以这里不会有任何问题。
非方阵的转置看起来有点麻烦,首先创建了一个临时的矩阵对象t,并用两个参数的构造函数创建了一个nxm的0矩阵。然后也是两个for循环,用p[i][j]给t[j][i]赋值结束后,t就是矩阵的转置了。
this指针指向了调用此函数的对象,那么*this就代表这个对象,很简单。
=运算符是无论简单类型(int, float)还是自定义的类对象都可以无须定义就可使用的一个运算符,默认的=只是简单的内存块复制,你那个对象有啥,我就一模一样的盗版过来。一般来说,这么做没什么问题,但是一旦数据成员中有指针的时候使用=运算符一定要留心。
还记得我们的动态二维数组吧,就是用T** p表示的那个~~。比如一个矩阵对象名字叫a,它里面的p指向了起始地址为1000的二维数组。这时候我又创建了一个矩阵对象b,并让b=a。由于默认的=会完全复制,造成的结果就是b里面的p也指向了起始地址为1000的二维数组。你改变了a,那b就跟着也被改了,更严重的是,一旦a销毁了,你那个可怜的b还以为自己的日子过的不错。你再使用b的时候,WINDOWS会给你一张很难看的脸。
上段代码的*this = t也有这个毛病,想解决这个问题。就必须重载=号运算符:
template <class T>
void CMatrix<T>::operator=(CMatrix<T>& a)
{
int i, j;
if (p)
{
for (i = 0; i < m; i++)
{
delete[] p[i];
}
delete[] p;
}
m = a.GetM(); n = a.GetN();
p = new T*[m];
for (i = 0; i < m; i++)
{
p[i] = new T[n];
for (j = 0; j < n; j++)
{
p[i][j] = a[i][j];
}
}
}
程序先释放二维数组的空间,然后又申请了一个和引用传递进来的矩阵a一样大小的空间,并用a给对象赋值。这样就复制了一个一模一样的矩阵,而且每个矩阵都有自己的空间,就不会出现冲突的问题了。
这个函数的形参是引用传递的。如果形参是一个对象,使用引用传递可以避免一次拷贝构造函数的调用,从而提供效率,不过要留心对象有可能被更改。
说到拷贝构造函数,就会有和上面类似的问题产生:
CMatrix<int> a(2, 2);
CMatrix<int> b(a);
第二条语句就调用了拷贝构造函数,如果你不提供拷贝构造函数,编译器会为你提供一个,不过这个和默认的=运算符有一样的隐患,所以让我们也把它写出来:
template <class T>
CMatrix<T>::CMatrix(CMatrix<T>& a)
{
int i, j;
m = a.GetM(); n = a.GetN();
p = new T*[m];
for (i = 0; i < m; i++)
{
p[i] = new T[n];
for (j = 0; j < n; j++)
{
p[i][j] = a[i][j];
}
}
}
因为对象创建之间肯定不会占用内存,所以释放内存这步就可以省略,其他的就和=运算符里的一样了。
一个转置牵扯出这么多问题,有点出乎意料吧?但是我们经过自己的努力,一个个的把它们解决掉,这种成就感是无可比拟的。这次就先到这,下次我们会完成最后的加,减和乘法的运算,都是比较简单的。88~~