文不可无观点,观点不可无论据。

转载请注明出处

MATLAB功能强大,编程方便,是国际广泛使用的计算软件。目前已有很多书籍介绍其在工程上的应用,但很少有从程序设计语言的角度写的书或文章。

MATLAB的核心之处为其一切皆为数组的设计,我们将先从其外在表现形式,再从内存中数据存储的研究展示这一点。

外在表现形式

在命令窗口输入

此时工作空间中增加了一个名称为a,值为3的变量。在命令窗口输入

a(1,1,1,1,1,1,1)

均返回3。从表现看,输入单个数值的a的看起来像一个数,又像一维数组,又像二维数组,还可以为n维数组。n最大值是多少呢?在文档中没有查到,执行

a=3;n=10^4;num2cell(ones(1,n));a(ans{:})=4

看来到了1万维还可使用。再执行

a=[1 2 3;4 5 6];

a(1,2) % 返回2

a(1,2,1)  % 返回2

a(3)  % 返回2

a(1,4) % 错误??? Attempted to access a(1,4); index out of bounds because size(a)=[2,3].

a(7)  % 错误??? Attempted to access a(7); index out of bounds because numel(a)=6.

a(1,1,2) % 错误??? Attempted to access a(1,1,2); index out of bounds because size(a)=[2,3,1].

看起来MATLAB将值存储为数组后,会记录数据各维的长度,但并不区分维数。其可以视为一维向量访问,只要总维度不超过矩阵元素总数即可;可以视为二维矩阵访问,此时会检测数组下标是否越界;还可以作为n维矩阵访问,只要其后维数为1即可。

MATLAB提供了函数sub2ind和ind2sub完成1维到n维之间的转换。

a=[1 2 3;4 5 6];

[i,j,k]=ind2sub(size(a), 3)

sub2ind(size(a),1,2,1)

另外,还有reshape函数,它用于更改MATLAB记录的维数信息,而不会改变数组值的排列本身。

a=[1 2 3;4 5 6];

b=reshape(a,3,2)  % b=[1 5;4 3;2 6]

b(3)  % 返回2,与a(3)相同

一般而言,数组生成多少维,我们就用多少维访问,这样最自然。但事实上一些常用法,可能在我们不知情的情况下使用了一维的访问方法,譬如无论对于行向量或列向量,我们都用一个下标访问,实际上行向量a访问为a(1,i),又譬如

a=[5 7;9 10];

find(a>8)    % 输出[2;4],已经没有矩阵概念

a(find(a>8))=2   % 输出[5 7;2 2]

以上怎么深入理解呢?MATLAB的15种数据类型,包括标量、向量、矩阵、字符串、元胞数组、结构体、对象,最终均组成矩阵或向量,这个矩阵和向量在MATLAB中称为数组(array),它是MATLAB唯一的数据类型,所有的MATLAB变量,均使用MATLAB数组格式存储。

MATLAB内一切数据皆为数组!

记住此,对理解MATLAB大有裨益;理解此,对使用MATLAB大有裨益。

内部数据存储

为达到一切数据皆为数组,MATLAB使用了一套数据结构管理这些数据类型,但这个内容已经被MATLAB包装了,根据Help文档描述(External Interfaces/Calling C/C++ and Fortran Programs from MATLAB Command Line/MATLAB Data),包装内容包括:数据的类型,维度,存储的数据地址,实数或复数(如果为数值),非零最大元素(如果为稀疏矩阵),域的数目及名称(如果为结构体或对象)。

但MATLAB自身没有提供内部数据的说明,也没有提供函数接口。它具体是如何组织的,对于MATLAB调用而言是一个黑盒子。

无论如何,数据都按二进制存储在计算机内存,利用MATLAB的C语言接口(用法见文末),可以读出并解析MATLAB内部数据储存格式。

MATLAB安装的示例程序(example)提供了explore.c文件,提供了关于MATLAB内部数组的相关信息。但源程序较长(616行),逻辑较多。此处采用简化程序研究MATLAB内部存储数据格式。值得注意的是,这种行为仅用于简单了解和研究用途,其本身是不被提倡的,对其不当使用有可能造成MATLAB的崩溃。

dispaddr.cpp程序(完成后输入mex dispaddr.cpp编译此程序,下同)

#include "mex.h"

typedef unsigned __int64 uint64;

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])

if(nrhs<1) mexErrMsgTxt("输入一个变量作为参数.\n");

mxPrintf("%p\n", prhs[0]);

第1行,引入头文件;

第2行,定义数据格式,本处定义了64位的无符号整型变量(其中_intxx为Microsoft C++定义,不用考虑使用short, int, long等哪个是几位的了。也许,其它编译器是不认的,那就改为unsigned long long)。这是由于目前大部分计算机为64位,并安装了64位的MATLAB,CPU寄存器为8个字节(64位),程序中使用8个字节表达整型和指针较为高效和方便。对于32位的MATLAB,当将其指针表达为64位时,只需在前4个字节补充0。因此,使用64位的变量可适用于目前的32位和64位程序;

