C++ Assignment 之矩阵类
Part 1. Description
1. 引言
朋友们好啊,我是一名ddl战士,刚才我把C++作业赶完,有个朋友跑来问我发生甚么事了,我说我写了一个矩阵类,他一听,嗷,让我详细说说,我跟他说我这个矩阵类主要写了拷贝构造,析构,重载了赋值运算符,输出运算符,乘法,和[ ]运算符,其中拷贝构造与赋值复制的都是矩阵头部,而clone函数copy了矩阵内部数据。详细分析了 左值引用和右值引用 在程序优化方面的技巧。他非要再让我说说。
2. 构造函数
以下为矩阵类的私有成员变量
private:
int* pcount; //point to count
int row, column, length;//length=row*column
float** m_data;
此处并没有采用一维数组来模拟矩阵,所以实现起来麻烦一些,但在寻址方面比一维数组效率要高。
这里的pcount是指向一块int型内存,用来方便对矩阵头部计数,后文会展开讲解。
m_data是矩阵内部数据,用二维指针表示。关于二维指针与二维数组名,值得注意的是,二者类型并不相同,详细内容可参考这篇博文,在此不多赘述。
虽然数组指针不行,但是指针数组就可以赋给二维指针了,指针数组是一个 元素都为指针的数组,其名字就相当于指向指针的指针。我们就采用指针数组的方式构造矩阵。
m_data = new float* [row];
if (m_data == NULL)
cerr << "内存分配失败" << endl;
for (int i = 0; i < row; i++)
{
this->m_data[i] = new float[column];
if (m_data[i] == NULL)
cerr << "内存分配失败" << endl;
}
3. 带参构造与赋值运算
当涉及较大规模矩阵运算时,频繁的复制数据会极大地影响效率,所以该矩阵类的拷贝构造与赋值符号均采用共享数据内存的方式,要注意,改变其中一个矩阵的值时,其他矩阵也会改变。
采用共享内存方式随之而来的另一个问题是析构函数引发的悬垂指针,这里借鉴了openCV的方式,每一个矩阵都有自己的头部来记录该内存被共享了几次,避免内存被重复释放。头部中的pcount变量指向一块长度为1的整形堆内存,每当有一个矩阵想要共用这块内存时让其pcount=共享内存的pcount,随后便让count+1,析构时count-1,因为每一个矩阵都有这样的指针,所以当一个矩阵的pcount+1或-1时,所有与其共享内存的矩阵的pcount指向的值同时+1或-1,十分巧妙。
以下为拷贝构造,赋值运算同理。
Matrix::Matrix(const Matrix& Mat)
{
row = Mat.row;
column = Mat.column;
length = Mat.length;
pcount = Mat.pcount;
(*pcount)++;
if (Mat.length == 0)
m_data = NULL;
else
m_data = Mat.m_data;
}
4. “* ”,“[ ]”,“ <<” 重载
重载符号本身无需多言,灵活运用友元函数即可,但当考虑效率时又引出了另一个问题,也是本篇report详写部分——引用。
以下是[ ]重载,返回指针的形式可以巧妙地使用 M [row][column]来访问数据。
float* Matrix:: operator[](int r)
{
if (length == 0)
cerr << "矩阵为空!" << endl;
if (r >= this->row || r < 0)
cerr << "下标越界!" << endl;
return m_data[r];
}
如果对矩阵运算的效率要求较高的话,尤其是在做大矩阵乘法时,就不要再判断是否越界了,每次访问数据都要判断,会慢很多!所以判断是否越界就交给程序员了,效率和方便不可得兼,舍方便而取效率也。
Part 2 . Left reference and Right reference
左值(lvalue):是一个表示数据的表达式(如变量名或解除引用的指针)。程序可获取其地址。
右值(rvalue):C++11新增了右值引用,相对的,右值标识是临时性对象的表达式,这类对象没有指定的变量名,都是临时计算生成的。包括字面常量(C-字符串除外,那是地址)诸如x+y表达式及返回值的函数(条件是返回值不能是引用)。——《C++ Primer Plus》
左值引用:&
注意函数头部
Matrix(Matrix& Mat); //copy constructor
Matrix::Matrix(Matrix& Mat) //copy constructor
{
···
}
int main()
{
Matrix M1(5, 4); //construct a Matrix whose row is 5 and column is 4
Matrix M2(M1);
}
没错,这一段代码是可执行的,他的函数头部没有用到我们常常写的const Matrix& Mat 而是去掉了const,这无非就是告诉编译器我可以允许修改Mat罢了。奥秘何在?请往下看这段代码,修改了M2的构造为M1*1。
int main()
{
Matrix M1(5, 4); //construct a Matrix whose row is 5 and column is 4
Matrix M2(M1*1); //It's a left value, noted that we have overload *
}
啪的一下!很快啊!编译器一上来就报错了,塔说“类Matrix没有适当的复制构造函数”,塔说我是乱引用的,我可真是乱引用的,因为Matrix本身是一个左值,“&”符号是左值引用,所以第一段代码正常运行,而M1*1是右值,不能用左值引用去绑定一个右值,正如不能 int &a = 3 一样,Matrix*1并不是Matrix类型的变量,C++11不允许这样做(之前的标准是可以的)。
接下来是第三段代码:
Matrix(const Matrix& Mat); //copy constructor
Matrix::Matrix(const Matrix& Mat) //copy constructor
{
···
}
int main()
{
Matrix M1(5, 4); //construct a Matrix whose row is 5 and column is 4
Matrix M2(M1*1);
}
只在函数头部多了两个const,这段程序就能正常执行,const变量是一个不可修改的左值,但却能接受右值,C++11是这样规定用const修饰的引用参数:“当实参的类型正确却不是左值,或实参类型不正确但可转换为正确类型时,会生成一个临时变量。”因为加了const表明你不会修改这个参数而只是将他用来传递,所以C++认为这个创建出来的临时变量是“安全”的,是不会对程序其他部分造成影响的,因此,用const修饰后,C++将在必要时生成匿名的临时变量,从而既安全又有通用性。
生成临时变量的过程中,当类巨大无比时,会损耗很大的效率,为此使用了C++11中新增的右值引用和移动构造。
右值引用:&&
继续考虑M2=M1*2的例子
Matrix operator*(int a);
Matrix Matrix::operator*(int a)
{
Matrix M;
M.clone(*this);
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
M.m_data[i][j] = a * m_data[i][j];
}
}
return M;
}
int main()
{
Matrix M1(5, 4);
Matrix M2(M1 * 2);
cout << "Matrix 1:" << "\n" << M1 << endl;
cout << "Matrix 2:" << "\n" << M2 << endl;
}
对每个构造函数计数,并关闭返回值优化选项 观察结果
construct 1 ,2分别对应M1和重载运算符里的局部变量M,destruct 1是M的析构。destruct 3,4对应M1,M2的析构
我们发现还有一个copyconstruct 1和destruct2,这一对是在乘法函数内部创建的M返回出来构造一个临时对象产生的,临时对象销毁后就是destruct 2。copyconstruct是M2的拷贝构造。同样的程序,当我们开启返回值优化时
一个拷贝构造没有了,编译器做了一次优化,临时对象的拷贝构造优化掉了,原理大概是在这个临时对象的栈的部分直接建立M2。抛开编译器的优化不谈,能否在C++语言上找到一种更有效率的方式减少拷贝构造的次数呢?
右值优化
延长了临时变量的声明周期,使其不会立刻被销毁,而是和被绑定的变量的生命周期一样长。
添加新的构造函数
Matrix (Matrix&& Mat) noexcept; //move constructor
Matrix::Matrix (Matrix&& Mat) noexcept
{
cout << "move construct" << endl;
row = Mat.row;
column = Mat.column;
length = Mat.length;
pcount = Mat.pcount;
(*pcount)++;
if (Mat.length == 0)
m_data = NULL;
else
m_data = Mat.m_data;
}
因为M2*2是一个右值,是确定的右值引用类型。所以匹配到了移动构造函数,这里移动构造函数是浅拷贝,也避免了重新调用copyconstructor带来的内存损耗(当copyconstructor是深拷贝时),因为移动构造函数可能会改变实参,所以右值引用不应加const
左值引用的const
const Matrix& M2(M1*2);
优化效果与上面是一样的,加上const之后,如前文所叙,const就可以接受右值了。
*** 总之,将左值引用与右值引用分开处理,因为机制不同,可以分别用复制构造和移动构造实现,让我们可以更加灵活的提高程序的效率。***
Part 3 Compile
CMake 正常执行,txt文件中只有短短两行
#project name
PROJECT(Matrix)
#assign target
add_executable(Demo main.cpp Matrix.cpp)