Почему malloc () не выделяет достаточно памяти - PullRequest
2 голосов
/ 09 июля 2019

У меня возникли некоторые проблемы с пониманием функциональности malloc, и, возможно, именно поэтому я вижу эту проблему, но, надеюсь, кто-то здесь может помочь мне понять.

Я выделяю двумерный массив с использованиемследующая функция:

int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols){
    uint8_t i = 0;  

    int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));

    for(i=0; i<num_rows; i++) {
        arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));
    }               

    return arr;     
}

Я вызываю эту функцию следующим образом:

twoD = create_2d_array(4, 512);

Если я остановлю свой код в начале цикла for и проверим содержимое arr, используямой терминал GDB, тогда я получаю следующее:

gdb $ p/x arr[0]
$96 = 0x2001fa00

gdb $ p/x arr[1]
$97 = 0x0

gdb $ p/x arr[2]
$98 = 0x0

gdb $ p/x arr[3]
$99 = 0x0

Это подразумевает, по крайней мере для меня, что arr[0] был выделен правильно, но не другие элементы arr.

Разве malloc не должен определять, может ли быть выделен запрошенный размер, и если нет, то он должен возвращать указатель NULL?По крайней мере, это мое понимание malloc.Есть ли что-то, что я делаю не так?

В качестве теста я выполнил следующую строку:

twoD_temp = create_2d_array(2, 4);

Снова я прекращаю выполнение в начале цикла for и печатаю содержимоеиз arr.

gdb $ p/x arr[0]
$121 = 0x2001fa00

gdb $ p/x arr[1]
$122 = 0x2001fa10

Это то, что я ожидал.Первый индекс является допустимым указателем, а второй индекс также является действительным указателем, поскольку я создал массив указателей.

После выполнения цикла for я печатаю то же содержимое:

gdb $ p/x arr[0]
$125 = 0x2001fa00

gdb $ p/x arr[1]
$126 = 0x2001fa10

Это все то же самое, что я и ожидал.Единственное отличие состоит в том, что для столбцов выделена память.

Ответы [ 4 ]

4 голосов
/ 09 июля 2019

После первого malloc, arr - указатель на кусок памяти, который содержит только мусор.Цикл for устанавливает отдельные записи так, чтобы они указывали на строки.

Таким образом, ни arr[0], ни arr[1] не должны содержать какого-либо конкретного значения, пока циклы for не установят свои значения, чтобы указывать на различные строки.что он создает.

Давайте внимательно посмотрим на код:

int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));

Это выделяет один блок памяти, достаточно большой, чтобы содержать один указатель для каждой строки.Переменная arr будет указывать на эту память.Блок памяти содержит мусор.

for(i=0; i<num_rows; i++) {
    arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));
}     

Это устанавливает arr[0], чтобы указывать на блок памяти, достаточно большой для размещения строки.Пока этот цикл for не будет выполнен, arr[0] и другие записи в массиве просто содержат мусор.


Возможно, диаграмма поможет.После первого выделения перед циклом (из arr, блока понтеров), вот что у вас есть:

       +--------+
arr -> | arr[0] | -> points to some arbitrary location
       +--------+
       | arr[1] | -> points to some arbitrary location
       +--------+
       | arr[2] | -> points to some arbitrary location
       +--------+
       | arr[3] | -> points to some arbitrary location
       +--------+

Эти указатели будут указывать на произвольные места, потому что malloc выделяет память, но делает не инициализируйте его чем-либо.

И это состояние, в котором вы исследуете все, поэтому каждое arr[] может быть любым значением,Как только вы пройдете цикл один раз, у вас будет:

       +--------+    +----------------+
arr -> | arr[0] | -> | arr[0][0..511] |
       +--------+    +----------------+
       | arr[1] | -> points to some arbitrary location
       +--------+
       | arr[2] | -> points to some arbitrary location
       +--------+
       | arr[3] | -> points to some arbitrary location
       +--------+

Только на балл указывает, что ваши выделения второго уровня начинают указывать на что-то полезное.

0 голосов
/ 09 июля 2019

Вы уже приняли ответ (и это правильно, поскольку он подробно объясняет, что было не так). Я знаю это, потому что внес в это свой вклад: -)

Однако я также хотел бы предложить другой способ размещения ваших 2D-массивов. Выполнение этого как серии выделений означает, что вы несете ответственность за очистку отдельных выделений, когда вы закончите работу с массивом.

Хотя это можно сделать с помощью другой функции, в нее необходимо передать число строк, чтобы можно было правильно освободить каждое выделение строк перед освобождением выделения массива указателей.

Метод, который я использовал в прошлом, состоит в том, чтобы выделить один блок памяти, достаточно большой для обоих массивов указателей и для всех массивов строк, а затем помассировать его так, чтобы он выглядит как обычный 2D-массив (так что такие вещи, как array[row][col] = 7 все еще работают).

Код ниже делает это, и единственным требованием является то, что требования к выравниванию для int16_t не являются более строгими, чем int16_t* (это было бы редко, и вы могли бы обойти это, но, вероятно, в этом нет необходимости Подавляющее большинство сред):

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

