C语言实现实数和复数矩阵及其各种运算(一)

一、前言

   本连载文章主要内容是实现复数矩阵的各种运算,并利用matlab进行联写、联调,验证C语言编写的各个矩阵运算函数的正确性。其难点笔者认为在于矩阵的各种运算,C++中有Eigen库可用,以前在学slam和做课题时候在Ubuntu下用过一段时间的Eigen,功能强大,提供了各种典型的matrix计算接口,但是在C中,需要我们自己编写每个功能模块。谨以此连载文档记录一下算法开发的关键--复数矩阵运算的实现,纯粹为了学习交流。
   网上关于C语言实现矩阵运算的demo较多,但是大体都不够完整和准确,很多function直接git下来各种问题,且用起来没有统一的标准,另外,只在实数域(Double)内讨论,但是我们知道在算法领域,很多矩阵运算都涉及到复数(Complex)运算,复数域内矩阵运算其实与实数域还是有很大区别的(PS:这也是我在写C程序时候遇到的坑,之后本文会一一介绍)。
   因此,在本连载文章中,我将完全使用C语言完成对矩阵的定义、内存初始化、访问索引、内存销毁的基本功能,更重要的是编写求取代数余子式、行列式、逆矩阵、协方差矩阵、特征值、特征向量等功能函数。所有函数demo我都单独拿出来仔细讲解,并结合matlab,编程对比验证demo正确性,大家可以放心阅读和使用。

二、知识储备和编程工具

   1. 知识储备:熟练C语言,精通指针、数组等C语言“灵魂”知识;
   2. IDE工具:Visual Studio2019Community; MATLAB2019a;
   3. C相关头文件:complex库

三、矩阵的定义

注意到社区内有小伙伴用二维数组来定义一个矩阵,但是矩阵并不总是二维的,在matlab里面,矩阵可以是N维的,若是试图用更高维度的数组去完成定义,在后续的访问矩阵/数组元胞时候显得非常冗余。虽说C是结构化程序设计语言,但一定的“鲁棒性”也是需要的。另外我是用C++和matlab居多,所以总是或多或少在编程时候有一定的C++的思想。后者面向对象,具有类模板和模板函数。
PS:在之后贴出来的code中,我都会分别给出Double和Complex两份demo,以及调用到的complex库文件的相关函数接口说明。

1.头文件

	 # include   // Complex Numbers
	 # include    // malloc/free, et.al

说明
关于复数头文件,C11开始提供了大量函数接口,这里我只对用到的单独拿出来讲一下。有兴趣的可以点开源文件看看都有什么内容。首先是定义,源文件见下:

	#ifndef _C_COMPLEX_T
    	#define _C_COMPLEX_T
    	typedef struct _C_double_complex
    	{
       	 double _Val[2];
    	} _C_double_complex;  // This is what I need

   	 typedef struct _C_float_complex
    	{
        	float _Val[2];
    	} _C_float_complex;

    	typedef struct _C_ldouble_complex
    	{
        	long double _Val[2];
    	} _C_ldouble_complex;
	#endif

	typedef _C_double_complex  _Dcomplex;  // This is what I need
	typedef _C_float_complex   _Fcomplex;
	typedef _C_ldouble_complex _Lcomplex;

简而言之,complex.h头文件通过定义一个结构体来表示一个复数。具体说来,结构体内部成员是一个一维数组,存储两个数据,由该数组来储存复数的实部(real)和虚部(imaginary),根据实/虚部浮点精度不同定义了三种数组,分别是double、float、long double,对应定义了三种结构体类型,再对应每个function都定义了三个函数。谨记,这三种常规类型指的是复数的实部和虚部的数据类型。算法中,我用到的是double类型。所以以下涉及到复数运算的我都只调用了complex.h头文件中跟double类型相关的函数。

2.定义

