C - 未知维度的2D游戏板的动态分配

Posted

技术标签:

【中文标题】C - 未知维度的2D游戏板的动态分配【英文标题】:C - dynamic allocation of 2D game board of unknown dimensions 【发布时间】:2021-11-02 13:43:21 【问题描述】:

我想编写一个基于棋盘的游戏,并希望将棋盘表示为 2D char-array。 特别是,该板由 4 种不同类型的字符/单元格组成:

玩家 (S) 目标单元格 (A) 墙 (#) 怪物(^v<>

怪物朝某个方向看,上面的箭头状字符表示。

我希望能够从以下未知的文本文件中加载关卡:

文件中的行数(二维数组中的行) 每行的字符数

关卡文本文件示例:

    ######                                 ##########
    #  < #                                 #        #
    #    #########                         #       ##
    #    #       #                         #^      #
    #    #       #                        ##       #
  ###            #      ########        ###        #
  #     #   ######      #      ##########   #     ##
  #     #^  #           #                 #########

如您所见,前 3 行包含 49 个字符,但其他行包含 48/47 个字符。 差异可能要大得多,因此我需要对两个维度都使用动态分配。

我应该使用固定大小的缓冲区逐个字符还是逐行读取文件,并在必要时扩展它?

这是我尝试过的:

int needed_num_rows = 1;
int needed_bytes_col = 16;

char **board = calloc(needed_num_rows, sizeof(char*));
char *buf = calloc(needed_bytes_col, sizeof(char));

int idx = 0;
while (fgets(buf, needed_bytes_col, level_file) != NULL) 
    if (buf[needed_bytes_col - 1] != '\n' && buf[needed_bytes_col - 1] != '\0')  // not read to the end yet
        needed_bytes_col = needed_bytes_col * 2 + 1;
        buf = realloc(buf, needed_bytes_col);
        buf += needed_bytes_col;
     else  // whole line has been read
        if (idx + 1 > needed_num_rows) 
            needed_num_rows = needed_num_rows * 2 + 1;
            board = realloc(board, needed_num_rows);
        

        board[idx++] = buf;
        needed_bytes_col = 16;
        buf = calloc(needed_bytes_col, sizeof(char));
    

【问题讨论】:

您发布的代码有什么问题? 有时会给我分段错误。我无法准确地确定它。我应该改用逐字符处理吗? 如果一行 >= (2*needed_bytes_col+1) 宽,这将严重破坏,导致您进入扩展部分两次。当这种情况发生时,第二轮buf 不再指向从mallocrealloccalloc 等返回的基地址或NULL,从而违反realloc 的要求并调用未定义的行为。就个人而言,我会重新考虑这一点并考虑getline/getdelim 解决方案,如果它在您的平台上可用。它会大大清理这个。 【参考方案1】:

这里你有读取未知大小的行的函数,为它动态分配缓冲区。

#define LINEDELTA 256

char *readline(FILE *fi)

    char *result = NULL;
    size_t currentSize = 0;
    if(fi)
    
        do
        
            char *tmp;
            tmp = realloc(result, currentSize + LINEDELTA);
            if(tmp) result = tmp;
            else  /* add error handling*/ 
            if(fgets(result + currentSize - !!currentSize, LINEDELTA + !!currentSize, fi) == (result + currentSize))
            

            
            else  /* add error handling*/ 
            currentSize += LINEDELTA;
        while(strchr(result, '\n'));
    
    return result;

【讨论】:

【参考方案2】:

您的代码有点[太]复杂,有太多特殊情况。

它可以被重构/简化。更容易将行读入固定 [最大] 大小的缓冲区,然后使用 strdup

char **board = NULL;
size_t board_count = 0;
size_t board_max = 0;

char buf[1000];

while (fgets(buf,sizeof(buf),level_file) != NULL) 
    // strip newline
    buf[strcspn(buf,"\n")] = 0;

    // allocate more rows
    // the increment here is arbitrary -- adjust to suit
    if (board_count >= board_max) 
        board_max += 10;
        board = realloc(board,sizeof(*board) * board_max);
    

    // store the new row
    board[board_count++] = strdup(buf);


// trim array to exact amount used
board = realloc(board,sizeof(*board) * board_count);

更新:

如果行长 4000 个字符怎么办? – 0___________

TL;DR 是char buf[1000000]; 当然,我们可以将buf 设为char * 并在其上执行realloc [独立于realloc for board],然后重新循环,但我认为在这种情况下,这太过分了。

但是...

如果板子任意大(例如,数百万行和可以有一百万个字符 [或十个] 的行),那么我们可以动态分配所有内容:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char **board = NULL;
size_t board_count = 0;
size_t board_max = 0;

int
lineget(char **bufp,FILE *level_file)

    char *buf;
    size_t bufmax = 0;
    size_t buflen = 0;
    char *cp;
    size_t off = 0;
    size_t remlen;
    size_t addlen;
    int valid = 0;

    buf = *bufp;

    while (1) 
        // get amount of free space remaining in buffer
        remlen = bufmax - off;

        // increase buffer length if necessary (ensure we have enough space
        // to make forward progress) -- this "lookahead" limit is arbitrary
        // as is the increment
        if (remlen < 2) 
            bufmax += 1000;
            buf = realloc(buf,bufmax);

            if (buf == NULL) 
                perror("realloc");
                exit(1);
            

            remlen = bufmax - off;
        

        // append to the buffer
        cp = fgets(&buf[off],remlen,level_file);
        if (cp == NULL)
            break;

        valid = 1;

        // get length of newly added chars
        addlen = strlen(&buf[off]);

        // get total length of filled area
        buflen = off + addlen;

        // check for newline and strip it and stop if we get one
        if (addlen > 0) 
            if (buf[buflen - 1] == '\n') 
                buf[--buflen] = 0;
                break;
            
        

        // increase the offset into the buffer
        off += addlen;
    

    // trim buffer to length
    buf = realloc(buf,buflen);

    *bufp = buf;

    return valid;


void
readfile(void)


    while (1) 
        // allocate more rows
        // the increment here is arbitrary -- adjust to suit
        if (board_count >= board_max) 
            board_max += 10;
            board = realloc(board,sizeof(*board) * board_max);

            // set null pointers on unfilled rows
            for (size_t idx = board_count;  idx < board_max;  ++idx)
                board[idx] = NULL;
        

        // fill the current row
        if (! lineget(&board[board_count],stdin))
            break;

        // advance row count
        ++board_count;
    

    // trim array to exact amount used
    board = realloc(board,sizeof(*board) * board_count);

【讨论】:

如果行长 4000 个字符怎么办? @0___________ TL;DR 是char buf[1000000]; 当然,我们可以将buf 设为char * 并在其上执行realloc [独立于realloc for board] , 并重新循环,但我认为在这种情况下这太过分了。 只需使用getline()。如果您的操作系统不支持 getline() 而需要源代码,请在此处获取:dev.w3.org/libwww/Library/src/vms/getline.c【参考方案3】:

我建议不要对每一行使用动态内存分配,而是分两次读取文件:一次用于确定两个维度的最大大小,一次用于实际读取数据。

这样,您将在第一遍之后知道确切的内存需求,并可以在第二遍之前分配适当大小的内存缓冲区。

此解决方案的一个优点是,在两个维度上具有固定长度可能是您在加载游戏后想要拥有的内存表示。

因此,我会推荐以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main( void )

    FILE *level_file;
    char *board;
    char buffer[1024];
    size_t rows = 0;
    size_t cols = 0;
    size_t current_line;

    level_file = fopen( "level.txt", "rt" );
    if ( level_file == NULL )
    
        fprintf( stderr, "error opening file!\n" );
        exit( EXIT_FAILURE );
    

    //first pass of file to determine size of both dimensions
    while ( fgets( buffer, sizeof buffer, level_file ) != NULL )
    
        char *p;
        size_t length;

        //verify that line was not too long
        p = strchr( buffer, '\n' );
        if ( p == NULL )
        
            //there are 3 explanations for not finding the newline character:
            //1. end-of-file was encountered
            //2. I/O error occurred
            //3. line was too long to fit in buffer

            if ( feof( level_file ) )
            
                //make p point to terminating null character
                p = buffer + strlen( buffer );
            
            else if ( ferror( level_file ) )
            
                fprintf( stderr, "I/O error occurred\n" );
                exit( EXIT_FAILURE );
            
            else
            
                fprintf( stderr, "line was too long to fit in buffer\n" );
                exit( EXIT_FAILURE );
            
        

        //calculate number of columns in line
        length = p - buffer;

        //update cols to higher value, if appropriate
        if ( length > cols )
            cols = length;

        rows++;
    

    //verify that loop terminated due to end-of-file
    if ( !feof( level_file ) )
    
        fprintf( stderr, "error reading with fgets!\n" );
        exit( EXIT_FAILURE );
    

    //rewind file back to start
    rewind( level_file );

    //allocate memory for board
    board = malloc( rows * cols );
    if ( board == NULL )
    
        fprintf( stderr, "error allocating memory for board!\n" );
        exit( EXIT_FAILURE );
    

    //second pass of file to actually read the data
    current_line = 0;
    while ( fgets( buffer, sizeof buffer, level_file ) != NULL )
    
        char *p;
        size_t length;

        //verify that line was not too long
        //note that it is possible that file was changed since the
        //first pass, so it is not wrong to check again (but maybe
        //not necessary)
        p = strchr( buffer, '\n' );
        if ( p == NULL )
        
            //this is identical to first pass

            if ( feof( level_file ) )
            
                //make p point to terminating null character
                p = buffer + strlen( buffer );
            
            else if ( ferror( level_file ) )
            
                fprintf( stderr, "I/O error occurred\n" );
                exit( EXIT_FAILURE );
            
            else
            
                fprintf( stderr, "line was too long to fit in buffer\n" );
                exit( EXIT_FAILURE );
            
        

        //calculate number of columns in line
        length = p - buffer;

        //verify that line consists only of valid characters
        if ( strspn(buffer," SA#^v<>") != length )
        
            fprintf( stderr, "invalid characters found in file!\n" );
            exit( EXIT_FAILURE );
        

        //make sure that line length has not increased since first pass
        if ( length > cols )
        
            fprintf( stderr, "detected that file has changed since first pass!\n" );
            exit( EXIT_FAILURE );
        

        //calculate start of row in board
        p = board + current_line * cols;

        //copy line contents into board
        memcpy( p, buffer, length );

        //fill rest of line with space characters
        for ( size_t i = length; i < cols; i++ )
            p[i] = ' ';

        //increment loop counter
        current_line++;
    

    //verify that loop terminated due to end-of-file
    if ( !feof( level_file ) )
    
        fprintf( stderr, "error reading with fgets!\n" );
        exit( EXIT_FAILURE );
    

    fclose( level_file );

    printf( "rows: %d, cols: %d\n\n", rows, cols );

    //print board contents
    for ( size_t i = 0; i < rows; i++ )
    
        for ( size_t j = 0; j < cols; j++ )
        
            putchar( board[i*cols+j] );
        

        putchar( '\n' );
    

    free( board );

我已经使用以下输入文件成功测试了这个程序:

   # S A v <
   #### ## A
AAA ###

但是请注意,由于以下声明,此程序仅支持最多 1022 个字符(1024 包括换行符和终止空字符)的行:

char buffer[1024];

虽然可以增加这个大小,但在堆栈上分配超过几千字节通常是一个坏主意,因为这可能会导致stack overflow。因此,如果单行可能大于几千字节,那么将 buffer 分配到堆栈以外的其他位置是合适的,例如使用动态内存分配。

【讨论】:

【参考方案4】:

这似乎有点复杂,但是你看它。我使用了mmap() 和一个行解析功能。这个函数只做换行查找,加上跳过重复的并检查长度。这个想法是有三个返回:-1 = 完成,-2 = 跳过这一行,肯定 = 使用这一行(或使用第二个参数,例如结构)。

所以即使只解析行的存在和长度,它仍然是某种解析。主要部分是:

plret = parse_lines(&pmap)

pmapmmap() 映射的移动副本。 parse_lines() 推进它。 plret 告诉您是 1) 停止 2) 继续还是 3) 行动并继续。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>

