在C语言中,如何检查一个变量是否属于某种特定类型(比较两种类型)?

109

在 C 语言(不是 C++/C#)中,如何检查一个变量是否为某种类型?

例如,像这样的代码:

double doubleVar;
if( typeof(doubleVar) == double ) {
    printf("doubleVar is of type double!");
}
更一般地说:我如何比较两种类型,使得compare(double1,double2)返回true,但是compare(int,double)将返回false。另外,我还想比较不同结构的组成部分。
基本上,我有一个函数,它操作"结构a"和"结构b"类型的变量。我想对"结构a"变量做一件事情,对"结构b"变量做另一件事情。由于C语言不支持重载,而且void指针失去了类型信息,因此需要检查类型。顺便问一下,如果不能比较类型,那么有一个typeof运算符会有什么意义呢?
对于我来说,sizeof方法似乎是一个实用的解决方法。感谢您的帮助。我仍然觉得有点奇怪,因为类型在编译时就已知,但是如果我想象一下机器中的处理过程,我可以看到为什么信息不是以类型为单位存储的,而是以字节大小为单位存储的。除了地址之外,大小是唯一真正相关的东西。

你不能将它们都转换为double类型(并添加0.00)吗?不确定在C语言中是否可能,只是一个建议。 - Kevin
1
查看源代码,它清楚地说明了 doubleVar 是一个 double 型变量。没必要(也不可能)在运行时进行检查。 - Habalusa
针对Edit #1的回复:你是否考虑过使用函数指针(类似于vtable)来解决你的问题? - Michael Foukarakis
如果你喜欢sizeof方法,请阅读关于gcc的tgmath实现的这篇文章 - quinmars
@Michael Foukarakis,你能提供一个例子吗? - con-f-use
13个回答

75

目前,在C11中使用_Generic泛型选择可以获取变量的类型。它在编译时工作。

语法有点像switch。以下是一个示例(来自这个答案):

    #define typename(x) _Generic((x),                                                 \
            _Bool: "_Bool",                  unsigned char: "unsigned char",          \
             char: "char",                     signed char: "signed char",            \
        short int: "short int",         unsigned short int: "unsigned short int",     \
              int: "int",                     unsigned int: "unsigned int",           \
         long int: "long int",           unsigned long int: "unsigned long int",      \
    long long int: "long long int", unsigned long long int: "unsigned long long int", \
            float: "float",                         double: "double",                 \
      long double: "long double",                   char *: "pointer to char",        \
           void *: "pointer to void",                int *: "pointer to int",         \
          default: "other")

要在编译时进行手动类型检查,您可以定义一个包含您期望的所有类型的enum,类似于此:

为了实际使用它进行编译时手动类型检查,您可以定义一个enum,其中包含您所期望的所有类型,例如:

    enum t_typename {
        TYPENAME_BOOL,
        TYPENAME_UNSIGNED_CHAR,
        TYPENAME_CHAR,
        TYPENAME_SIGNED_CHAR,
        TYPENAME_SHORT_INT,
        TYPENAME_UNSIGNED_CHORT_INT,
        TYPENAME_INT,
        /* ... */
        TYPENAME_POINTER_TO_INT,
        TYPENAME_OTHER
    };

然后使用 _Generic 将类型与此 enum 匹配:

    #define typename(x) _Generic((x),                                                       \
            _Bool: TYPENAME_BOOL,           unsigned char: TYPENAME_UNSIGNED_CHAR,          \
             char: TYPENAME_CHAR,             signed char: TYPENAME_SIGNED_CHAR,            \
        short int: TYPENAME_SHORT_INT, unsigned short int: TYPENAME_UNSIGNED_SHORT_INT,     \
              int: TYPENAME_INT,                     \
        /* ... */                                    \
            int *: TYPENAME_POINTER_TO_INT,          \
          default: TYPENAME_OTHER)

27

C语言不支持这种类型反射机制。在C中,你所要求的是不可能实现的(至少没有使用特定于编译器的扩展;但在C++中是可能实现的)。

通常情况下,在C中,你需要知道变量的类型。由于每个函数都有其参数的具体类型(除了变长参数,我想),因此你不需要在函数体中进行检查。我能想到的唯一剩下的情况是在宏定义体中,但是,C宏并不是真正强大的工具。

此外,请注意,C语言不会将任何类型信息保留到运行时。这意味着,即使假设存在类型比较扩展,它也只能在编译时已知类型的情况下正常工作(例如,它不能用于测试两个void *是否指向相同类型的数据)。

