何时使用空指针?

51

我理解使用void指针实现malloc的用法。

void* malloc  ( size_t size );

有没有人能提出其他原因或者提供一些实际应用场景。

谢谢

14个回答

38

使用 void* 的一个好的情况是当你想要实现任何通用的ADT时,如果你不知道它将保留和处理哪种数据类型。例如,像下面这样的链表:

typedef struct node_t node;
struct
{
    void* data;
    node* prev, next;
} node_t;

typedef struct list_t list;
typedef void* (func)(void*) cpy_func;
typedef void (func)(void*) del_func;
struct
{
   node* head, tail, curr;
   cpy_func copy;
   del_func delete;
} list_t;

initializeLinkedList(cpy_func cpy, del_func del);
//here you keep going defining an API

例如,在这里,您将向其他函数传递初始化函数指针,这些函数将能够将数据类型复制到列表中并在之后释放它。因此,使用void*可以使您的列表更通用。

我认为void*仅因向后兼容而保留在C++中,因为在C++中,您有更安全和更复杂的方法来实现相同的结果,例如模板、函数对象等,并且您不需要在编写C++时使用malloc。

关于C ++,我没有任何特定的有用示例。


2
谢谢 Artem。就是这个问题。对于 C 语言,同样的事情也可以使用 char* 实现。我无法想到一个不能使用 char* 的场景。(适用于 malloc)。 - Mac13
1
C++的惯用语是使用模板,标准库提供了许多有用的模板(包括链表)。 - David Thornley
5
使用 void* 代替 char* 提供了一定的类型安全性。这样告诉编译器:“我不应该被允许引用这些指针中的任何一个。” - Phil Miller
在我看来,C++实现相同功能的惯用方法是使用“继承”和“模板”。@David - Rohan Prabhu

15

如果你正在与C代码进行接口,并且需要传递一个C++对象,但是C库只接受通用指针,则当您检索指针时,需要重新将其转换为正确的类型。

空指针可能不应该经常使用,但是它们可以在尝试使用处理任意指针并且不关心该内存表示的数据的库函数时有所帮助。


我认为你是正确的,C++调用C并使用void*的情况现在是最常见的。 - sparc_spread
还有不要忘记 MFC 调用,例如当您需要使用所需类型的对象启动线程时,可以这样做:AfxBeginThread(MyThreadProc, pNewObject); - ZoomIn

12

void指针应该在数据块的内容不重要时使用。例如,复制数据时,内存区域的内容被复制,但数据的格式并不重要。

对于那些操作内存块而不需要理解其内容的函数,使用void指针可以向用户明确设计意图,让他们知道函数不关心任何数据格式。通常,函数使用char *处理内存块,即使函数实际上与数据内容无关。


10

在C++中,我发现void*指针最吸引人的用途是提供给代码一个选项,在已经使用的对象上存储任意的"用户数据"。

假设你编写了一个表示Car的类,用于与对Car对象进行有用操作的软件(交通模拟、租车清单等)一起使用。现在假设你发现自己处于这样一种情况:应用程序想要跟踪汽车后备箱中的任意内容。后备箱中存储的具体细节对于Car类并不重要,可以是任何东西--这真的取决于使用Car类的应用程序的目的。这时就需要用到void*指针。

class Car
{
    public:

        // Existing methods of your Car class

        void setContentsOfTrunk(void* contentsOfTrunk);
        void* contentsOfTrunk() const;

    private:

        void* m_contentsOfTrunk;
}

现在,使用您的Car类的任何应用程序都可以选择将任意数据对象附加到现有的Car对象上,以便可以从具有Car对象的任何代码中获取它。行李箱的内容随着Car对象一起“旅行”,无论它在您的代码中走到哪里。

在这种情况下,有两种替代方案可以不使用void*。

第一种方法是根据行李箱内容对象的类型模板化您的类:

template <class TrunkContentsType>
class Car
{
    public:

        // Existing methods of your Car class

        void setContentsOfTrunk(TrunkContentsType contentsOfTrunk);
        TrunkContentsType contentsOfTrunk() const;

    private:

        TrunkContentsType m_contentsOfTrunk;
}

这似乎是一种不必要的侵入式做法。对于应用程序来说,行李箱内的内容类型只是重要的。使用 Car 对象的算法和数据结构并不关心行李箱里放了什么东西。通过给类加模板,你强制使用该类的应用程序选择行李箱内容的类型,但在许多情况下,应用程序也并不关心行李箱里的内容。

第二个选择是从 Car 派生一个新类,为行李箱内容添加一个数据成员和访问器:

class Car
{
    public:

        // Existing methods of your Car class
        // No methods having anything to do with trunk contents.

    private:

        // No data member representing trunk contents.
}

class CarWithTrunkContents
{
    public:

        // Existing methods of your Car class