第4行,入口函数;

第6行,如果未输入变量,调用MATLAB定义的宏输出错误信息。本句函数与MATLAB内部的error(‘输入一个变量作为参数.’)效果一致;

第7行,调用mxPrintf宏打印字符,它会调用C语言的printf函数,但printf本身已被连接到MATLAB内部,此时无需再连接stdio库了。printf的%p描述符表示使用16进制格式打印变量的地址,后面还要用到的%x描述符表示使用16进制格式打印变量的值。

第7行中最重要的为prhs[0],它指输入的第一个变量的地址。正如前述,这个地址及其内容已经被MATLAB包装了,它是如何组织的,对于MATLAB调用而言是一个黑盒子。要解开黑盒子,就要先知道黑盒子在哪,具体就是参数的地址入口。

完成后在command window中输入mex dispaddr.cpp编译此程序。

getaddr.cpp

#include "mex.h"

typedef unsigned __int64 uint64;  // Microsoft c++ defined

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])

if(nrhs<1) mexErrMsgTxt("输入一个变量作为参数.\n");

plhs[0]=mxCreateNumericMatrix(1, 1, mxUINT64_CLASS, mxREAL);  // 创建一个mxArray*类型的64位整型变量

uint64* pointer = (uint64*) mxGetPr(plhs[0]);     // pointer为指向plhs[0]数据的地址

*pointer=(uint64)prhs[0];    // prhs[0]为mxArray*指针,将之强制转换为整型,并赋值到plhs[0]处

第8行创建一个MATLAB数组作为输出,既然MATLAB自身使用了一套数据结构存储数据,那么自然是使用其自带函数创建数组了。这个函数需要返回变量地址的值,看起来像一个1×1的64位的整型数值,因此使用了mxCreateNumericMatrix函数,其函数原型为

mxArray *mxCreateNumericMatrix(mwSize m, mwSize n, mxClassID classid, mxComplexity ComplexFlag);

其前两个参数表示维数,第3个表示变量类型,最后一个表示为实数还是复数;

第10行得到输出数组的数据写入地址,既然MATLAB使用一套数据结构管理数据,那具体数据写入地址同样需要通过其提供的接口API给出,就是mxGetPr,其原型为

double *mxGetPr(const mxArray *pm);

返回的为双精度实数类型的指针,但我们需要的是64位整型指针,因此需要进行一个强制类型转换;

第11行,将输入变量地址写入此指针。这个函数返回值就是这个地址。

完成后在MATLAB中输入mex getaddr.cpp编译此函数。

在MATLAB中输入

dispaddr(a);   % 输出BD8A738

getaddr(a)   % 输出0

可获得注释后的输出。注意每台机器、每次运行的输出均不一致,这是由于变量是操作系统动态申请的,申请时地址均不仅相同。

看起来两者输出并不一致,这是因为dispaddr输出的为16进制,而getaddr输出的为10进制。输入dec2hex(0),或hex2dec('BD8A738'),即可发现两者是一致的。

无庸质疑,mxArray必然为一个结构体,其中含有一系列的变量和指针,指针又指向一个又一个结构体,MATLAB并未披露mxArray的信息,甚至其大小。为确定mxArray长度,假设申请了一个变量,然后再次申请一个变量,那么根据操作系统分配,有可能存在两个变量正好相邻的情况。因此输入

n=1e2;p=zeros(n,1);

for i=1:n eval(sprintf('a%d=i;p(i)=getaddr(a%d);', i, i));end

min(diff(sort(p)))

在32位系统上,运行多次后,发现输出均为固定的数值56,因此,基本可以确定32为系统上,MATLAB 2010b的mxArray数据结构大小为56字节,以下只需要考虑如何显示这个56字节。

dispmem.cpp

#include "mex.h"

#include "windows.h"

typedef unsigned __int64 uint64;

typedef unsigned __int8  uint8;

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])

if (nrhs < 1 || !mxIsUint64(prhs[0])) mexErrMsgTxt("第一个参数必须是64位的地址参数");

if (nrhs < 2 || !mxIsDouble(prhs[1])) mexErrMsgTxt("第二个参数必须是double型的大小值");

int nbytes=(int)(mxGetScalar(prhs[1])+1e-5); // 得到显示的总数目

uint64* data=(uint64*)mxGetData(prhs[0]); // 得到输入变量存储数值的地址

uint8* mem=(uint8 *)data[0]; // 得到其值,并认为是1个字节的变量

if(IsBadReadPtr((void*)mem, nbytes)) mexErrMsgTxt("内存不可读\n"); // 监测内存是否可读,非常重要!!!!!