void *create_2d_array(uint8_t num_rows, uint16_t num_cols, int clear_data) {
    // Create a single block big enough for both level-1 pointer array
    // and level-2 value array.

    size_t totalMemSz =
        (num_rows * sizeof(uint16_t*)) +            // pointers.
        (num_rows * num_cols * sizeof(uint16_t));   // values.
    void *memBlock = clear_data ? calloc(totalMemSz, 1) : malloc(totalMemSz);
    if (memBlock == NULL) return NULL;

    // Populate the level-1 pointers to point at the level-2 rows.

    for (size_t i = 0; i < num_rows; ++i) {
        ((int16_t**)memBlock)[i] = (int16_t*)(&(((char*)memBlock)[
            num_rows * sizeof(uint16_t*) +    // skip pointers.
            i * num_cols * sizeof(uint16_t)   // select row.
        ]));
    }

    return memBlock;
}

#include <stdio.h>
#include <stdbool.h>

void dumpArr(int16_t **addr, size_t num_rows, size_t num_cols) {
    for (size_t i = 0; i < num_rows; ++i) {
        printf("Pointer[%zd] = %p, data =", i, addr[i]);
        for (size_t j = 0; j < num_cols; ++j) {
            printf(" %2d", addr[i][j]);
        }
        putchar('\n');
    }
}

int main() {
    puts("Running ...");
    int16_t **arr = create_2d_array(4, 7, true);
    arr[0][0] = 1;
    arr[1][2] = 42;
    arr[3][6] = 77;
    dumpArr(arr, 4, 7);
    free(arr);
}

Этот код является полной тестовой программой для функции create_2d_array, поэтому вы можете проверить ее, как пожелаете. Важно то, что когда вы закончите работу с 2D-массивом, вы просто отпускаете его с free(arr), а не выполняете какую-либо специальную обработку, зависящую от размера.

Пример выполнения кода в его текущем виде:

Running ...
Pointer[0] = 0x675440, data =  1  0  0  0  0  0  0
Pointer[1] = 0x67544e, data =  0  0 42  0  0  0  0
Pointer[2] = 0x67545c, data =  0  0  0  0  0  0  0
Pointer[3] = 0x67546a, data =  0  0  0  0  0  0 77
0 голосов
/ 09 июля 2019

Ваш код не имеет проблемы. Вопрос в том, как вы понимаете, как работает malloc.

Когда память выделяется динамически через malloc, значения байтов равны неопределенны , поэтому они могут принимать любое значение, включая 0. Они не гарантированно содержат какое-либо конкретное значение.

В вашем примере:

int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));

Если мы предположим, что указатель занимает 8 байтов, а num_rows равен 4, это выделяет 32 байта пространства, что достаточно для 4 значений типа int16_t *. В этот момент каждый член массива не содержит значащего значения. Когда вы позже сделаете это:

arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));

Вы присваиваете (при условии успешного вызова) действительный адрес памяти, возвращенный из malloc члену массива, перезаписывая любое значение мусора, которое было там ранее. Если вы посмотрите на элементы arr[i] на данный момент, то есть arry[i][0], arry[i][1] и т. Д., Эти значения также будут неопределенными, пока вы не назначите им что-то.

0 голосов
/ 09 июля 2019

Нет ничего плохого в вашем коде. У меня есть несколько замечаний, которые я приму ниже.

Разве malloc не должен определять, может ли быть выделен запрошенный размер, и если нет, то он должен возвращать указатель NULL?

Да, это правда. Но вы не проверяете никаких возвращаемых значений, так откуда вы знаете?

Кроме того, это хорошая привычка использовать переменную вместо типа для sizeof. Это уменьшает дублирование кода. Так что пишите T *t= malloc(n * sizeof(*t)) вместо T *t = malloc(n * sizeof(T)). И приведение совершенно не нужно и не дает никаких преимуществ, если вы не компилируете свой код C с помощью компилятора C ++.

Итак, учитывая все это, я написал бы код следующим образом:

int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols)
{
    uint8_t i = 0;  

    int16_t **arr = malloc(num_rows * sizeof(*arr));
    if(!arr) {
        fprintf(stderr, "Error allocating memory\n");
        exit(EXIT_FAILURE);
    }    
    for(i=0; i<num_rows; i++) {
        arr[i] = malloc(num_cols * sizeof(*arr[0]));
        if(!arr[i]) {
            fprintf(stderr, "Error allocating memory\n");
            exit(EXIT_FAILURE);
        }    
    }        

    return arr;     
}

И если вы хотите пойти по-настоящему хардкорно и иметь возможность протестировать свои собственные функции так же, как вы тестируете malloc, то вы можете сделать это следующим образом. Это одно из немногих принятых применений для goto

int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols)
{
    uint8_t i = 0;

    int16_t **arr = malloc(num_rows * sizeof(*arr));
    if(!arr)
        goto cleanup1;

    for(i=0; i<num_rows; i++) {
        arr[i] = malloc(num_cols * sizeof(*arr[0]));
        if(!arr[i])
            goto cleanup2;
    }

    return arr;

cleanup2:
    do {
        free(arr[i]);
    } while(i-- > 0);

cleanup1:
    free(arr);

    return NULL;
}

Тогда вы можете сделать это:

int16_t ** array2d = create_2d_array(5,6);
if(!array2d) { /* Handle error */ }
...