【问题标题】:How to generically assign a pointer passed into a function in C如何一般分配传递给C中函数的指针
【发布时间】:2019-08-14 02:55:39
【问题描述】:

我是 C 新手,想知道如何做一些 pointer stuff。具体来说,我想知道如何将指针传递给函数并“从函数中获取值”。有点像这样(半伪代码):

assign_value_to_pointer(void* pointer) {
  if (cond1) {
    pointer = 10;
  } else if (cond2) {
    pointer = "foo";
  } else if (cond3) {
    pointer = true;
  } else if (cond4) {
    pointer = somestruct;
  } else if (cond5) {
    pointer = NULL;
  } else if (cond6) {
    // unknown type!
    pointer = flexiblearraymember.items[index];
  }
}

main() {
  void* pointer = NULL;

  assign_value_to_pointer(&pointer);

  if (cond1) {
    assert(pointer == 10);
  } else if (cond2) {
    assert(pointer == "foo");
  } else if (cond3) {
    assert(pointer == true);
  } else if (cond4) {
    assert(pointer == somestruct);
  } else if (cond5) {
    assert(pointer == NULL);
  }
}

换一种说法:

p = new Pointer()
assign_a_value(p)
assert(p.value == 10) // or whatever

基本上是将指针传递给函数,函数为指针赋值,然后当它返回时,你可以在函数外部使用该值。您可能不知道您从函数中获得了什么样的值(但这可以通过扩展 this 以使用结构等来处理),因此是 void 指针。主要目标只是将指针传递给某个函数并让它吸收一些值。

想知道如何通过快速示例实现在 C 中正确执行此操作。不必涵盖所有案例即可开始。

我想用它来实现一些东西,比如将一个 NULL 错误对象传递给一个函数,如果有错误,它将错误的指针设置为一些错误代码等。

我认为这不应该是一个广泛的问题,但如果是,那么知道在哪里可以找到源代码中更全面的解释或示例会很有帮助。

【问题讨论】:

  • 在这种情况下,惯用的方法是使用void *指针。真的很难理解到底是什么你想要实现的...
  • 是的,你的问题甚至没有提到void **
  • 我是从下面的回答中了解到的。
  • new 不是 C 而是 C++。

标签: c function pointers void-pointers


【解决方案1】:

我想用它来实现一些东西,比如将一个 NULL 错误对象传递给一个函数,如果有错误,它将错误的指针设置为一些错误代码等。

