STL
ポインター演算/動的メモリ割り当てを使用するか、代わりにベクトルなどの手法を使用して、C++ で整数の 3 次元配列を実装する安全な方法を見つけたいと思います。
基本的に、整数配列の次元を次のようにします。
[ x ][ y ][ z ]
x と y の範囲は 20 ~ 6000 z は既知で、4 に等しい。
STL
ポインター演算/動的メモリ割り当てを使用するか、代わりにベクトルなどの手法を使用して、C++ で整数の 3 次元配列を実装する安全な方法を見つけたいと思います。
基本的に、整数配列の次元を次のようにします。
[ x ][ y ][ z ]
x と y の範囲は 20 ~ 6000 z は既知で、4 に等しい。
Boost多次元配列ライブラリを見てください。以下に例を示します (Boost のドキュメントから抜粋):
#include "boost/multi_array.hpp"
int main() {
// Create a 3D array that is 20 x 30 x 4
int x = 20;
int y = 30;
int z = 4;
typedef boost::multi_array<int, 3> array_type;
typedef array_type::index index;
array_type my_array(boost::extents[x][y][z]);
// Assign values to the elements
int values = 0;
for (index i = 0; i != x; ++i) {
for (index j = 0; j != y; ++j) {
for (index k = 0; k != z; ++k) {
my_array[i][j][k] = values++;
}
}
}
}
以下は、配列ごとに1つのメモリチャンクでCまたはC++を使用して3D配列を作成する簡単な方法です。BOOSTを使用する必要はありません(それが良い場合でも)、または複数の間接参照を持つ行間で割り当てを分割する必要はありません(これは通常、データにアクセスするときに大きなパフォーマンスの低下をもたらし、メモリを断片化するため、非常に悪いです)。
理解する唯一のことは、多次元配列のようなものはなく、(配列の)配列の配列だけであるということです。最も内側のインデックスは、メモリ内で最も遠いものです。
#include <stdio.h>
#include <stdlib.h>
int main(){
{
// C Style Static 3D Arrays
int a[10][20][30];
a[9][19][29] = 10;
printf("a[9][19][29]=%d\n", a[9][19][29]);
}
{
// C Style dynamic 3D Arrays
int (*a)[20][30];
a = (int (*)[20][30])malloc(10*20*30*sizeof(int));
a[9][19][29] = 10;
printf("a[9][19][29]=%d\n", a[9][19][29]);
free(a);
}
{
// C++ Style dynamic 3D Arrays
int (*a)[20][30];
a = new int[10][20][30];
a[9][19][29] = 10;
printf("a[9][19][29]=%d\n", a[9][19][29]);
delete [] a;
}
}
実際の問題については、2つの未知の次元が存在する可能性があるため、私の提案には1つの未知の次元しか許可されないという問題があります。それを管理する方法はいくつかあります。
良いニュースは、変数の使用がCで機能するようになったということです。これは、可変長配列と呼ばれます。詳細はこちらをご覧ください。
int x = 100;
int y = 200;
int z = 30;
{
// C Style Static 3D Arrays
int a[x][y][z];
a[99][199][29] = 10;
printf("a[99][199][29]=%d\n", a[99][199][29]);
}
{
// C Style dynamic 3D Arrays
int (*a)[y][z];
a = (int (*)[y][z])malloc(x*y*z*sizeof(int));
a[99][199][29] = 10;
printf("a[99][199][29]=%d\n", a[99][199][29]);
free(a);
}
C ++を使用する場合、最も簡単な方法は、おそらく演算子のオーバーロードを使用して配列構文に固執することです。
{
class ThreeDArray {
class InnerTwoDArray {
int * data;
size_t y;
size_t z;
public:
InnerTwoDArray(int * data, size_t y, size_t z)
: data(data), y(y), z(z) {}
public:
int * operator [](size_t y){ return data + y*z; }
};
int * data;
size_t x;
size_t y;
size_t z;
public:
ThreeDArray(size_t x, size_t y, size_t z) : x(x), y(y), z(z) {
data = (int*)malloc(x*y*z*sizeof data);
}
~ThreeDArray(){ free(data); }
InnerTwoDArray operator [](size_t x){
return InnerTwoDArray(data + x*y*z, y, z);
}
};
ThreeDArray a(x, y, z);
a[99][199][29] = 10;
printf("a[99][199][29]=%d\n", a[99][199][29]);
}
上記のコードには、InnerTwoDArrayにアクセスするための間接コストがあります(ただし、優れたコンパイラーはおそらくそれを最適化できます)が、ヒープに割り当てられた配列に1つのメモリチャンクのみを使用します。これは通常、最も効率的な選択です。
明らかに、上記のコードがまだ単純で単純であっても、STLまたはBOOSTはそれをうまく実行するため、車輪の再発明を行う必要はありません。簡単にできることを知るのは今でも面白いと思います。
角かっこの各ペアは、逆参照操作です (ポインターに適用された場合)。例として、次のコード行のペアは同等です。
x = myArray[4];
x = *(myArray+4);
x = myArray[2][7];
x = *((*(myArray+2))+7);
提案された構文を使用するには、最初の逆参照から返された値を逆参照するだけです。
int*** myArray = (some allocation method, keep reading);
//
// All in one line:
int value = myArray[x][y][z];
//
// Separated to multiple steps:
int** deref1 = myArray[x];
int* deref2 = deref1[y];
int value = deref2[z];
この配列を割り当てるには、実際には整数の 3 次元配列を持っていないことを認識する必要があります。整数の配列の配列の配列があります。
// Start by allocating an array for array of arrays
int*** myArray = new int**[X_MAXIMUM];
// Allocate an array for each element of the first array
for(int x = 0; x < X_MAXIMUM; ++x)
{
myArray[x] = new int*[Y_MAXIMUM];
// Allocate an array of integers for each element of this array
for(int y = 0; y < Y_MAXIMUM; ++y)
{
myArray[x][y] = new int[Z_MAXIMUM];
// Specify an initial value (if desired)
for(int z = 0; z < Z_MAXIMUM; ++z)
{
myArray[x][y][z] = -1;
}
}
}
この配列の割り当て解除は、割り当てと同様のプロセスに従います。
for(int x = 0; x < X_MAXIMUM; ++x)
{
for(int y = 0; y < Y_MAXIMUM; ++y)
{
delete[] myArray[x][y];
}
delete[] myArray[x];
}
delete[] myArray;
ベクトルの場合:
std::vector< std::vector< std::vector< int > > > array3d;
要素が既に追加されている場合、すべての要素は array3d[x][y][z] でアクセスできます。(例: push_back 経由)
STL を使用してメモリを管理することには、new/delete を使用するよりも多くの利点があります。データを表現する方法の選択は、それをどのように使用する予定かによって異なります。1 つの提案は、実装の決定を隠し、3 次元の get/set メソッドを 1 次元の STL ベクトルに提供するクラスです。
カスタム 3d ベクター タイプを作成する必要があると本当に信じている場合は、まず Boost を調べてください。
// a class that does something in 3 dimensions
class MySimpleClass
{
public:
MySimpleClass(const size_t inWidth, const size_t inHeight, const size_t inDepth) :
mWidth(inWidth), mHeight(inHeight), mDepth(inDepth)
{
mArray.resize(mWidth * mHeight * mDepth);
}
// inline for speed
int Get(const size_t inX, const size_t inY, const size_t inZ) {
return mArray[(inZ * mWidth * mHeight) + (mY * mWidth) + mX];
}
void Set(const size_t inX, const size_t inY, const size_t inZ, const int inVal) {
return mArray[(inZ * mWidth * mHeight) + (mY * mWidth) + mX];
}
// doing something uniform with the data is easier if it's not a vector of vectors
void DoSomething()
{
std::transform(mArray.begin(), mArray.end(), mArray.begin(), MyUnaryFunc);
}
private:
// dimensions of data
size_t mWidth;
size_t mHeight;
size_t mDepth;
// data buffer
std::vector< int > mArray;
};
3 番目の (そして最下位の) 次元が既知であるため、すべての意図と目的のために、2D 配列のみを扱っていることに注意してください。
配列の各次元に含まれるエントリの数が事前にわからない場合、STL または Boost を使用することは非常に優れた方法です。これらの方法を使用すると、動的なメモリ割り当てが得られるためです。データ セットが大部分が静的なままである場合、またはほとんどが新しいエントリのみを受け取り、多くの削除がない場合。
ただし、事前にデータセットについて何か知っている場合 (たとえば、保存される項目の総数や、配列にまばらにデータが入力される場合など) は、ある種のハッシュ/バケット関数を使用したほうがよい場合があります。キーとしての XYZ インデックス。この場合、次元あたりのエントリが 8192 (13 ビット) 以下であると仮定すると、40 ビット (5 バイト) のキーで十分です。または、常に 4 x Z エントリがあると仮定すると、単純に 26 ビットの XY キーを使用します。これは、速度、メモリ使用量、および動的割り当ての間のより効率的なトレードオフの 1 つです。
もちろん、Pieter の提案は良いことですが、覚えておくべきことの 1 つは、大きな配列の構築の場合、かなり遅くなる可能性があるということです。ベクトル容量が変更されるたびに、すべてのデータをコピーする必要があります (ベクトルの「n」個のベクトル)。