I've been trying to improve my skills and knowledge in C. Today I've tried to create a function that takes an array of any type, but I haven't found a successful way, I'm using ANSI C and I tried to pass it as a void pointer, but when I'm trying to go through the memory operating with the parameter, the compiler complains. Is there any way to achieve it? I was thinking that possibly it can be done through pre-processor directives, but I ain't sure.
P.S: My aim isn't to fill the array with data, that's just a function, but to understand and learn how to pass data if I don't know its type, or allow my function to work with more than just one type of data.
This is the output of the compilation process:
array_test.c: In function 'array_fill':
array_test.c:34:13: warning: pointer of type 'void *' used in arithmetic [-Wpointer-arith]
*(array + i) = data;
^
array_test.c:34:5: warning: dereferencing 'void *' pointer
*(array + i) = data;
^~~~~~~~~~~~
array_test.c:34:5: error: invalid use of void expression
*(array + i) = data;
^
And this is my code:
#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;
}
Pointers point to the beginning of some object in memory. Most pointers also know the size of that object in memory through the type, the exception being void *.
E.g. If the value of a pointer to 32-bit integer is 0, we know bits 0 through 31 contain the data corresponding to that 32-bit integer.
0 31
|---| <- 32 bits storing the data for a 32-bit integer
More importantly for your question, if we know that this pointer points to a sequence of 32-bit integers, we know that we can get the next integer by moving the pointer forward by 32 bits. E.g. the second integer would start at 32.
0 31 32 63
|---| |---|
This is what int[2]. might look like in memory on a 32-bit system
This is how pointer arithmetic works. With a void pointer void *array you cannot do array++ or even *array because there is no way to know how many bits to advance the pointer or how many bits correspond to array.
0 ??
|----
We don't know how many bits a void pointer points to
You can technically get around this by passing the size of the object as well, although this is probably not a good idea.
// 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;
}
}
If you don't want to use memcpy you could also manually copy the fill object to byte_ptr one byte at a time.
Without a type the data referenced by array has not element size so the pointer arithmetic is undefined. For the expression to be valid, you must cast array to an appropriate data type such as:
*((int*)array + i) = data;
But that defeats the purpose of having an undefined type. The simple and most efficient solution is to define separate functions for array each type you wish to fill. It is possible to define a function that will handle multiple integer types thus:
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;
}
The above makes two assumptions:
char type.Modifications are necessary where those assumptions are not true. Note I have also used array index notation rather than pointer arithmetic - it results in fewer parentheses so is easier to read.
The function might then be called, in your case for example thus:
array_fill( array, array_length(array), 0, sizeof(*array) ) ;
and array may have any type.
However filling an array with zero is a special case that does not need this complexity, (i.e. for your example usage it serves no purpose). The following :
memset( array, sizeof(array), 0 ) ;
has the same effect some all bytes of the integer 0 are zero in any case. The function is of more use for values where each byte differs.
array_fill( array, array_length(array), 0x01234ABCD, sizeof(*array) ) ;
Now if array is of type uint8_t for example it will be filled with 0xCD, if it is uint16_t then 0xABCD. If it were long long and on the target that is a 64 bit type, it will be filled with 0x0000000001234ABCD.
It is possible if somewhat cumbersome to also use this function to fill a float or double array thus for example:
double array[ARBITRARY_SIZE];
double x = 0.5 ;
array_fill(array, ARBITRARY_SIZE, *(long long*)(&x), sizeof(array) );
Another approach that allows also aggregate types or even arbitrary length sequences to be used as the fill is:
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;
}
Then it can be used truly for any type. Examples:
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
struct S{ int x; double f ; } array[ARBITRARY_SIZE], x = {1234, 0.5};
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
2D array
int array[ARBITRARY_SIZE][2], x[2] = { 12, 98 } ;
array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
The implementation avoids endian issues, but cannot accept literal-constant initialisers, because you cannot take the address.
This last implementation can be improved (simplified, and made more efficient); for example:
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;
}
That's the version I'd go with.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With