Есть ли способ передать массив неизвестного типа в качестве параметра функции в C? - PullRequest
0 голосов
/ 09 января 2019

Я пытался улучшить свои навыки и знания в C. Сегодня я попытался создать функцию, которая принимает массив любого типа, но я не нашел успешного способа, я использую ANSI C и я попытался передать его как пустой указатель, но когда я пытаюсь просмотреть память, работающую с параметром, компилятор жалуется. Есть ли способ добиться этого? Я думал, что, возможно, это можно сделать с помощью директив препроцессора, но я не уверен.

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

Это вывод процесса компиляции:

array_test.c: в функции 'array_fill':

array_test.c: 34: 13: предупреждение: указатель типа 'void *', используемый в арифметике [-Wpointer-arith]

* (массив + i) = данные;

^

array_test.c: 34: 5: предупреждение: разыменование указателя 'void *'

* (массив + i) = данные;

^ ~~~~~~~~~~~

array_test.c: 34: 5: ошибка: недопустимое использование пустого выражения

* (массив + i) = данные;

^

А это мой код:

#include <stdio.h>

#define array_length(array) (sizeof(array)/sizeof(array[0]))

#define ARBITRARY_SIZE 10    

typedef enum
{
  false,
  true
} bool;

int array_fill(void*, int, int);

int main(int argc, char* argv[])
{
  int array[ARBITRARY_SIZE];
  int i;

  array_fill(array, array_length(array), 0);

  for(i = 0; i < array_length(array); i++)
  {
    printf("array[%d]: %d\n", i, *(array + i));
  }

  return 0;
} 

int array_fill(void* array, int size, int data)
{
  int i;

  for(i = 0; i < size; i++)
  {
    *(array + i) = data; 
  }

  /*I will implement a check later, in case of errors.*/
  return 0; 
}

Ответы [ 5 ]

0 голосов
/ 09 января 2019

Слишком похоже на @ Все более идиотский хороший ответ.

Так что я сделаю эту вики. Полезно для справки.


Есть ли способ передать массив неизвестного типа в качестве параметра функции в C?

Да, код может вызывать такую ​​функцию с массивом, но массив будет преобразован в адрес первого элемента массива. Именно этот адрес будет использовать функция.

some_type a[N];
foo(a);

Чтобы функция принимала любой тип объекта массива, параметром функции является void *.

int foo(void *address_of_first_element);

К сожалению foo() потерял тип.


Есть ли способ достичь этого?

В случае OP, array_fill() нужен только размер типа, а не сам тип. Поэтому передайте размер шрифта.

OP видит размер массива и передает его - хорошо. Также необходим размер элемента и указатель на значение заполнения.

Чтобы сделать математику указателя, преобразуйте void* в char *, так как математика указателя на void* не определяется C.

// int array_fill(void* array, int size, int data)
int array_fill(void* array, size_t a_size, const char *fill, size_t e_size) {
  char *data = array;
  for(size_t a = 0; a < a_size; a++) {
    memcpy(data, fill, e_size);  // Copy `e_size` bytes.
    data += e_size;              // Advance `e_size` bytes. 
  }
  return 0; 
}

int main(void) {
  int array[ARBITRARY_SIZE], fill_value = 42;    
  array_fill(array, array_length(array), &fill_value, sizeof array[0]);

  for(size_t i = 0; i < array_length(array); i++) {
    printf("array[%zu]: %d\n", i, *(array + i));
  }

  return 0;
} 
0 голосов
/ 09 января 2019

Без типа данные, на которые ссылается array, не имеют размера элемента, поэтому арифметика указателя не определена. Чтобы выражение было допустимым, вы должны привести array к соответствующему типу данных, например:

*((int*)array + i) = data; 

Но это побеждает цель иметь неопределенный тип. Простое и наиболее эффективное решение - определить отдельные функции для массива каждого типа, который вы хотите заполнить. Можно определить функцию, которая будет обрабатывать несколько целых типов таким образом:

int array_fill(void* array, size_t array_length, long long data, size_t data_size )
{
    if( data_size > sizeof(data) )
    {
        data_size = sizeof(data) ;
    }

    for( size_t i = 0; i < array_length; i++)
    {
        for( int b = 0; b < data_size; b++ )
        {  
            ((char*)array)[i * data_size + b] = (data >> (b * 8)) & 0xff ;
        }
    }

  return 0; 
}

Вышесказанное делает два предположения:

  • цель использует порядок байтов в младшем порядке,
  • цель имеет 8-битный char тип.

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

Затем можно вызвать функцию, в вашем случае, например, так:

array_fill( array, array_length(array), 0, sizeof(*array) ) ;

и array могут иметь любой тип.

Однако заполнение массива нулями - это особый случай, который не требует этой сложности (то есть для вашего примера использования он не имеет смысла). Следующее:

memset( array, sizeof(array), 0 ) ;

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

array_fill( array, array_length(array), 0x01234ABCD, sizeof(*array) ) ;

