记住,内存只是内存。听起来平凡,但许多人似乎认为在C语言中分配内存和管理内存是某种魔法。事实并非如此。在一天结束时,你所需的内存就是你分配的,完成后就释放它。
所以从最基本的问题开始:如果需要 'n' 个 double
值,你会如何分配它们?
double *d1d = calloc(n, sizeof(double));
free(d1d);
很简单。下一个问题分两部分,其中第一部分与内存分配无关:
- 大小为
m*n
的2D数组中有多少个double
值?
- 我们如何分配足够的内存来容纳它们。
答案:
- 在大小为
m*n
的m*n 2D双精度矩阵中,有m*n
个双精度浮点数。
- 分配足够的内存以容纳(m*n)个双精度浮点数。
看起来很简单:
size_t m=10;
size_t n=20;
double *d2d = calloc(m*n, sizeof(double));
但是我们如何访问实际元素呢?需要进行一些数学计算。知道m
和n
后,你可以简单地执行以下操作:
size_t i = 3; // value you want in the major index (0..(m-1)).
size_t j = 4; // value you want in the minor index (0..(n-1)).
d2d[i*n+j] = 100.0;
有没有更简单的方法来做这件事?在标准C中,有;在C++中,没有。标准C支持一个非常方便的功能,可以生成正确的代码来声明动态大小的可索引数组:
size_t m=10;
size_t n=20;
double (*d2d)[n] = calloc(m, sizeof(*d2d));
不能再强调一下:标准C支持这种方法,但是C++不支持。如果您正在使用C++,您可能需要编写一个对象类来为您完成所有操作,因此除此之外将不再提到此方法。
那么上面的代码究竟是做什么的呢?首先,显而易见的是我们仍然分配了与之前相同数量的内存,即m*n个元素,每个元素的大小为sizeof(double)。但是你可能会问自己,“变量声明是什么意思?”这需要一些解释。
这里有一个明显的区别:
double *ptrs[n]; // declares an array of `n` pointers to doubles.
并且这个:
double (*ptr)[n]
编译器现在知道每行有多少个双精度数字(
n
个),因此我们现在可以使用
两个索引来引用数组中的元素:
size_t m=10;
size_t n=20;
double (*d2d)[n] = calloc(m, sizeof(*d2d));
d2d[2][5] = 100.0; // does the 2*n+5 math for you.
free(d2d);
我们能将其扩展到3D吗?当然可以,数学会变得有些奇怪,但仍然只是向大块RAM中进行偏移计算。首先是“自己做数学”的方法,使用[i,j,k]索引:
size_t l=10;
size_t m=20;
size_t n=30;
double *d3d = calloc(l*m*n, sizeof(double));
size_t i=3;
size_t j=4;
size_t k=5;
d3d[i*m*n + j*m + k] = 100.0;
free(d3d);
你需要花一分钟仔细观察这个公式,才能真正理解它是如何计算那个大块 RAM 中的
double
值所在位置的。使用上述尺寸和所需索引,"raw" 索引为:
i*m*n = 3*20*30 = 1800
j*m = 4*20 = 80
k = 5 = 5
======================
i*m*n+j*m+k = 1885
所以我们正在访问那个大线性块中的第1885个元素。让我们再做一次。[0,1,2]怎么样?
i*m*n = 0*20*30 = 0
j*m = 1*20 = 20
k = 2 = 2
======================
i*m*n+j*m+k = 22
即线性数组中的第22个元素。
现在应该显而易见,只要您保持在数组的自定义范围内,i:[0..(l-1)], j:[0..(m-1)], and k:[0..(n-1)]
任何有效的索引三元组都将定位到线性数组中唯一的一个值,而其他有效的三元组也不会定位到这个值。
最后,我们使用与2D数组相同的数组指针声明,但将其扩展为3D:
size_t l=10;
size_t m=20;
size_t n=30;
double (*d3d)[m][n] = calloc(l, sizeof(*d3d));
d3d[3][4][5] = 100.0;
free(d3d);
再次强调,这实际上只是我们手工计算时所做的相同数学运算,但现在是让编译器为我们完成。
我意识到这可能有点难以理解,但这很重要。如果你需要连续存储的内存矩阵(比如向OpenGL等图形渲染库提供矩阵),你可以使用以上技术相对轻松地实现。
最后,你可能会想为什么有人会一开始就使用指针数组、指向指针数组的指针、指向指针数组的指针数组等等这些东西,而不直接这样做呢?有很多原因。例如,如果你要替换行,交换指针很容易;但复制整行非常耗费资源。还有一个不太明显的答案:如果行之间的宽度需要彼此独立(例如,第0行可以是5个元素,第1行可以是6个元素),那么固定大小为l * m * n
的分配方法就行不通了。
祝你好运。