for(int i=0;i<nbytes;i++)

mexPrintf("%02x ", mem[i]);  // 逐字节输出

if( (i+1)%16==0) mexPrintf("%\n"); // 每16个字节回车以利于显示

else if( (i+1) %8==0) mexPrintf(" "); // 每8个字节加个空格以利于显示

mexPrintf("%\n");

// 将刚才的显示信息输出,以利于后续使用

char* str=new char[nbytes*2];char s[2]; // 申请内存空间

for(int i=0;i<nbytes;i++)

sprintf(s, "%02x", mem[i]); //利用了输出的16进制功能

str[2*i]=s[0]; // 将内容输出到变量利于后续访问

str[2*i+1]=s[1];

plhs[0]=mxCreateString(str); // 将C语言的char*类型转为MATLAB的字符串

delete str;  // 删除动态分配的内存

第8、9行进行判断,确认第1个输入参数是否为64位无符号的地址,第2个参数是否为双精度实数;

第11行得到需要显示的总字节数目;

第13行得到输入的变量(地址)的数据存储的地方。这是由于输入的第1个参数看起来为一个地址,但实际上在MATLAB内部,它仍然被存储为一个复杂的数据结构,必须一点一点地剥去外衣,得到真正的数据所在。这个数据就是mxGetData得到的地址的第1个元素;

第14行,得到这个地址,然后告诉编译器这个地址是8位的无符号数,这样就可以一个字节一个字节第输出这个数了;

第15行,调用windows的API函数,监测内存是否可读,不可读的内存段将导致MATLAB的崩溃。此处IsBadReadPtr(const void*, int)检测程序能否读取制定的内存段。Mac和Linux中无此函数,需要手动完成,如分析/proc文件、读取内存后分析操作系统信号,以及使用access(const char*)函数辅助分析(此方法不完全正确,但基本可用)等;

第19行,逐字节输出每个字节的16进制表达式,由于addr是unsigned char型的,C语言自然知道是逐字节输出;

第26行,将刚才打印的内容置入输出,其语法与上和getaddr.cpp类似;

第33行,mxCreateString表示创建MATLAB字符串,并将C语言的str复制过来。

完成后在MATLAB中输入mex dispmem.cpp编译此程序。

在MATLAB中输入

1. a=zeros(5,8);

2. dispmem(getaddr(a), 56)  % 显示变量a的mxArray内数据,如为64位系统或其它版本,数值不一定为56。

MATLAB输出显示

00 00 00 00 06 00 00 00  00 00 00 00 00 00 00 00

02 00 00 00 00 00 00 00  00 02 00 00 05 00 00 00

08 00 00 00 50 73 4b 1d  00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00

看到了05和08,看起来像维度。而 50 73 4b 1d像一个指针。稍微改一下dispaddr.cpp为dispdataaddr.cpp

dispdataaddr.cpp

#include "mex.h"

typedef unsigned __int64 uint64;

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])

if(nrhs<1) mexErrMsgTxt("输入一个变量作为参数.\n");

mexPrintf("%p\n", mxGetData(prhs[0])); // 比dispaddr多了一个mxGetData

上述程序仅比dispaddr多了一个mxGetData,输入

dispdataaddr(a)

输出1D4B7350,看起来和50 73 4b 1d有点像,只是顺序不一样。实际上,这两个完全是一个东西。它和数据在计算机内的存储方式有关。

在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。

目前在各种体系的计算机中,通常采用big-endian和little-endian两种字节存储机制描述在多字节数中各个字节的存储顺序。

1. Little-endian:将低序字节存储在起始地址(低位编址),上述编码就是LE,x86采用LE编码顺序

2. Big-endian:将高序字节存储在起始地址(高位编址)

辅助资料:Matlab的C语言调用接口

MATLAB可调用C语言程序,接口的入口函数如下

void mexFunction(

int nlhs, mxArray *plhs[],

int nrhs, const mxArray *prhs[])

/* more C/C++ code ... */

即入口名称为mexFunction,无返回值,其参数包含

函数右侧的输入参数数组

函数左侧的输出参数数组

右侧参数数目,即prhs数组的维数

左侧参数数目,即plhs数组的维数

以X = myFunction(Y, Z)函数为例,其左端含1个参数,右端为2个参数,对应数组如下所示:

MATLAB的C语言接口函数参数数组示意图

在使用前,安装任一C语言编译器,如gcc、visual studio等,在MATLAB输入mex –setup命令配置编译器,即可编译、运行编写的C语言程序。

其具体使用可参见帮助MATLAB/User’s Guide/External Interfaces/Creating C/C++ Language MEX-Files/C/C++ Source MEX-Files。

往期文章:

微信扫一扫

关注“理念世界的影子”

版权声明:

本文是"洞穴之外"作者原创文章,欢迎转载,须署名并注明来自“理念世界的影子”公众号。