这里我用一个结构体来完成对矩阵的定义和“模拟”。

	typedef _Dcomplex ComplexType;   // Complex Variables
	typedef double DoubleType;       // Double Variables

	/* Complex Cell */
	typedef struct   // Matrix Structor
	{
		int row, column;  // Size
		ComplexType* arrayComplex;     // Pointer to Cell
	}Matrix;

	/* Double Cell */
	typedef struct
	{
		int row, column;
		DoubleType* arrayDouble;
	}Matrix2Double;
	/* bool */
	typedef enum 
	{ 
		False = 0, True = 1 
	}Bool;

说明
(1) 虽然complex.h头文件内已经对复数类型_C_double_complex取了别名_Dcomplex,为了区别double和complex,我对二者再次取了一个别名,遵守Google命名规则,随后的定义矩阵的两个结构体类型和bool变量的枚举类型同理;
(2) 重点来了,两种不同元素类型的矩阵,我定义了两个结构体,一个是元胞是实数类型的,一个是复数类型的。结构体内部两个int类型的member代表的是矩阵的行数(row)和列数(column),指针member用于指向存储元素/元胞(cell)的一段内存,因此矩阵元胞可以用指针来进行访问和遍历,此时内部数据结构指针是1D的,但却可以表示2D及以上的矩阵,从这里可以看到指针代替数组的优越性;
(3) 最后,笔者还定义了一个枚举类型,后续的用于安全机制的函数有用到;
(4) 可以看到,两个不同类型的矩阵其结构体其实完全一样,只是内部指针变量类型不一样。

3.初始化内存分配

首先是写一个对一个复数进行初始化的函数,因为复数也是一个结构体,当定义一个复数变量时候,最好也对其进行初始化。此外,后续涉及到复数的累加累乘,因此也需要对其赋值和初始化:

	/* Initiation of Complex Number */
	void InitComplex(ComplexType* Complex)  // Transfer of Pointer
	{
		Complex->_Val[0] = 0.0;
		Complex->_Val[1] = 0.0;

	}

然后是对矩阵进行初始化:

	/* Initiation of Complex Matrix */
	void InitComplexMatrix(Matrix* matrix, int row, int column)  // Transmiss of Pointer
	{
		int size = row * column * sizeof(ComplexType);
		if (size <= 0)
		{
			puts("ERROE: An invalid matrix!\n");
			return;
		}
		matrix->arrayComplex = (ComplexType*)malloc(size); 			// initiate pointer
		if(matrix->arrayComplex)
		{
			matrix->row = row;                           			 //  initiate row and size
			matrix->column = column;
			for (int index = 0; index < row * column; index++)       //  initiate cell
			{
				InitComplex(matrix->arrayComplex + index);  // call InitComplex() function
			}
		}
	}
	

	/* Initiation of Double Matrix */
	void InitDoubleMatrix(Matrix2Double* matrix, int row, int column)
	{
		int size = row * column * sizeof(DoubleType);
		if (size <= 0)
		{
			puts("ERROE: An invalid matrix!\n");
			return;
		}
		matrix->arrayDouble = (DoubleType*)malloc(size);
		if (matrix->arrayDouble)
		{
			matrix->row = row;
			matrix->column = column;
			for (int row_i = 0; row_i < row; ++row_i)
			{
				for (int column_j = 0; column_j < column; ++column_j)
				{
					matrix->arrayDouble[row_i * matrix->column + column_j] = 0.0;
				}
			}

		}	
	}