/* file must have NL at end, and memory a \0 */
int parse_lines(char **pmap) 

    int const maxline = 60;
    char *map = *pmap;
    while(*map == '\n')            //skip empty lines??? 
        map++;

    if (! *map)
        return -1;                 // means EOF 

    char *nnl = strchr(map, '\n');  
    *pmap = nnl + 1;               // new position for caller: start of next line

    if (nnl - map < maxline) 
        return nnl - map;          // return length of line  
    else
        return -2;                 // too long  

void parse_file() 
    char const *f = "test";
    size_t const mmlen = 256*256;                     // or fstat()

    int fd = open(f, O_RDONLY);
    char *mapping = mmap(0, mmlen, PROT_READ, MAP_PRIVATE, fd, 0),
         *pmap = mapping;  
    
    int plret;
    while ((plret = parse_lines(&pmap)) != -1) 

        if (plret > 0) 

            char *s = calloc(plret+1, 1);
            memcpy(s, pmap-plret-1, plret);           // NL (+1) or not? 

            printf("%d at %p now %p:\t\"%s\"\n", plret, pmap, s, s);
            free(s);                                  // rather store it in an array
         
        else
            printf("line too long - skip or truncate or abort\n"); 

    /* End of File */
    munmap(mapping, mmlen);
    // + realloc array for @s-pointers, if it was allocated "big enough"