Теперь, если array имеет тип uint8_t, например, он будет заполнен 0xCD, если это uint16_t, тогда 0xABCD. Если это было long long и для цели, которая является 64-битным типом, она будет заполнена 0x0000000001234ABCD.

Возможно, если несколько громоздко, использовать эту функцию для заполнения массива float или double, например:

double array[ARBITRARY_SIZE];
double x = 0.5 ;
array_fill(array, ARBITRARY_SIZE, *(long long*)(&x), sizeof(array) );

Другой подход, который позволяет использовать в качестве заливки также агрегированные типы или даже последовательности произвольной длины:

int array_fill( void* array, size_t array_length, 
                const void* fill_pattern, size_t fill_pattern_length )
{
    for( size_t i = 0; i < array_length; i++)
    {
        for( int b = 0; b < fill_pattern_length; b++ ) 
        {  
            ((char*)array)[i * fill_pattern_length + b] = ((char*)fill_pattern)[b] ;
        }
    }

  return 0; 
}

Тогда его можно использовать действительно для любого типа. Примеры:

Double

double array[ARBITRARY_SIZE], x = 0.5 ;
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );

INT

int array[ARBITRARY_SIZE], x = 123456 ;
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );

структура

struct S{ int x; double f ; } array[ARBITRARY_SIZE], x = {1234, 0.5};
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );

2D массив

int array[ARBITRARY_SIZE][2], x[2] = { 12, 98 } ;
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );

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

Эта последняя реализация может быть улучшена (упрощена и сделана более эффективной); например:

int array_fill( void* array, size_t array_length, 
                const void* fill_pattern, size_t fill_pattern_length )
{
    for( size_t i = 0, byte_index = 0; 
         i < array_length; 
         i++, byte_index += fill_pattern_length )
    {
        memcpy( &((char*)array)[byte_index], fill_pattern, fill_pattern_length ) ;
    }

  return 0; 
}

Это версия, с которой я бы пошел.

0 голосов
/ 09 января 2019

Указатели указывают на начало какого-либо объекта в памяти. Большинство указателей также знают размер этого объекта в памяти через тип, исключение составляет void *.

например. Если значение указателя на 32-разрядное целое число равно 0, мы знаем, что биты от 0 до 31 содержат данные, соответствующие этому 32-разрядному целому числу.

0  31
|---| <- 32 bits storing the data for a 32-bit integer

Что еще более важно для вашего вопроса, если мы знаем, что этот указатель указывает на последовательность 32-разрядных целых чисел, мы знаем, что мы можем получить следующее целое число, переместив указатель вперед на 32 бита. Например. второе целое число будет начинаться с 32.

0  31 32 63
|---| |---|

This is what int[2]. might look like in memory on a 32-bit system

Так работает арифметика указателей. С пустым указателем void *array вы не можете сделать array++ или даже *array, потому что нет способа узнать, сколько битов для продвижения указателя или сколько битов соответствуют array.

0    ??
|----

We don't know how many bits a void pointer points to

Технически Вы можете обойти это, передав также размер объекта, хотя это, вероятно, не очень хорошая идея.

// array points to the memory to be filled
// len is the number of elements in the array
// size is the size of an element (in bytes)
// fill points to an object to be used to fill array
void array_fill(void* array, int len, size_t size, void* fill) {
    // char is always a single byte
    char* byte_ptr = (char*) array;

    for (int i = 0; i < len; i++) {
        // Fill the current element
        memcpy(byte_ptr, fill, size);

        // Advance byte_ptr the correct number of bytes
        byte_ptr += size;
    }
}

Если вы не хотите использовать memcpy, вы также можете вручную скопировать объект fill в byte_ptr по одному байту за раз.

0 голосов
/ 09 января 2019

Если вы ожидаете заполнить массив типа данными, скажем, массив типа double со значением 2.2 или даже en массив структуры {int a; что-нибудь б}; тогда в основном ответ - нет, вы не можете сделать это таким образом.

Вы можете использовать макрос для этого, как

# define FILL_ARRAY(arr, data, len) for (size_t i = 0; i < len; i++) { arr[i] = data }

Но это не функция.

Но вы можете создать функцию, которая принимает обратный вызов и может назначать данные, например:

void fill_array(void * array, size_t item_size, size_t array_len, void (*cb)(void *))
{
    unsigned char *bytes = array;
    for (size_t i = 0; i < array_len; i++) {
        cb(&bytes[i * item_size]);
    }
}

void fill_double(void *data)
{
    const value = 2.2;
    double *ptr = *data;

    *data = value;
}

int main(void)
{
    double array[30];

    fill_array(array, sizeof double, 30, fill_double);
}

Не уверен, что это того стоит, но это должно выглядеть как решение вашего вопроса (не скомпилировано, может содержать ошибки)

0 голосов
/ 09 января 2019

Проблема здесь двоякая. Первый - разыменование указателя void, а другой выполняет арифметику с ним. Компилятор также предупреждает вас об этом, как вы показали в своем посте.

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

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

...