        void setContentsOfTrunk(TrunkContentsType contentsOfTrunk);
        TrunkContentsType contentsOfTrunk() const;

    private:

        TrunkContentsType m_contentsOfTrunk;
}
新的CarWithTrunkContents类是一个应用程序特定的类,它添加了一个数据成员来存储车辆后备箱内容所需的类型。这似乎过于繁重。为什么必须派生一个全新的类来添加一个不会影响类行为的附加数据呢?如果使用Car类的应用程序通常需要存储后备箱内容,为什么要强制每个应用程序为其特定类型的后备箱内容派生一个新类呢?
最后,虽然我编造的后备箱内容示例可能栩栩如生地描述了任意后备箱内容与Car对象一起旅行,但在实践中,您很可能提供更通用的机制来将应用程序特定数据附加到Car上:
class Car
{
    public:

        // Existing methods of your Car class

        void setUserData(void* userData);
        void* userData() const;

    private:

        void* m_userData;
}

这样,一个应用程序可以附加代表树干内容的对象,或者代表驾驶执照和注册的对象,或者代表租赁协议的对象,或者任何其他对象。我见过这种void*指针被称为“userData”(即被类的用户理解),“blindData”(即类对其所携带的对象的内容是盲目的)或“applicationData”(即应用程序定义的类型和目的的数据)。


4
一个了解void *和其他C语言相关主题的好方法是观看斯坦福大学“编程范式”在iTunes-U上的前半部分。它真正精彩地解释了void *(C泛型)和指针的使用!它确实帮助我更好地学习C语言......
如果您想在函数中接受不同类型的数据,则使用void *的最大用途之一是使用void *。 (这里有一个示例:http://142.132.30.225/programming/node87.html
以下是您可以使用它们的进一步示例:
  int i;
  char c;
  void *the_data;

  i = 6;
  c = 'a';

  the_data = &i;
  printf("the_data points to the integer value %d\n", *(int*) the_data);

  the_data = &c;
  printf("the_data now points to the character %c\n", *(char*) the_data);

如果您不想观看斯坦福大学的免费课程,我建议您搜索空指针并阅读那里的所有材料。

2
是的,我们明白了,但为什么不直接创建具有适当指针类型的变量呢?为什么要费力去追踪存储在void中的数据类型?为什么不使用int或char*呢? - Kekoa
当您想要创建一个通用函数(接受/返回不同类型的函数)时,void * 是最有用的。您可以创建一个函数,它可以接受 int、double 或 long,而无需为每个类型都创建一个函数! - micmoo
链接已损坏。 - ZoomIn

4

void *实际上是C语言的一种机制,它允许C做一些在其他情况下无法合理完成的事情。

char *不能被用于任何可移植的事情,因为不同的平台可以生成不同类型的指针。一个char *不一定会被处理成和void *相同(甚至不是相同的大小)的指针。

所以当数据类型在C中不确定(或者是多态的或者其他动态的情况),那么void *允许您生成正确的底层指针类型 - 能正确指向任何东西的指针类型。

在C ++中,void *通常不会出现,除非涉及与遗留的C代码进行交互的一种形式或另一种形式。


在C++中,你使用什么来替代void*? - Alan Kałuża
1
选择适当的指针类型或父类指针类型。 - Mordachai

3

通常在数值代码中使用,例如C语言的根求解器函数可能如下所示:

double find_root(double x0, double (*f)(double, void*), void* params)
{
/* stuff */
y = f(x, params);
/* other stuff */
}

paramsf转换为一些它知道的结构,但find_root不知道。


3
更通用地说, void* 是回调函数和其他一些需要向函数传递参数但不需要知道参数类型的情况下很好的选择。 - Phil Miller

3

另一个使用void *实现的C语言“泛型”的例子是标准的qsort函数:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

您可以对任何类型的数组进行排序:int、long、double、char * 或某些结构体指针...

3
空指针在编写需要在多个操作系统上运行且需要相对独立于底层框架API的代码时非常有用。例如,OS X、Windows和Linux都具有窗口对象的基本概念,但它们都非常不同。因此,我有通用代码将它们作为void*传递,并具有平台特定的实现,将void*转换为本机类型(HWND等)。但是,正如其他人在这个帖子中所说的那样,除非必要,否则应该避免使用这种方法。

3
使用空指针有很大的优势。 指针变量是一个存储另一个变量地址的变量。 例如:
int a;
int *x= &a;

现在,'x'存储了整数变量的地址。

但是这个失败了:

float f;
int *x = &f;

因为整数指针变量只能存储整数变量地址。同样的方式适用于其他数据类型。

当您使用void *指针时,它可以存储任何类型变量的地址。

void *pointer = &i;
void *pointer = &f;

在检索时,必须进行解引用。

*((int*)pointer)

因此,请小心使用空指针。

这可能对您有所帮助,谢谢。


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