在C语言中编写一个通用函数,如何处理字符串

13

我有一个函数,它接受一个 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
让我换一种说法:你最终分配的那些 char 指针指向什么? - Seva Alekseyev
6
是的!我们有一位三星级的虚空程序员! - wildplasser
使用 foo_xxx 的方式来实现可能会创建一个较大的接口,但它将更有效、更明确,实现也更简单。 - goji
1
@KVM 注意:void* 是通用的,但 void** 不是。 - Grijesh Chauhan
@troy “这样通用的函数有什么必要呢?数据类型通常是在运行时确定的吗?” -- foo实际上是一个read_from_file函数,用户在运行时提供要读取的文件名和其中的数据类型。 - Korizon
显示剩余12条评论
5个回答

7
这种情况应该如何处理?我能否将char***传递给期望void**的函数,但在其中正确地进行强制类型转换?
不行,这在技术上是未定义的行为。在你的计算机上它可能有效,但在实现不同指针类型且具有不同表示形式的未来计算机上会失败,这是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***,那么这个转换是合法的,在任何代码路径中都不会有未定义行为。


1
@BrianCampbell:在Adam的例子中,&values的类型是void **,函数接受该类型,因此没有转换。在您的例子中,&values的类型是char ***,显然与类型void **不同。C11 6.3.2.3.7说:“对象类型的指针可以转换为指向不同对象类型的指针。如果所得到的指针对于引用类型没有正确对齐,则行为未定义。否则,在再次转换时,结果应与原始指针相等。” - Crowman
1
这是未定义的行为,因为实现允许针对指向的类型具有多个不同的指针表示。为了避免未定义的行为,您需要将任何指针实际转换为或从void *指针转换--您不能只转换void **并期望指向的指针具有其类型转换。但是,大多数实现都没有多个指针表示,所以一切都没问题。 - Chris Dodd
1
@BrianCampbell:在C11 6.3.2.3.1下,您可以放心地在void *和任何其他指向对象类型的指针之间进行安全转换。因此,虽然在char ***void **之间进行转换不保证是OK的,但在char ***void *之间进行转换是保证OK的。 void *是唯一保证这样做是OK的指针类型。 - Crowman
1
@willus:好的,我现在明白你的意思了。转换子句处理void *和其他指针类型之间的转换,因此当从double *转换为void *时,您已经拥有一个正确对齐的double *,而void *必须能够存储它并将其返回。在您使用malloc()的示例中,如果存在这样的对齐要求,我认为它必须被编写为仅返回满足任何和所有对齐要求的指针。 - Crowman
@PaulGriffiths--如果是这样的话,我认为大部分讨论都是学术性的,但仍然非常有趣。我想我已经回答了自己的问题。在gcc/i386上,我使用malloc函数分配了几个不同的内存大小(连续地),它们都返回16的倍数的值。 - willus
显示剩余11条评论

5
一个 void * 只是指向未指定类型的指针;它可以是指向一个 int,或者一个 char,或者一个 char *,或者一个 char **,或者任何你想要的类型,只要你确保在解引用时将其视为适当的类型(或者原始类型可以安全地被解释为该类型之一)。
因此,一个 void ** 只是指向一个 void * 的指针,它可以是指向任何你想要的类型的指针,例如一个 char *。所以,如果你正在分配某些对象类型的数组,并且在某种情况下这些对象是 char *,那么你可以使用 void ** 来引用它们,从而得到一个可以称为 char *** 的东西。
通常不直接看到这种结构,因为通常你会将一些类型或长度信息附加到数组上,而不是有一个 char ***,你有一个类似于 struct typed_object **foo 的东西,其中 struct typed_object 有一个类型标签和指针,你将从这些元素中提取的指针强制转换为适当的类型,或者你有一个 struct typed_array *foo,它是一个包含类型和数组的结构体。
关于风格的一些注意事项。首先,这种做法可能会使你的代码难以阅读。一定要非常小心地将其结构化并明确地记录下来,以便人们(包括你自己)可以弄清楚发生了什么。此外,请不要转换 malloc 的结果;void * 自动升级为其分配的类型,如果你忘记包含 <stdlib.h> 或者更新类型声明但忘记更新转换,转换 malloc 的结果可能会导致微妙的错误。有关更多信息,请参见这个问题
此外,将声明中的 * 附加到变量名而不是类型名上通常是一个好习惯,因为这就是实际解析的方式。下面声明了一个 char 和一个 char *,但是如果按照你一直写的方式编写它,你可能会期望它声明两个 char *
char *foo, bar;

或者用另一种方式写:

char* foo, bar;

3
您不需要(并且可能不应该)使用void **,只需使用常规的void *。根据C11 6.3.2.3.1,“指向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 **,它将被 foo() 隐式转换为 void *,并且 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 指向新分配的内存块。

OP希望函数foo分配内存并通过输出参数返回已分配的内存,而不需要调用函数分配内存。因此,您需要一个void **,以便可以对其进行解引用并将地址返回给调用者。 - Adam Rosenfield
@AdamRosenfield:如果他传递的第一个参数是调用者指针的地址,你不需要使用void **,普通的void *就可以了。任何函数都可以通过接受指针地址为其调用者动态分配内存。如果调用函数执行了int * allocate_me; foo(&allocate_me, 1);,那么我的第一个示例将按要求工作。&allocate_me当然是int **类型,它会隐式转换为void *,然后foo()将其转换回int **并间接修改allocate_me - Crowman
使用void *的目的是避免您提到的未定义行为。如果foo接受一个void *,它可能真的是一个float **int **char ***,具体取决于datatype参数,它可以将其转换为正确的类型并存储它,存储调用者实际期望的指针类型。 - Chris Dodd

1

已经有一种内置机制可以实现这个功能,而且还允许可变数量的参数。通常以以下格式出现: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); 没有比这更通用的了。你甚至可以设置多个整数类型来告诉它在特定的整数上运行不同的函数集。如果格式字符串太繁琐,那么你也可以很容易地使用int datatype代替它,但是你将受到1个参数的限制(从技术上讲,你可以使用位操作符 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);
}

0

有一些技巧,你可以做到。看看这个例子:

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

在调用者中,只需将返回值转换为适当的指针,并使用它。

3
这个 void *rc = (void*)malloc(... 接收一个 void *,并将其转换为 void * 以便在 void * 中存储它。在任何时候都不推荐将 malloc() 返回值转换类型,但这种情况有点过分了。 - Crowman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接