一.先从用malloc()函数申请多维动态函数说起。
C语言中用普通的方式定义的数组其大小不能改变。如通过int a[N];定义大小为N的整型数组,其后N的改变不会再改变原来数组大小。但我们可以通过malloc()或calloc()等动态存储分配函数申请分配一块空间,将其返回的所分配单元的起始地址赋给指针,我们就可以利用得到的指针进行和数组一样的操作。因为我们知道普通的数组名其实就是数组的起始地址,相当于一个指针常量。
1.定义一个大小为n的整型动态数组,如下:
int *a;
int n;
n = 10; //此处设为10
a =(int *)malloc(sizeof(int)*n); /*动态分配存放10个整型的内存,将所分配空间起始地址转换为
整型地址赋给指针a*/
a[5] = 100; /*引用同一般数组,但不应超出所分配内存的大小
a[5]相当于*(a+5), 即从首地址向后跳过5个(整型)内存*/
free(a); //释放a所指的内存区
在知道地址和变量类型的情况下,a[i]等同于*(a+i),即从首地址开始跳过i个已知类型的数据元素。这两个内存函数原型为void *malloc(usigned size);和void *calloc(unsigned n, unsign size);它们包含在<stdlib.h>中。两者区别有二:一是前者分配size个字节大小的连续空间,后者分配n个size字节的连续空间。二是后者分配的内存初始化为0;而前者没有;如果想改变一个已分配地址的内存区的大小可以用函数void *realloc(void *p, unsign size)。这几个函数返回void 指针类型,赋值给其他类型指针时,需经过类型转换,系统一般在编译时也进行隐式转换。
2.定义一个三维动态数组大小为n1,n2,n3
想得到多维的的动态数组,可以嵌套使用前面的方法,如将得到的动态数组的元素a[i]再定义为动态数组。即定义指向指针的指针,由外向内逐级分配空间。
int ***a; //定义指针的指针的指针
int n1, n2, n3;
int i, j, k;
scanf("%d%d%d",&n1,&n2,&n3);
a=(int ***)malloc(n1*sizeof(int **)); /*a有n1个元素,每个元素仍为数组(其实是指针的指针)
a为数组首地址,a[1],a[2],...,a[n1]为其子数组元素首地址,以此类推直至得到元素*/
for(i=0; i<n1; i++)
{
a[i]=(int **)malloc(n2*sizeof(int *)); //a[i]有n2个元素,每个元素为一维数组的首地址(其实是指针)
for(j=0; j<n2; j++)
{
a[i][j]=(int *)malloc(n3*sizeof(int)); //a[i][j]为一维数组,大小为n3
for(k=0; k<n3; k++)
{
a[i][j][k] = i*n2*n3+j*n3+k; //元素a[i][j][k]的引用同一般数组
}
}
}
.......;
//使用后要释放存储空间,注意由内向外逐级释放
for(i=0; i<n1; i++)
{
for(j=0; j<n2; j++) free(a[i][j]);
free(a[i]);
}
free(a);
多维数组分配存储空间从外往内,释放空间时要由从内往外。函数分配的内存空间,即使没有指针指向,它仍然存在,不自动回收,所以不能通过这定指针为空来释放空间。
3.从上述多维动态数组的分配了解指针与数组的关系
从外向内:下图以一个P[2][2][2]的三维动态数组举例:
上图为申请动态数组的内存结构图,从图中可以看出来,内存并不连续。
int a[3][4];
a[0][0]=4;
a[2][3]=4;
int *p=a;
//int (*p)[4]=a;
//p=a;
printf("%d%d",*(*(a+2)+3),*p);
上面这个程序可以输出结果:4 4
但是并不能通过二重指针的方式来访问对应的数组,如下打印代码:
printf("%d%d",*(*(a+2)+3),*(p+11));
要想以二重指针的方式以上面第一个格式打印出来的,得通过以下定义:
int a[3][4];
a[0][0]=4;
a[2][3]=4;
//int *p=a;
int (*p)[4]=a; /*这里很像定义了一个指针数组,但并不是,指针数组没有括号,
这里就是一个指针,就是相当于定义了一个指向四个int型长度为一个单位的指针变量p,
这样就会把二维数组按照4段一个单位,把4个单位相应的首地址存储在p中,整个的存储结构和上面的三维动态数组是一样的*/
//p=a;
printf("%d%d%d",*(*(a+2)+3),*(*(p+2)+3)),p[2][3]);
输出结果都为4;后面加一个数组下标对应二维的数量,这里就和定义二维数组初始化一样,必须要知道低维度所有的数量,才能确定这个数组的大小,这里才能知道。动态与静态数组的区别就在于数据地址的连续性。
int a[3][4];
a[0][0]=4;
a[2][3]=4;
//int *p=a;
int (*p)[4]=a;
//p=a;
int c[2] ={3};
int f[2]={1};
int *d[2]={c,f};
for (int i = 0; i < 2; i++)
{
printf("%d\n",*d[i]);
}
int r[2]={1};
for (int i = 0; i < 2; i++)
{
printf("%d\n",r[i]);
}
printf("%d%d%d",*(*(a+2)+3),*(*(p+2)+3),p[2][3]);
以上代码可以看出指针数组与动态数组的区别。
二.聊聊for循环嵌套的效率问题
因为CPU流水线和因为流水线导致需要分支预判这两个工作机制的原因,会让大部分人认为for循环嵌套总是次数多的放在内部花费的时间少,原因主要是因为,次数多的放在内部,分支预判出错的概率就会少(分支预判就是if,switch这样的有判断的),但事实并非如此,有很多因素会影响花费的时间;如果处理的指令是取内存中的数据并对其赋值等操作,因为内存的取存操作需要CPU先将内存中的数据渠道CPU中的缓存中取,缓存中的数据才是直接和CPU交换的,保证了CPU的处理速度与取数据的速度保持一致,最大的利用CPU的性能,这时因为CPU中缓存的容量很小一般有5兆左右,存储的数据都是需要经常或马上会使用的,而当对二维数组存储时,数组中同一个维度的数据会在一种算法的作用下(这个算法来维持内存和缓存中数据的稳定,是缓存中的数据保留的全是需要经常使用的)先被送到缓存中去,这时如果是直接对连续的数组数据进行操作,效率就较高,如果是分层对数组数据操作,CPU在缓存中的命中率就会低很多,不断要去内存中获取数据,效率就会低不少,所以这种情况下就不应内层次数多的时间少了;
参考如下例子:
void test1()
{
long dt = DateTime.Now.Ticks;
for (int i = 0; i < 1000000; i++)
{
for (int j = 0; j < 10; j++)
{
a[i,j] = 0;
}
}
Console.WriteLine("test1:{0}",DateTime.Now.Ticks - dt);
}
void test2()
{
long dt = DateTime.Now.Ticks;
for (int j = 0; j < 10; j++)
{
for (int i = 0; i < 1000000; i++)
{
a[i,j] = 0;
}
}
Console.WriteLine("test2:{0}",DateTime.Now.Ticks - dt);
}