int main(void) 
    parse_file();

我遗漏了数组arr[idx++] = s(没有检查/重新分配)。但是输出很好地说明了正在发生的事情(倒数第三行超长):

53 at 0x7fdc29283036 now 0x5557230822a0:        "    ######                                 ##########"
53 at 0x7fdc2928306c now 0x5557230826f0:        "    #  < #                                 #        #"
53 at 0x7fdc292830a2 now 0x555723082730:        "    #    #########                         #       ##"
52 at 0x7fdc292830d7 now 0x555723082770:        "    #    #       #                         #^      #"
52 at 0x7fdc2928310c now 0x5557230827b0:        "    #    #       #                        ##       #"
52 at 0x7fdc29283141 now 0x5557230827f0:        "  ###            #      ########        ###        #"
52 at 0x7fdc29283176 now 0x555723082830:        "  #     #   ######      #      ##########   #     ##"
51 at 0x7fdc292831aa now 0x555723082870:        "  #     #^  #           #                 #########"
line too long - skip or truncate or abort
2 at 0x7fdc292831fd now 0x5557230828b0: "##"
1 at 0x7fdc292831ff now 0x5557230828d0: "#"

0x7fd 处的字节...在 0x555 处作为字符串找到了新家...

他们丢失了换行符,但现在有一个空终止符。

【讨论】:

以上是关于C - 未知维度的2D游戏板的动态分配的主要内容,如果未能解决你的问题,请参考以下文章

在C中操作动态分配的2D char数组

在C中动态分配的从文件读取的未知长度字符串(必须防止从文件中读取数字)

核心转储,如何增加矩阵维度(可用),在 C 中释放 2d 指针时出错

如何在一个分配 C 中动态分配二维数组

在 C++ 中的 2D 动态内存分配数组中释放分配的内存

GPU 2D 共享内存动态分配