从上面的引用和问题中的代码来看,您似乎正在寻找一个可以“保存”不同类型的变量,即有时您希望它是整数,有时是浮点数,有时是浮点数一个字符串等等。这在某些语言中称为变体,但在 C 中不存在 变体(有关变体的更多信息,请参见https://en.wikipedia.org/wiki/Variant_type

因此,在 C 语言中,您必须编写自己的变体类型。有几种方法可以做到这一点。我将在下面给出示例。

但首先是关于 C 中指针的几句话,因为问题中的代码似乎揭示了一个误解,因为它直接将值分配给指针,例如pointer = somestruct; 这是非法的。

在 C 中理解“指针的值”和“指向对象的值”之间的区别非常重要。第一个,即指针的值,告诉指针指向的位置,即指针的值是指向对象的地址。对指针的赋值会改变指针指向的位置。要更改指向对象的值,必须首先取消引用该指针。示例(伪代码):

pointer = &some_int; // Make pointer point to some_int

*pointer = 10;       // Change the value of the pointed to object, i.e. some_int
                     // Notice the * in front of pointer - it's the dereference
                     // that tells you want to operate on the "pointed to object"

pointer = 10;        // Change the value of the pointer, i.e. where it points to
                     // In other words, pointer no longer points to some_int

现在回到“变体”实现。如前所述,有几种方法可以在 C 中编写代码。

从您的问题来看,您似乎想使用空指针。这是可行的,我将首先展示一个使用 void-pointer 的示例,然后再展示一个使用联合的示例。

在您的问题中不清楚 cond 是什么,所以在我的示例中,我将假设它是一个命令行参数,并且我只是添加了一些解释以便有一个运行示例。

示例的常见模式是使用“标签”。这是一个额外的变量,它告诉当前对象类型的值(也称为元数据)。所以一般变体数据类型看起来像:

struct my_variant
{
    TagType tag;     // Tells the current type of the value object
    ValueType value; // The actual value. ValueType is a type that allows
                     // storing different object types, e.g. a void-pointer or a union
}

示例 1:空指针和强制类型转换

下面的示例将使用 void 指针指向包含实际值的对象。有时是整数,有时是浮点数或任何需要的值。使用 void 指针时,有必要在取消引用指针之前强制转换 void 指针(即在访问指向的对象之前)。 tag 字段告诉了指向对象的类型,从而也告诉了强制转换的方式。

#include <stdio.h>
#include <stdlib.h>

// This is the TAG type.
// To keep the example short it only has int and float but more can 
// be added using the same pattern
typedef enum
{
    INT_ERROR_TYPE,
    FLOAT_ERROR_TYPE,
    UNKNOWN_ERROR_TYPE,
} error_type_e;

// This is the variant type
typedef struct
{
    error_type_e tag;  // The tag tells the type of the object pointed to by value_ptr
    void* value_ptr;   // void pointer to error value
} error_object_t;

// This function evaluates the error and (if needed)
// creates an error object (i.e. the variant) and
// assigns appropriate values of different types
error_object_t* get_error_object(int err)
{
    if (err >= 0)
    {
        // No error
        return NULL;
    }

    // Allocate the variant
    error_object_t* result_ptr = malloc(sizeof *result_ptr);

    // Set tag value
    // Allocate value object
    // Set value of value object
    if (err > -100)  // -99 .. -1 is INT error type
    {
        result_ptr->tag = INT_ERROR_TYPE;
        result_ptr->value_ptr = malloc(sizeof(int));
        *(int*)result_ptr->value_ptr = 42;
    }
    else if (err > -200)  // -199 .. -100 is FLOAT error type
    {
        result_ptr->tag = FLOAT_ERROR_TYPE;
        result_ptr->value_ptr = malloc(sizeof(float));
        *(float*)result_ptr->value_ptr = 42.42;
    }
    else
    {
        result_ptr->tag = UNKNOWN_ERROR_TYPE;
        result_ptr->value_ptr = NULL;
    }
    return result_ptr;
}

int main(int argc, char* argv[])
{
    if (argc < 2) {printf("Missing arg\n"); exit(1);}
    int err = atoi(argv[1]);  // Convert cmd line arg to int

    error_object_t* err_ptr = get_error_object(err);
    if (err_ptr == NULL)
    {
        // No error

        // ... add "normal" code here - for now just print a message
        printf("No error\n");
    }
    else
    {
        // Error

        // ... add error handler here - for now just print a message
        switch(err_ptr->tag)
        {
            case INT_ERROR_TYPE:
                printf("Error type INT, value %d\n", *(int*)err_ptr->value_ptr);
                break;
            case FLOAT_ERROR_TYPE:
                printf("Error type FLOAT, value %f\n", *(float*)err_ptr->value_ptr);
                break;
            default:
                printf("Error type UNKNOWN, no value to print\n");
                break;
        }

        free(err_ptr->value_ptr);
        free(err_ptr);
    }

    return 0;
}

运行这个程序的一些例子:

> ./prog 5
No error
> ./prog -5
Error type INT, value 42
> ./prog -105
Error type FLOAT, value 42.419998
> ./prog -205
Error type UNKNOWN, no value to print

如上例所示,您可以使用 void-pointer 实现变体类型。但是,代码需要大量转换,这使得代码难以阅读。一般来说,我不会推荐这种方法,除非您有一些特殊要求强制使用 void-pointer。

示例 2:指向联合的指针

如前所述,C 没有其他语言中已知的变体。但是,C 有一些非常接近的东西。那是工会。一个联合可以在不同的时间持有不同的类型——它错过的只是tag。因此,您可以使用标签和联合,而不是使用标签和空指针。好处是 1) 不需要强制转换,并且 2) 避免了 malloc。示例:

#include <stdio.h>
#include <stdlib.h>

typedef enum
{
    INT_ERROR_TYPE,
    FLOAT_ERROR_TYPE,
    UNKNOWN_ERROR_TYPE,
} error_type_e;

// The union that can hold an int or a float as needed
typedef union
{
    int n;
    float f;
} error_union_t;

typedef struct
{
    error_type_e tag;    // The tag tells the current union use
    error_union_t value; // Union of error values
} error_object_t;

error_object_t* get_error_object(int err)
{
    if (err >= 0)
    {
        // No error
        return NULL;
    }

    error_object_t* result_ptr = malloc(sizeof *result_ptr);
    if (err > -100)  // -99 .. -1 is INT error type
    {
        result_ptr->tag = INT_ERROR_TYPE;
        result_ptr->value.n = 42;
    }
    else if (err > -200)  // -199 .. -100 is FLOAT error type
    {
        result_ptr->tag = FLOAT_ERROR_TYPE;
        result_ptr->value.f = 42.42;
    }
    else
    {
        result_ptr->tag = UNKNOWN_ERROR_TYPE;
    }
    return result_ptr;
}

int main(int argc, char* argv[])
{
    if (argc < 2) {printf("Missing arg\n"); exit(1);}

    int err = atoi(argv[1]);  // Convert cmd line arg to int

    error_object_t* err_ptr = get_error_object(err);
    if (err_ptr == NULL)
    {
        // No error

        // ... add "normal" code here - for now just print a message
        printf("No error\n");
    }
    else
    {
        // Error

        // ... add error handler here - for now just print a message
        switch(err_ptr->tag)
        {
            case INT_ERROR_TYPE:
                printf("Error type INT, value %d\n", err_ptr->value.n);
                break;
            case FLOAT_ERROR_TYPE:
                printf("Error type FLOAT, value %f\n", err_ptr->value.f);
                break;
            default:
                printf("Error type UNKNOWN, no value to print\n");
                break;
        }

        free(err_ptr);
    }

    return 0;
}

在我看来,这段代码比使用 void-pointer 的代码更容易阅读。

示例 3:联合 - 无指针 - 无 malloc

即使示例 2 比示例 1 更好,示例 2 中仍然存在动态内存分配。动态分配是大多数 C 程序的一部分,但只有在真正需要时才应使用它。换句话说 - 具有自动存储持续时间的对象(也称为局部变量)应在可能的情况下优先于动态分配的对象。

下面的例子展示了如何避免动态分配。

#include <stdio.h>
#include <stdlib.h>

typedef enum
{
    NO_ERROR,
    INT_ERROR_TYPE,
    FLOAT_ERROR_TYPE,
    UNKNOWN_ERROR_TYPE,
} error_type_e;

typedef union
{
    int n;
    float f;
} error_union_t;

typedef struct
{
    error_type_e tag;    // The tag tells the current union usevalue_ptr
    error_union_t value; // Union of error values
} error_object_t;

error_object_t get_error_object(int err)
{
    error_object_t result_obj;
    if (err >= 0)
    {
        // No error
        result_obj.tag = NO_ERROR;
    }
    else if (err > -100)  // -99 .. -1 is INT error type
    {
        result_obj.tag = INT_ERROR_TYPE;
        result_obj.value.n = 42;
    }
    else if (err > -200)  // -199 .. -100 is FLOAT error type
    {
        result_obj.tag = FLOAT_ERROR_TYPE;
        result_obj.value.f = 42.42;
    }
    else
    {
        result_obj.tag = UNKNOWN_ERROR_TYPE;
    }
    return result_obj;
}