至于typeof:首先,typeof是GCC扩展,不是C语言的标准部分。它通常用于编写只评估其参数一次的宏,例如(来自GCC手册):

 #define max(a,b) \
   ({ typeof (a) _a = (a); \
      typeof (b) _b = (b); \
     _a > _b ? _a : _b; })

typeof关键字允许宏定义一个本地临时变量来保存其参数的值,使得参数只被评估一次。

简而言之,C不支持重载;你只需要创建一个func_a(struct a *)func_b(struct b *),并调用正确的函数。或者,你可以创建自己的内省系统:

struct my_header {
  int type;
};

#define TYPE_A 0
#define TYPE_B 1

struct a {
  struct my_header header;
  /* ... */
};

struct b {
  struct my_header header;
  /* ... */
};

void func_a(struct a *p);
void func_b(struct b *p);

void func_switch(struct my_header *head);
#define func(p) func_switch( &(p)->header )

void func_switch(struct my_header *head) {
  switch (head->type) {
    case TYPE_A: func_a((struct a *)head); break;
    case TYPE_B: func_b((struct b *)head); break;
    default: assert( ("UNREACHABLE", 0) );
  }
}

当创建这些对象时,您必须记得正确初始化标头。


2
有没有什么变通方法或者使用宏之类的聪明技巧? - con-f-use
2
@con-f-use,你为什么需要这个? - bdonlan
5
@con-f-use,这是一个很好的解决方法,直到你添加一个成员,大小变得相等,突然它总是采用struct a分支,即使它是struct b。 :) - bdonlan
@bdonlan,“首先,typeof是GCC的扩展”...等等,GCC扩展有什么问题吗?它不好用吗? - Pacerier
@Pacerier:如果你的目标是要用另一个编译器构建你的代码,那么它们的兼容性就不太好。 - Oliver Charlesworth
显示剩余6条评论

13

像其他人已经说过的那样,C语言不支持此功能。但是,您可以使用 sizeof() 函数检查变量的大小。这可能会帮助您确定两个变量是否可以存储相同类型的数据。

在执行此操作之前,请阅读下面的评论


5
另外,如果您坚持执行此操作,请添加静态断言以确保大小不会意外相等:struct STATIC_ASSERT_size_not_equal_s { char STATIC_ASSERT_size_not_equal[sizeof(a) == sizeof(b) ? -1 : 1]; }; - bdonlan
在我的情况下,比较结构体时它们都具有相同的成员,除了其中一个结构体有两个额外的双精度成员。因此,如果我执行"if(sizeof(a)>sizeof(b))",不考虑架构或其他因素,应该是安全的。无论如何,谢谢。 - con-f-use
5
你可以使用 sizeof() 函数来检查变量的大小,但是 sizeof (int) == sizeof (float) ,它们却具有完全不同的存储格式。 - phoxis

9
Gnu GCC具有用于比较类型的内置函数__builtin_types_compatible_phttps://gcc.gnu.org/onlinedocs/gcc-3.4.5/gcc/Other-Builtins.html。 此内置函数返回1,如果类型type1和type2(它们是类型而不是表达式)的未限定版本是兼容的,则返回1,否则返回0。此内置函数的结果可用于整数常量表达式。
该内置函数忽略顶层限定符(例如const、volatile),例如int等同于const int。
您的示例中使用:
double doubleVar;
if(__builtin_types_compatible_p(typeof(doubleVar), double)) {
    printf("doubleVar is of type double!");
}

7

正如其他人所提到的,您无法在运行时提取变量的类型。但是,您可以构建自己的“对象”并将类型存储在其中。然后,您就可以在运行时检查它:

typedef struct {
   int  type;     // or this could be an enumeration
   union {
      double d;
      int i;
   } u;
} CheesyObject;

然后在代码中根据需要设置类型:
CheesyObject o;
o.type = 1;  // or better as some define, enum value...
o.u.d = 3.14159;

7
作为另一个回答所提到的,你现在可以使用C11中的_Generic来实现这个功能。
例如,下面是一个宏,用于检查某个输入是否与另一种类型兼容:
#include <stdbool.h>
#define isCompatible(x, type) _Generic(x, type: true, default: false)

您可以像下面这样使用宏:
double doubleVar;
if (isCompatible(doubleVar, double)) {
    printf("doubleVar is of type double!\n");  // prints
}

