Страницы

Поиск по вопросам

понедельник, 30 декабря 2019 г.

Недопонимание с кодом

#c #указатели


Наткнулся на код, по которому у меня возникает пару вопросов, если ответите, то буду
признателен

#include 
#include 

int isOdd(int a) {
    return (a % 2 != 0);
}

unsigned int filter(int *arr, unsigned size, int (*pred)(int), int** out) {
    unsigned i;
    unsigned j;
    *out = (int*) malloc(sizeof(int)*size);
    for (i = 0, j = 0; i < size; i++) {
        if (pred(arr[i])) {
            (*out)[j] = arr[i];
            j++;
        }
    }
    *out = (int*) realloc(*out, j*sizeof(int));
    return j;
}

int main () {
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    unsigned i;
    unsigned size;
    int *aOdd = NULL;

    size = filter(a, 10, isOdd, &aOdd);
    for (i = 0; i < size; i++) {
        printf("%d ", aOdd[i]);
    }

}


Вопрос №1:
Зачем требуются явные преобразования такого вида? 

*out = (int*) malloc(sizeof(int)*size);
*out = (int*) realloc(*out, j*sizeof(int));


Они же излишни, если я не ошибаюсь.

Вопрос №2: Что происходит в строках, которые я выделил восклицательными знаками?

    unsigned int filter(int *arr, unsigned size, int (*pred)(int), int** out) {
    unsigned i;
    unsigned j;
    *out = (int*) malloc(sizeof(int)*size); /* !!!!!!!!!! */
    for (i = 0, j = 0; i < size; i++) {
        if (pred(arr[i])) {
            (*out)[j] = arr[i]; /* !!!!!!!!!! */
            j++;
        }
    }
    *out = (int*) realloc(*out, j*sizeof(int)); /* !!!!!!!!!! */
    return j;
}


Вопрос №3: Разве не легче было реализовать функцию из вопроса №2 таким образом (просто
через указатель, а не через указатель на указатель)

unsigned int filter(int *arr, unsigned size, int (*pred)(int), int* out) {
    unsigned i;
    unsigned j;
    for (i = 0, j = 0; i < size; i++) {
        if (pred(arr[i])) {
            *(out + j) = arr[i];
            j++;
        }
    }

    return j;
}


Есть ли существенные различия или даже преимущества между функцией из 
вопроса №2 и функцией из вопроса №3?
    


Ответы

Ответ 1



Вопрос №1: Зачем требуются явные преобразования такого вида? Для C не нужны. Для C++ необходимы, т.к. в C++ неявное преобразование из void* запрещено. Вопрос №2: Что происходит в строках, которые я выделил восклицательными знаками? 1) Выделение памяти для size int'ов. 2) Копирование элемента, удовлетворяющего предикату в только что выделенный массив. 3) Отрезаем не заполненный кусок массива данных Вопрос №3: Разве не легче было реализовать функцию из вопроса Они дают разный результат Есть ли существенные различия или даже преимущества между функцией из вопроса №2 и функцией из вопроса №3? Для варианта из вопроса №2 память выделяется непосредственно в функции, а потом усекается. Для варианта из третьего вопроса эта ответственность ляжет на вызывающую сторону. Преимущества и недостатки есть у обоих способов. Всё зависит от того, как их использовать.

Ответ 2



Я уже писал об этом раньше. Когда вы реализуете функцию, размер массива-результата которой заранее неизвестен, т.е. становится известен только внутри функции, то возникает проблема выделения правильного количества памяти для сохранения результата. Пользователь (т.е. вызывающий код) в общем случае заранее эту память выделить не может, ибо пока еще не знает, сколько памяти понадобится. В таких случаях есть несколько основных стратегий: В некоторых случаях заранее известен максимально возможный размер результата, можно выделить память заранее "по максимуму" и такое максимальное выделение является, условно выражаясь, "разумным". Это как раз ваш случай. И да, ваш №3 будет прекрасно работать по этой схеме. Пользователь только обязан предоставить выходной массив размера не менее size и иметь в виду, что в общем случае не весь он будет использован. Выделять память внутри функции: либо сразу, когда ее размер становится известен, либо инкрементально по мере накопления результата, либо "по максимуму" с последующим урезанием, либо еще как. Это ваш вариант №2. Проблема этого подхода в том, что выделение памяти в таком случае нелегко кастомизировать. В простейшем случае будет просто malloc, как у вас в примере. А может пользователю не нужен malloc? Реализовать функцию по образу и подобию snprintf, т.е. предоставить возможность "холостого" прохода, который лишь подсчитывает и возвращает размер результата, но пока не формирует результат. Получив подсчитанный размер, пользователь выделяет память любым удобным для него образом и затем снова вызывает ту же функцию, теперь уже для "настоящего" прохода, т.е. для формирования результата в буфере. То есть ваши №2 и №3 - два альтернативных подхода к решению задачи. Оба способа по-своему правильны.

Ответ 3



В дополнение к ответу Groessmah добавлю пояснение к 3-му вопросу: Ваша ф-ция реализована ошибочно: во-первых отсутствует выделение памяти - у вас указатель все еще указывает на NULL! А для того, чтобы выделить память в ф-ции и нужен указатель на указатель, иначе память выделить вы сможете, но вот сделат так, чтобы нужный нам указатель указывал на нее - нет. Во-вторых, если мы и выделим память до вызова ф-ции, урезать ее в ф-ции, опять таки, не получится.

Комментариев нет:

Отправить комментарий