说明
(1) 矩阵初始化涉及到确定矩阵row/column的大小、初始化元胞,也就是对结构体变量进行初始化赋值,其核心是对指针动态申请一段内存,采用的是malloc()函数,包含在头文件stdlib.h中;
(2) 用指针访问/遍历(iteration)矩阵元胞,其实也就是指针指向的每个单元内保存的数,在initiate cell环节,这里其实有三种表示方式,主要是指针访问元素矩阵访问元胞具有不同的方式。后续访问元素/元胞时候我都是随机选一种写的(sorry,我当时写代码时候这些都不是重点,所以想到哪种写哪种),但是为了避免读者阅读混乱,这里,笔者都给出来:

	// Solution_1
	for (int index = 0; index < row * column; index++)       //  initiate cell
	{
		InitComplex(matrix->arrayComplex + index);
	}
	// Solution_2
	for (int index = 0; index < row * column; index++)
	{
		InitComplex(&(matrix->arrayComplex[index]));
	}
	// Solution_3
	for (int row_i = 0; row_i < row; ++row_i)
	{
		for (int column_j = 0; column_j < column; ++column_j)
		{
			InitComplex(matrix->arrayComplex + row_i * matrix->column + column_j);
		}
	}

(3) 另外,为了安全起见,我还加入了两个if语句,可以进一步减少最后compile时出现的各种堆栈溢出等内存bug和exception,比如free掉已经不存在的内存或者malloc失败的内存,也就是C语言使用指针出现的野指针堆错误
(4) double类型的矩阵类同complex类型,甚至在初始化值时候还要简单一点,因为每个double元胞只有一个元素,而complex有实部和虚部两个。

4.内存销毁

我们知道在Linux内,malloc()和free()系统函数总是成对出现,有内存申请必有内存的释放,同样在VS下,利用一个结构体变量定义了一个矩阵之后,调用了malloc()函数申请了一段memory,故而需要释放:

	/* Validity of Complex Matrix */
	Bool IsNullComplexMatrix(const Matrix* matrix)
	{
		int size = matrix->row * matrix->column;

		if (size <= 0 || matrix->arrayComplex == NULL)
		{
			return True;
		}
		return False;
	}	
	/* Free Memory of Complex Matrix */
	void DestroyComplexMatrix(Matrix* matrix)
	{
		if (!IsNullComplexMatrix(matrix))    // Nested Call of IsNullComplexMatrix()
		{
			free(matrix->arrayComplex);      // Pair: malloc--free
			matrix->arrayComplex = NULL;
		}
		matrix->row = matrix->column = 0;
	}
	
	
	/* Validity of Double Matrix */
	Bool IsNullDoubleMatrix(const Matrix2Double* matrix)
	{
		int size = matrix->row * matrix->column;

		if (size <= 0 || matrix->arrayDouble == NULL)
		{
			return True;
		}
		return False;
	}
	/* Free Memory of Double Matrix */
	void DestroyDoubleMatrix(Matrix2Double* matrix)
	{
		if (!IsNullDoubleMatrix(matrix))
		{
			free(matrix->arrayDouble); 
			matrix->arrayDouble = NULL;
		}
		matrix->row = matrix->column = 0;
	}

说明
(1) 首先,定义一个IsNullComplexMatrix()函数用于判断矩阵是否初始化成功,主要判断结构体内部的member是否初始化成功(体现在矩阵,就是行列数和内存)。属于保障内存安全的操作。看到没,这里我就调用了前文说到的Bool枚举类型成员;
(2) 之后,定义一个DestroyComplexMatrix()函数释放结构体内部的member,即嵌套调用free()释放掉指针成员指向的内存,并置为NULL(这步虽然不是必须的但是笔者建议各位读者每次都加上),再将两外两个int类型的member变量置为0;
(3) 关于double类型,同样给出,不再赘述。
(4) 补充一点,在我的算法框架中,出现了大量矩阵的定义,也就是多次内存的申请和释放,甚至在几个核心的算法函数内部,出现了二十个以上的矩阵,要是每个矩阵调用一次初始化和销毁函数,会严重影响编程美观和效率。因此,我又写了几个初始化(initiate)和内存销毁(destroy)函数,其目的是一次性只调用一个函数将我需要的所有的矩阵进行内存的申请和释放。当然,这个功能只在我的具体算法场景下用得到,所以我只是做一个补充拓展贴出来,另外,我只给出内存销毁函数,后续我会依次写demo验证一下这个function:

	/* Free Memory of Complex Matrice Array */
	void DestroyComplexMatrixArray(Matrix matrixArray[], int num)  // Array Transfer--->Pointer Transfer
	{
		if (num)      // if no matrix
		{
			for (int i = 0; i < num; i++)
			{
				DestroyComplexMatrix(&matrixArray[i]);  // Nested Call of DestroyComplexMatrix()
			}
		}
	}


	/* Free Memory of Double Matrice Array */
	void DestroyDoubleMatrixArray(Matrix2Double* matrixArray, int num)
	{
		if (num)  // if no cell
		{
			for (int i = 0; i < num; i++)
			{
				DestroyDoubleMatrix(&matrixArray[i]);
			}
		}
	}

