Подтвердить что ты не робот

Написание обобщенной функции в C, как обрабатывать строки

У меня есть функция, которая принимает аргумент void** и целое число, которое указывает его тип данных

void foo (void** values, int datatype)

Внутри функции, в зависимости от типа данных, я делаю это так:

if (datatype == 1)
    *values = (int*) malloc (5 * sizeof(int));
else if (datatype == 2)
    *values = (float*) malloc (5 * sizeof(float));

Все хорошо до сих пор. Однако, когда персональные строки попадают в картину, все становится сложнее. void** должен быть void***, так как мне нужно будет сделать что-то вроде этого:

*values = (char**) malloc (5 * sizeof(char*));
for(i=0;i<5;i++)
    (*values)[i] = (char*) malloc (10);
..
strncpy( (*values)[0], "hello", 5);

Как следует справляться с такой ситуацией? Могу ли я передать char*** функции, ожидающей void**, но введя ее правильно внутри?

void foo (void** values, int datatype) {

if(datatype == 3) {
    char*** tmp_vals = (char***) values;
    *tmp_vals = (char**) malloc (5 * sizeof(char*));
    ...
    (*tmp_vals)[i] = (char*) malloc (10 * sizeof(char));
    strncpy (  (*tmp_vals)[i], "hello", 5);
}

Итак, я просто приложил void** к char***. Я пробовал это и игнорировал предупреждения, он работал нормально. Но разве это безопасно? Есть ли более изящная альтернатива?

4b9b3361

Ответ 1

Вам не нужно (и, вероятно, не следует) использовать void ** вообще - просто используйте обычный void *. Per C11 6.3.2.3.1 "указатель на void может быть преобразован в указатель или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно, результат будет сравниваться с исходным указателем". Переменная указателя, включая указатель на другой указатель, является объектом. void ** не является указателем на void ". Вы можете свободно и безопасно конвертировать в void * и void *, но вам не гарантируется возможность безопасного преобразования в void **.

Итак, вы можете просто сделать:

void foo (void* values, int datatype) {
    if ( datatype == 1 ) {
        int ** pnvalues = values;
        *pnvalues = malloc(5 * sizeof int);

    /*  Rest of function  */
}

и т.д., а затем называть его похожим на:

int * new_int_array;
foo(&new_int_array, 1);

&new_int_array имеет тип int **, который будет неявно преобразован в void * на foo(), а foo() преобразует его обратно в тип int ** и разыгрывает его, чтобы косвенно изменить new_int_array на укажите на новую память, которую он динамически выделил.

Для указателя на динамический массив строк:

void foo (void* values, int datatype) {

    /*  Deal with previous datatypes  */

    } else if ( datatype == 3 ) {
        char *** psvalues = values;
        *psvalues = malloc(5 * sizeof char *);
        *psvalues[0] = malloc(5);

    /*  Rest of function  */
}

и т.д. и назовите его:

char ** new_string_array;
foo(&new_string_array, 3);

Аналогично, &new_string_array является типом char ***, снова получает неявное преобразование в void *, а foo() преобразует его обратно и косвенно делает new_string_array ссылкой на вновь выделенные блоки памяти.

Ответ 2

Как следует справляться с такой ситуацией? Могу ли я передать char*** функции, ожидающей void**, но введя ее правильно внутри?

Нет, это технически Undefined Поведение. Возможно, он работает на вашем компьютере, но может быть неудачным на каком-то будущем компьютере, который реализует разные типы указателей с разными представлениями, что допускается стандартом языка C.

Если ваша функция ожидает void**, то вам лучше передать ее void**. Любой тип указателя может быть неявно преобразован в void*, но работает только на верхнем уровне: char* может быть преобразован в void*, а char** может быть неявно преобразован в void* (поскольку char** "указатель на char*" ), но char** не может быть преобразован в void**, а также char*** также не может быть преобразован в void**.

Правильный способ вызова этой функции - передать ему правильный void**, а затем вернуть результирующий указатель void* к его исходному типу:

void foo(void **values, int datatype)
{
    if(datatype == 3)
    {
        char ***str_values = ...;
        *values = str_values;  // Implicit cast from char*** to void*
    }
    else
    ...
}

...

void *values;
foo(&values, 2);
char ***real_values = (char ***)values;

Предполагая, что *values на самом деле указана на char***, то это действие допустимо и не имеет никакого поведения Undefined в любом из кодов кода.

Ответ 3

A void * - это просто указатель на неуказанный тип; это может быть указатель на int или char, или char *, или char **, или все, что вы хотели, до тех пор, пока вы убедитесь, что при разыменовании вы относитесь к нему как к соответствующему тип (или тот, который исходный тип можно было бы безопасно интерпретировать как).

Таким образом, a void ** является просто указателем на void *, который может быть указателем на любой тип, который вы хотите, например, char *. Поэтому да, если вы выделяете массивы некоторых типов объектов, и в одном случае эти объекты char *, то вы можете использовать void ** для ссылки на них, предоставляя вам что-то, что можно назвать char ***.

Обычно нередко видеть эту конструкцию напрямую, потому что обычно вы добавляете в массив некоторую информацию о типе или длине, вместо того, чтобы иметь char ***, у вас есть struct typed_object **foo или что-то вроде того, где struct typed_object имеет тип тег и указатель, и вы отбрасываете указатель, который вы извлекаете из этих элементов, в соответствующие типы или у вас есть struct typed_array *foo, который является структурой, содержащей тип и массив.

Несколько заметок о стиле. Во-первых, это может сделать ваш код трудным для чтения. Будьте очень осторожны, чтобы структурировать его и четко документировать, чтобы люди (включая вас самих) могли понять, что происходит. Кроме того, не выдавайте результат malloc; void * автоматически продвигается к типу, которому он назначен, а результат результата malloc может привести к тонким ошибкам, если вы забудете включить <stdlib.h> или ваше обновление объявления типа, но забудьте обновить бросок. См. этот вопрос для получения дополнительной информации.

И вообще хорошая привычка присоединять * к объявлению к имени переменной, а не к имени типа, как к тому, что он фактически анализирует. Следующее объявляет один char и один char *, но если вы напишете его так, как вы их пишете, вы можете ожидать, что он объявит два char *:

char *foo, bar;

Или написано другим способом:

char* foo, bar;

Ответ 4

Существует встроенный механизм, чтобы сделать это уже с добавленным бонусом, что он допускает переменное количество аргументов. Обычно это видно в этом формате yourfunc(char * format_string,...)

/*_Just for reference_ the functions required for variable arguments can be defined as:
#define va_list             char*
#define va_arg(ap,type)     (*(type *)(((ap)+=(((sizeof(type))+(sizeof(int)-1)) \
                                & (~(sizeof(int)-1))))-(((sizeof(type))+ \
                                (sizeof(int)-1)) & (~(sizeof(int)-1)))))
#define va_end(ap)          (void) 0
#define va_start(ap,arg)    (void)((ap)=(((char *)&(arg))+(((sizeof(arg))+ \
                                (sizeof(int)-1)) & (~(sizeof(int)-1)))))
*/

Итак, вот базовый пример, который вы могли бы использовать с строкой формата и переменным числом аргументов

#define INT '0'
#define DOUBLE '1'
#define STRING '2'

void yourfunc(char *fmt_string, ...){
  va_list args;
  va_start (args, fmt_string);
  while(*fmt_string){
    switch(*fmt_string++){
     case INT: some_intfxn(va_arg(ap, int));
     case DOUBLE: some_doublefxn(va_arg(ap, double));
     case STRING: some_stringfxn(va_arg(ap, char *));
     /* extend this as you like using pointers and casting to your type */
     default: handlfailfunc();
    }
  }
  va_end (args);
}

Итак, вы можете запустить его как: yourfunc("0122",42,3.14159,"hello","world"); или так как вам нужно только 1, чтобы начать с yourfunc("1",2.17); Это не становится намного более общим, чем это. Вы могли бы даже настроить несколько целых типов, чтобы сказать, чтобы он запускал другой набор функций для этого конкретного целого. Если format_string слишком утомительно, тогда вы можете так же легко использовать int datatype вместо этого, но вы будете ограничены 1 аргументом (технически вы можете использовать бит ops для OR datatype | num_args, но я отвлекаюсь)

Вот форма значения одного типа:

#define INT '0'
#define DOUBLE '1'
#define STRING '2'

void yourfunc(datatype, ...){ /*leaving "..." for future while on datatype(s)*/
  va_list args;
  va_start (args, datatype);
  switch(datatype){
     case INT: some_intfxn(va_arg(ap, int));
     case DOUBLE: some_doublefxn(va_arg(ap, double));
     case STRING: some_stringfxn(va_arg(ap, char *));
     /* extend this as you like using pointers and casting to your type */
     default: handlfailfunc();
  }
  va_end (args);
}

Ответ 5

С некоторыми трюками вы можете это сделать. Пример:

int sizes[] = { 0, sizeof(int), sizeof(float), sizeof(char *) }

void *foo(datatype) {
   void *rc = (void*)malloc(5 * sizes[datatype]);
   switch(datatype) {
     case 1: {
       int *p_int = (int*)rc;
       for(int i = 0; i < 5; i++)
         p_int[i] = 1;
     } break;
     case 3: {
       char **p_ch = (char**)rc;
       for(int i = 0; i < 5; i++)
         p_ch[i] = strdup("hello");
     } break;
   } // switch
   return rc;
} // foo

В вызывающем абоненте просто передайте возвращаемое значение соответствующему указателю и работайте с ним.