int main(int argc, char* argv[])
{
    if (argc < 2) {printf("Missing arg\n"); exit(1);}
    int err = atoi(argv[1]);  // Convert cmd line arg to int

    error_object_t err_obj = get_error_object(err);

    switch(err_obj.tag)
    {
        case NO_ERROR:
            printf("No error\n");
            break;    
        case INT_ERROR_TYPE:
            printf("Error type INT, value %d\n", err_obj.value.n);
            break;
        case FLOAT_ERROR_TYPE:
            printf("Error type FLOAT, value %f\n", err_obj.value.f);
            break;
        default:
            printf("Error type UNKNOWN, no value to print\n");
            break;
    }

    return 0;
}

总结

有很多方法可以解决 OP 解决的问题。这个答案给出了三个例子。在我看来,示例 3 是最好的方法,因为它避免了动态内存分配和指针,但在某些情况下示例 1 或 2 可能更好。

【讨论】:

    【解决方案2】:

    你实际上可以根据case(条件)在main()中类型转换指针并使用。但是,在我看来,您可以为此目的使用联合。

    创建一个包含所有可能数据类型的联合。

    typedef union _my_union_type_ {
        int intVal;
        char* stringVal;
        bool boolVal;
        SomestructType somestruct;//Assuming you need a structure not structure pointer.
        void*   voidPtrType;
    } my_union_type;
    

    现在在main(),创建这个联合类型的变量并将联合的地址传递给函数。

    main() {
      my_union_type my_union;
      memset(&my_union, 0x00, sizeof(my_union));
    
      assign_value_to_pointer(&my_union);
    
      if (cond1) {
        assert(my_union.intVal == 10);
      } else if (cond2) {
        assert(strcmp(my_union.stringVal, "foo")); //String comparison can not be done using '=='
      } else if (cond3) {
        assert(my_union.boolVal == true);
      } else if (cond4) {
        assert(memcmp(&my_union.somestruct, &somestruct, sizeof(somestruct)); //Assuming structure not structure pointer.
      } else if (cond5) {
        assert(my_union.voidPtrType == NULL);
      } else if (cond5) {
        //Check my_union.voidPtrType
      }
    }
    

    并且在assign_value_to_pointer中,你可以将需要的值存储在联合变量中。

    assign_value_to_pointer(my_union_type* my_union) {
      if (cond1) {
        my_union->intVal = 10;
      } else if (cond2) {
        my_union->stringVal = "foo";
      } else if (cond3) {
        my_union->boolVal = true;
      } else if (cond4) {
        memcpy(&(my_union->somestruct), &somestruct, sizeof(somestruct));
      } else if (cond5) {
        my_union->voidPtrType = NULL;
      } else if (cond6) {
        // unknown type!
        my_union->voidPtrType = flexiblearraymember.items[index];
      }
    }
    

    【讨论】:

      【解决方案3】:

      首先,我会直接回答你的问题,希望你明白为什么你需要非常小心。这对于实现队列或通信堆栈可能是一种有用的技术 - 但您需要确定您可以重新跟踪正在存储的类型,否则您的程序逻辑将完全中断。然后,我将尝试简要介绍一些用例和一些使其安全的方法。

      按照你说的做的简单例子

      #include <stdio.h>
      #include <stdlib.h>
      
      //Some basic error type for reporting failures
      typedef enum my_error
      {
          ERROR_NONE = 0,
          ERROR_FAIL = 1,
      } my_error;
      
      struct my_struct
      {
          int age;
          char *name;
          int order_count;
      };
      
      int someCond = 1;
      
      //Let's start with a simple case, where we know the type of the pointer being passed (an int)
      //If int_out is NULL, then this function will invoke undefined behavior (probably a 
      //runtime crash, but don't rely on it).
      my_error assign_int(int *int_out)
      {
          if(someCond)
              *int_out = 5;
          else
              *int_out = 38;
      
          return ERROR_NONE;
      }
      
      //Need to use a 'double pointer', so that this function is actually changing the pointer 
      //that exists in the parent scope
      my_error dynamically_assign_value_to_pointer(void **pointer)
      {
          //A pointer internal to this function just to simplify syntax
          void *working_ptr = NULL;
      
          if(someCond)
          {
              //Allocate a region of memory, and store its location in working_ptr
              working_ptr = malloc(sizeof(int));
              //store the value 12 at the location that working_ptr points to (using '*' to dereference)
              *((int *) working_ptr) = 12;
          }
          else
          {
              //Allocate a region of memory, and store its location in working_ptr
              working_ptr = malloc(sizeof(struct my_struct));
              //Fill the struct with data by casting (You can't dereference a void pointer, 
              //as the compiler doesn't know what it is.)
              ((struct my_struct *) working_ptr)->age = 22;
              ((struct my_struct *) working_ptr)->name = "Peter";
              ((struct my_struct *) working_ptr)->order_count = 6;
          }
      
          //Set the pointer passed as an argument to point to this data, by setting the 
          //once-dereferenced value
          *pointer = working_ptr;
      
          return ERROR_NONE;
      }
      
      int main (int argc, char *argv[])
      {
          int an_int;
          void *some_data;
      
          assign_int(&an_int);
      
          //an_int is now either 5 or 38
      
          dynamically_assign_value_to_pointer(&some_data);
      
          //some_data now points to either an integer OR a my_struct instance. You will need 
          //some way to track this, otherwise the data is useless.
          //If you get this wrong, the data will be interpreted as the wrong type, and the 
          //severity of the issue depends what you do with it.
          //For instance, if you KNOW FOR SURE that the pointer contains the int, you could 
          //print it by:
          printf("%d", *((int *) some_data));
      
          //And because it is dynamically allocated, you MUST free it.
          free(some_data);
      
          return 0;
      }
      

      实际上,这对队列很有用,例如,您可以编写一个通用队列函数,然后为不同的数据类型创建不同的队列。这是部分代码,因此无法编译,并且在这种有限的情况下是个坏主意,因为类型安全的替代方案对设计来说是微不足道的,但希望你能明白:

      extern my_queue_type myIntQueue;
      extern my_queue_type myStructQueue;
      
      my_error get_from_queue(void *data_out, my_queue_type queue_in);
      
      int main (int argc, char *argv[])
      {
          //...
          int current_int;
          struct my_struct current_struct;
      
          get_from_queue(&current_int, myIntQueue);
          get_from_queue(&current_struct, myStructQueue);
      
          //...
      }
      

      或者,如果您真的想将许多不同的类型存储在一起,您至少应该在结构中跟踪类型以及指针,以便在必要时使用“开关”来适当地转换和处理逻辑。同样,部分示例无法编译。

      enum my_types
      {
          MY_INTEGER, MY_DOUBLE, MY_STRUCT
      };
      
      struct my_typed_void
      {
          void *data;
          enum my_types datatype;
      };
      
      my_error get_dynamic_from_global_queue(struct my_typed_void *data_out)
      {
          //...
          data_out->data = malloc(sizeof int);
          *((int *)(data_out->data)) = 33;
          data_out->datatype = MY_INTEGER;
          //...
      }
      
      int main (int argc, char *argv[])
      {
          struct my_typed_void current;
      
          if(get_dynamic_from_global_queue(&current) == ERROR_NONE)
          {
              switch(current.datatype)
              {
              //...
              case MY_INTEGER:
                  printf("%d", *((int *) current.data));
                  break;
              //...
              }
              free(current.data);
          }
      
          return 0;
      }
      

      【讨论】:

        【解决方案4】:

        我不能 100% 确定您在寻找什么,但可能是这样的:

        enum pointer_type{INTEGER, STRUCTURE_1, STRUCTURE_2, INVALID};
        
        
        int assign_value_to_pointer(void ** ptr)
        {
            uint8_t cond = getCondition();
            switch(cond)
            {
                case 1:
                    *ptr = (void*) 10;
                    return INTEGER;
                case 2:
                    *ptr = (void*) someStructOfType1;
                    return STRUCTURE_1;
                case 3:
                    *ptr = (void*) someStructOfType2;
                    return STRUCTURE_2;
                default:
                    *ptr = NULL;
                    return INVALID;
            };
        }
        
        void main(void)
        {
            void * ptr = NULL;
        
        
            int ptrType = assign_value_to_pointer(&ptr);
        
            switch(ptrType)
            {
                case INTEGER:
                    assert(ptr == (void*)10);
                    break;
                case STRUCTURE_1:
                    assert( ((structType1*) ptr)->thing == something);
                    break;
                case STRUCTURE_2:
                    assert( ((structType2*) ptr)->something == something);
                    break;
                default:
                assert(ptr == NULL);
            }
        }
        

        【讨论】:

          【解决方案5】:
          void assign_value_to_pointer(int** pointer) {
              **pointer = 20;      
          }
          
          void main() {
            void* pointer = NULL;
            pointer=malloc(sizeof(int));
            *(int *)pointer=10;
            assign_value_to_pointer(&pointer);
          }
          

          【讨论】:

          • 这毫无意义。将pointer 的地址传递给一个永远不会改变pointer 值的函数是......毫无意义。
          • 不,该函数不会改变指针的值。该函数改变指向内存位置的值。你会得到相同的结果使用:void assign_value_to_pointer(int* pointer) { *pointer = 20; } 和类似的调用:assign_value_to_pointer(pointer);
          【解决方案6】:

          你离成功不远了,你只是错过了一个取消引用参数的星号:

          void assign_value_to_pointer(void* pointer) {
            if (cond1) {
              *pointer = 10;       // note the asterisk
            ...
          }
          
          void main() {
            void* pointer = NULL;
          
            assign_value_to_pointer(&pointer);
          
          }
          

          在 C 语言中,函数的参数总是按值传递。如果希望函数修改参数,则必须传递要修改的变量的地址。在 main() 中,您正在这样做 - 正确。被调用函数可以写入其参数指向的位置,从而修改原始变量;为此,您必须取消对参数的引用。

          编译器应该对赋值感到愤怒,因为它不知道要写入多少字节(我保持简单)。所以,你必须说指针指向什么样的对象,像这样:

          *(int *) pointer = 10;
          

          您选择的类型转换取决于您,这取决于上下文。

          此时...为什么不以不同的方式声明函数:

          void assign_value_to_pointer(int* pointer) {
            if (cond1) {
              *pointer = 10;       // note the asterisk
          }
          

          现在不再需要类型转换,因为编译器知道对象的种类(我再次保持简单 - void 非常特别)。

          ******* 在 cmets 后编辑

          好吧,我不是 C 语言的专家,此外,我想保持低调以更好地帮助 OP。

          对于简单的情况,正确的声明是幼稚的。类型转换可以更加灵活,因为函数可以根据上下文有多个赋值语句可供选择。最后,如果函数传递了指针和其他一些参数,一切皆有可能,包括使用 memcpy()。但是最后一个解决方案打开了一个世界......

          回复 Lance(以下评论):嗯,我认为如果您不知道要写入的对象的类型,没有办法进行分配。这对我来说似乎很矛盾......

          【讨论】:

          • 想知道是否可以演示如何处理您不知道它是什么类型的情况,如*pointer = myarray.items[index] 其中myarray 是@987654321 @。 Compiler gets angry 那里,我不知道如何解决它,我在哪里 *value = collection.items[collection.len--]。我得到incomplete type 'void' is not assignable
          • @LancePollard 你是对的......看我更新的答案。
          【解决方案7】:

          要么返回指针,要么将指针传递给指针(然后函数将更改指针):

          void* f1(void* p)
          {
            p = whatever(p, conditions);
            return p;
          }
          
          void f2(void** p)
          {
            *p = whatever(*p, conditions);
          }
          

          【讨论】:

          • 想知道您是否可以再解释一下,不确定whatever 做了什么,因为您将指针传递给它。
          • @LancePollard whatever 是您想要的任何逻辑。我只是将其作为可能输入的函数进行了拼写。东西进来,东西出去。它不必是真正的 C 函数。如果你愿意,这是一个比喻。
          猜你喜欢
          • 1970-01-01
          • 2019-10-19
          • 2018-07-31
          • 2013-10-30
          • 2013-11-14
          • 2016-12-03
          • 2014-06-03
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多