int intVar;
if (isCompatible(intVar, double)) {
    printf("intVar is compatible with double too!\n");  // doesn't print
}

这也可以用于其他类型,包括结构体。例如:

struct A {
    int x;
    int y;
};

struct B {
    double a;
    double b;
};

int main(void)
{    
    struct A AVar = {4, 2};
    struct B BVar = {4.2, 5.6};

    if (isCompatible(AVar, struct A)) {
        printf("Works on user-defined types!\n");  // prints
    }

    if (isCompatible(BVar, struct A)) {
        printf("And can differentiate between them too!\n");  // doesn't print
    }

    return 0;
}

还有关于typedef的内容。

typedef char* string;

string greeting = "Hello world!";
if (isCompatible(greeting, string)) {
    printf("Can check typedefs.\n");
}

然而,它并不总是能提供您期望的答案。例如,它无法区分数组和指针。

int intArray[] = {4, -9, 42, 3};

if (isCompatible(intArray, int*)) {
    printf("Treats arrays like pointers.\n");
}

// The code below doesn't print, even though you'd think it would
if (isCompatible(intArray, int[4])) {
    printf("But at least this works.\n");
}

答案来自这里:http://www.robertgamble.net/2012/01/c11-generic-selections.html

(本文介绍了C++11中的泛型选择,旨在提高代码的可读性和可维护性。泛型选择是使用模板和类型推断进行更好的重载和派发的一种方法。它允许开发人员根据类型隐式地选择特定的操作或实现方式,从而减少代码冗余并提高代码的清晰度。)

6

来自linux/typecheck.h

/*
 * Check at compile time that something is of a particular type.
 * Always evaluates to 1 so you may use it easily in comparisons.
 */
#define typecheck(type,x) \
({  type __dummy; \
    typeof(x) __dummy2; \
    (void)(&__dummy == &__dummy2); \
    1; \
})

这里有关于代码中使用了哪些来自标准和GNU扩展的语句的解释。

虽然问题不是关于类型不匹配导致失败,但是还是在此保留。


3
这是非常愚蠢的,但如果你使用以下代码:

fprintf("%x", variable)

如果您在编译时使用-Wall标志,那么gcc将会提示一个警告,表明它希望参数为“unsigned int”,而实际参数类型为“____”。 (如果没有出现此警告,则变量的类型为“unsigned int”)。祝好运! 编辑:如下方所述,这仅适用于编译时。 当尝试解决指针问题时非常有用,但如果需要在运行时使用则不太有用。

是的,但这在C语言中是无法检查的,只能在编译时而非运行时工作。 - con-f-use
真的。我在调试一些运行时指针数学操作时遇到了这个问题,因此在编译时确定问题解决了我的问题。 - Daniel Peirano

1

C语言是一种静态类型的语言。你不能声明一个操作类型A或类型B的函数,也不能声明一个持有类型A或类型B的变量。每个变量都有明确声明且不可更改的类型,你应该利用这个知识。

当你想知道void *指向的内存表示是浮点数还是整数时,你必须将这些信息存储在其他地方。这种语言特意设计成不关心char *指向的是以int还是char存储的内容。


1

从C2x开始,typeof现在是语言标准的一部分。这允许创建一个宏来比较两个值的类型:

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#define sametypeof(A,B) _Generic(A, typeof(B): true, default: false)

int main() {
  if (sametypeof(1, 2)) {
    printf("1 and 2 have the same type.\n");
  } else {
    printf("1 and 2 don't have the same type.\n");
  }
}

这段代码使用最新的GCC 13实验版进行编译,使用-std=c2x标志。

如果你想比较两种类型,可以使用以下解决方法:

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#define sametype(A,B) _Generic(*((A*)0), B: true, default: false)

int main() {
  if (sametype(void*, nullptr_t)) {
    printf("void* and nullptr_t are the same type.\n");
  } else {
    printf("void* and nullptr_t are not the same type.\n");
  }
}

尽管在运行时 *((A*)0) 不是有效的代码,但编译器仍能够推断它的类型为 A,因此它可以在 _Generic 中使用,因为这段代码本身不会运行并将被丢弃。(据我所记,在我使用的每个符合 C11 标准的编译器中,包括 Clang 和 Tiny C 编译器,这个技巧都可以奏效。)
(你也不能只写成 (A)0,因为 0 不能被转换为结构体。)

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