5.获取矩阵行、列、元胞数目

这里我也定义了三个函数用于返回相应的值:

/* Return Matrix Row Size */
int MatrixRow(const Matrix* matrix)
{
	return matrix->row;
}
/* Return Matrix Column Size */
int MatrixColumn(const Matrix* matrix)
{
	return matrix->column;
}
/* Return Complex Matrix Size */
int MatrixSize(const Matrix* matrix)
{
	return MatrixRow(matrix) * MatrixColumn(matrix) ;   // Size Refers to Numbers of Cell of Matrix
}

说明
(1) 由于这里的矩阵笔者用的结构体来~~“模拟”~~ 实现,我们为了让矩阵看起来“更像”我们数学中那样的“格式”,数学美学要用计算机实现,还是定义了相关函数来返回我们要的量,但是事实上,可以直接采用如下格式直接获取

/** pointer     &   variable**/
matrix->row; // matrix.row
matrix->column; // matrix.column
matrix->row * matrix->column; // matrix.row * matrix.column

四、总结

这是第一章节的内容,整体上来讲,只是为了完成矩阵的定义、初始化、内存销毁等,但是非常重要,因为后续的所有矩阵运算函数和接口都基于此,事实上,我遇到的很多问题都是出现在这一部分。为后续方便读者阅读,或者帮助读者编程时候排雷,笔者对个人编程习惯和这部分遇到的坑做个总结:

  1. 尽量写void类型的函数,避免在函数内部定义local variable,也就是局部矩阵,频繁调用初始化和销毁函数,造成计算机内存频繁的申请和释放;
  2. 函数形参尽量定义为指针类型,尤其是矩阵/结构体变量,因为是void函数,没有return返回值,因此用传地址方式而不能采用传值操作,避免出错;
  3. 调用函数时候,实参尽量定义为一般变量,尤其是矩阵/结构体变量,这样可以避免结构体指针变量本身就需要分配和释放内存,关于这一点,由于笔者使用C语言编程经历真心不多,所以当时把实参也清一色定义为指针,造成后续在每个函数里面出现了大量结构体指针初始化、结构体内部指针初始化的操作,矩阵多了之后就很混乱,如果分配和释放不及时,会造成编译时候cast各种exception,这也是我当时遇到过的第一个大坑;
  4. 因为const在C++里面意义非常丰富,在C里面,我也习惯性只要是在函数内部不改变传入的实参值的,都在定义函数时为形参加上一个const。这样可以避免在复杂的函数嵌套调用实参和形参传递矩阵运算过程中出现对本不想修改的变量进行修改的情况,造成结果不可预测。事实上,只要是不会修改的实参,我们在定义形参的时候都应为其加上一个const;
  5. 在定义函数内部,只要是传入了一个实参矩阵,我都对其进行是否为空的判断(调用一个IsNullComplexMatrix()函数),再进行下面的功能实现。事实上,编程时候,都应该在code内部加上一些安全性的操作逻辑;
  6. 下一章我将详细给出实数、复数域上矩阵运算的函数,敬请期待…
    传送门:(二)…

你可能感兴趣的:(实习项目--C语言矩阵运算,c语言,指针,算法,矩阵)