3

我正在尝试制作一个数据结构来表示 C++ 中的迷宫。

我需要保存的关于迷宫的所有数据都可以使用按位运算存储在 16 位整数中(以表示迷宫的每个单元格):(来源:mazeworks.com16 位无符号整数
替代文字

所以,我想出了一个 16 位整数的二维数组,我很适合我的 Maze 数据结构。我想保持数据结构的大小,以便我可以轻松创建非常密集的迷宫

因此,我需要能够在运行时动态创建一个 16 位 n*m 大小的整数的二维数组。在某处,我读到 C++ 中的 2d 数组只是 [n *m] 大小的 1d 数组的语法糖,您可以通过 [row + col * width] 访问元素。

这是我的工作尝试:

class Maze {
    public:

        Maze(int mazeWidth, int mazeHeight)
        {
            mazeGrid = new unsigned int16_t[width*height];
            width = mazeWidth;
            height = mazeHeight;
        }

        unsigned int16_t getArrayValue(int row, int col)
        {
            return mazeGrid[row + col*width];
        }

        void setArrayValue(int row, int col, unsigned int16_t value)
        {
            mazeGrid[row + col*width] = value;
        }

    private:
        int width, height;
        unsigned int16_t *mazeGrid;

}

有一些 C++ 知识的人对我的 Maze 课程有什么建议吗?我对 C++ 很陌生,所以这对我来说都是一次学习经历。

4

9 回答 9

10

这将是一个很长的答案,因此它将涉及一些编程/C++ 概念:封装、RAII、初始化列表、析构函数、常量、定义运算符、模板类、模板函数和位域。 .

第一件事是我总是从用户的角度开始设计。要为迷宫设计数据结构,用户将是想要使用该数据结构的程序员(可能是您)。从这个角度来看,结构是内存优化的事实是一个实现细节,不如使用相关。

使用您的知识库,我将从更改接口开始,这样用户就不需要通过定义一个封装位操作的类来关心内部结构,类似于此(稍后我将处理它):

class cell {
public: 
  void setBacktrace( unsigned int value ); // value must be between 0-15
  unsigned int getBacktrace() const;
  // same for all other fields
private:
  uint16_t data;
};

现在用户不需要关心底层细节。调用者代码可以简单地写:

cell c;
c.setBacktrace( 5 ); // set the backtrace value to 5
std::cout << c.getBacktrace() << std::endl;

现在迷宫是单元对象周围的容器。正如其他人所指出的,您可以使用 std::vector 来简化操作,或者您可以定义自己的容器。既然我认为您正在学习,我们将沿着漫长的道路前进:

class maze {
public:
   maze( size_t width, size_t height );
   ~maze();
   cell getCell( size_t row, size_t col ) const;
   void setCell( size_t row, size_t col, cell c );
private:
   size_t width_;
   size_t height_;
   cell * data_;
};

您的代码对接口的更改是:添加一个析构函数。它将按照RAII习语释放构造函数请求的资源。单元格元素的访问器被标记为 const,因为它只会返回一个值而不改变结构。这是您应该从一开始就开始使用的另一个概念:将所有非变异方法标记为 const

现在到实现:

// Constructor and destructor
maze::maze( size_t width, size_t height ) 
   : width_( width ), height_( height ), data_( new cell[width*height] )
{
}
maze::~maze()
{
   delete [] data_;
}

构造函数是使用初始化列表定义的。在初始化列表中,字段宽度_、高度_和数据_的字段构造函数被称为传递宽度、高度和新句子的返回指针作为参数。

使用初始化列表而不是在构造函数块 ({}) 中添加等效代码有两个原因。初始化列表比括号内的等效代码更有效(不是那么重要),但主要允许您调用特定的构造函数或初始化引用,这两者都不能在构造函数块内完成。

我添加了一个析构函数来释放您获取的数据。如果你没有在你的类中添加析构函数,编译器将提供一个默认析构函数,它将调用你类中所有属性的析构函数。在您的情况下,默认析构函数不会释放在构造期间获得的内存

其他方法我就不赘述了。到目前为止,我们从用户的角度来看:

int main() {
  maze m( 10, 50 ); // Consctruct the maze
  cell c;
  c.setBacktrace( 5 );
  m.setCell( 3, 4, c);  // Store c in the container
  assert( m.getCell( 3, 4 ).getBacktrace() == 5 );
}

我们可以通过稍微改变界面使这段代码更加用户友好。如果我们提供一个operator(),它接受两个索引并返回对单元元素的引用,则使用会更简单(数组接口上的 C++ FAQ lite):

class maze {
    // most everything as before, changing set/get to:
public:
   cell const & operator()( size_t row, size_t col ) const;
   cell & operator()( size_t row, size_t col ); 
};

那么用法会更简单:

int main()
{
   maze m( 10, 10 );
   m( 3, 4 ).setBacktrace( 5 );
   assert( m( 3, 4 ).getBacktrace() == 5 );
}

无需构造单元对象并对其应用更改然后存储该对象,我们可以直接修改存储的对象。现在实现:

cell const & maze::operator()( size_t row, size_t col ) const
{
   return *data_[ row + col*width_ ];
}
cell & maze::operator()( size_t row, size_t col )
{
   return *data_[ row + col*width_ ];
}

两种实现都相似,唯一的区别是第一个告诉编译器它不会更改迷宫的内容,它返回一个常量引用以保证调用者不会更改单元格。

在处理完迷宫类之后,您会注意到使它成为迷宫的唯一原因是存储的数据是一个单元格,但所有逻辑都只是一个普通的 2D 数组的逻辑。我们可以利用它并将其重新定义为模板类,以便我们可以在不同的情况下重用代码,并使用不同的存储类型定义:

template <typename T> // stored data
class array_2d
{
public:
   array_2d( size_t width, size_t height );
   ~array_2d();
   T const & operator()( size_t row, size_t col ) const;
   T & operator()( size_t row, size_t col );
private:
   size_t width_;
   size_t height_;
   T* data_;
};

用法将是:

int main()
{
   array_2d<cell> maze( 10, 10 );
   maze( 3, 4 ).setBacktrace( 5 );
}

定义实现会有些不同,但不会复杂得多:

template <typename T>
array_2d<T>::array_2d( size_t width, size_t height )
   : width_( width ), height_( height ), data_( new T[ width*height ] )
{
}

在定义每种方法的实现时也是如此。没那么难,不是吗?

最后,我们可以回到 cell 的定义,让它更自然地使用。我们拥有一组访问器方法,它们将执行按位操作以与四个内部字段(回溯、解决方案、边界、墙壁)中的每一个进行交互。单元格只是一个存储四个整数字段的 POD(普通旧数据)结构,类似于:

struct big_cell
{
   unsigned int backtrack;
   unsigned int solution;
   unsigned int borders;
   unsigned int walls;
};

那将用作:

int main()
{
   array_2d<big_cell> maze( 10, 10 );
   maze( 3, 4 ).backtrack = 5;
   assert( maze( 3, 4 ).backtrack == 5 );
}

但这种结构将占用比我们需要的更多的空间。存储实现细节迫使我们改变类的使用。让我们尝试解决这个问题。将无符号整数更改为无符号字符会将结构的大小减少到 32 位(而不是完全优化的结构的 16 位):

struct medium_cell
{
   unsigned char backtrack;
   unsigned char solution;
   unsigned char borders;
   unsigned char walls;
};

这个解决方案每个单元只浪费 2 个字节,这可能不会太多(空间需求翻倍,但现在内存很便宜)。这也可以在 32 位处理器中更快地执行。一些 32 位架构需要更长的时间来检索和操作 16 位元素。

无论如何,如果您仍然对完全紧凑的内存模型感兴趣,您可以使用 C++ 中一个不为人知的特性:位域。它们允许您定义结构中的某些字段只需要一些位:

struct small_cell {
   uint16_t backtrack : 4; // take 4 bits from the uint16_t
   uint16_t solution : 4;
   uint16_t borders : 4;
   uint16_t walls : 4;
};

用法将是:

int main() 
{
   small_cell c;
   c.solution = 5; 
   c.backtrack = 3;
}

现在这个结构只占用 16 位内存,可以像前两个结构一样使用。由于迷宫现在只是一个模板化的二维数组,您可以很容易地测试这三个结构。您可以为测试定义模板函数

#include <time.h>

// templated for comparissons with cell types
template <typename CellStruct>
void test()
{
   array_2d< CellStruct > maze;
   // Test operations...
}

void print_test( std::string const & test, clock_t ticks )
{
   std::cout << "Test: " << test << " took " << ticks 
      << " ticks, or " << ticks / CLOCKS_PER_SEC << " seconds." 
      << std::endl;
}

int main()
{
   clock_t init = clock();
   test< big_cell >();
   clock_t after_big = clock();
   test< medium_cell >();
   clock_t after_med = clock();
   test< small_cell >();
   clock_t end = clock();

   print_result( "big_cell", after_big - init );
   print_result( "medium_cell", after_med - after_big );
   print_result( "small_cell", end - after_med );
}

测试函数仅是模板化的,以便可以使用不同的单元实现来执行。一旦您知道哪种实现最适合您的问题域,您就可以制作将使用特定单元格类型的特定代码。

于 2009-02-14T12:54:37.120 回答
5

构造函数存在问题 - 在分配之前使用“宽度”和“高度”。您还需要一个析构函数来释放内存:

~Maze()
{
    delete [] mazeGrid;
}

除此之外,它看起来还可以。

于 2009-02-14T08:41:59.330 回答
3

在 C++ 中,构造是初始化。所以你可以重写c'tor:

    Maze(int mazeWidth, int mazeHeight) 
       :width(mazeWidth), height(mazeHeight), mazeGrid(new uint16_t[width*height])
    {
    }

请注意,数据成员是按照它们在类中定义的顺序进行初始化的,而不是您初始化它们的顺序。
还要注意 unsinged int16_t 变成了 uint16_t。如果你要使用这些家伙,最好一路走下去。
通常将数据成员称为 m_width 和 m_height 而不仅仅是宽度和高度。

代替 set 和 get 方法,我将定义一个以这种方式operator[]返回的uint16_t*方法,您可以获得一种模仿原始类型的更自然的语法:

   ....
   uint16_t* operator[](int col) { return &(mazeGrid[col*width]); }
   ....

   uint16_t d = mymaze[col][row];

我会让你弄清楚为什么它可以正常工作。

于 2009-02-14T09:11:13.427 回答
2

尝试使用 std::vector

于 2009-02-14T08:29:06.210 回答
1

您可以使用std::bitset

于 2009-02-14T09:08:44.523 回答
0

Not addressing your main space issues, but here's a simple version that doesn't involve you with explicit dynamic memory management:

#include <vector>
#include <iostream>
using namespace std;

class Maze {
    public:

        Maze(int mazeWidth, int mazeHeight) {
           for ( int i = 0; i < mazeHeight; i++ ) {
              mMaze.push_back( vector <int>( mazeWidth ) );
           }
        }

        int At(int row, int col) const {
           return mMaze.at(row).at(col); 
        }

        int & At(int row, int col) {
           return mMaze.at(row).at(col); 
        }

    private:

        vector < vector <int> > mMaze;
};


int main() {
    Maze m( 5, 5 );
    m.At( 2, 2 ) = 42;
    cout << m.At( 2, 2 ) << "\n";
}
于 2009-02-14T17:50:04.647 回答
0

One thing not mentioned in the previous answers is that when your class contain a pointer to allocated memory you should either provide copy constructor and assignment operator or simply forbid them to avoid use of the default implementations for these that C++ is providing.

于 2009-02-14T18:21:22.403 回答
0

编写一个小宏来转换二维坐标以提高可读性。就像是。这个:

#define TWO_DIM(x,y,w) (x+y*w)

使用 STL 容器:

// Define and get memory
std::vector <int16_t> mazedata;
mazedata.resize(newsize);
// Assign values
mazedata[TWO_DIM(row,col,width)]=newvalue;

请记住,STL 实现了内存效率std::vector<bool>,它可以像普通数组一样使用,但每比特数据占用 1 比特。在大型阵列的情况下,您不会注意到 STL 的开销。

于 2009-02-14T13:08:20.237 回答
0

好吧 setArrayValue 还没有做任何事情(尽管你现在可能已经注意到了)。此外,您没有删除功能,因此 mazeGrid 永远不会被释放。否则我觉得没问题。

于 2009-02-14T08:33:13.893 回答