Написание обобщенной функции в 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***
. Я пробовал это и игнорировал предупреждения, он работал нормально.
Но разве это безопасно? Есть ли более изящная альтернатива?
Ответы
Ответ 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
В вызывающем абоненте просто передайте возвращаемое значение соответствующему указателю и работайте с ним.