C 语言 generic 关键字

2024年8月28日 | 阅读 12 分钟

在本文中,我们将讨论 C 语言中的泛型关键字,包括其语法、功能和示例。

C11 标准引入了一个实用的特性,使得类型泛型编程成为可能:_Generic 关键字。它通过根据宏调用的参数类型进行条件编译,使得构建易于处理多种数据类型的泛型代码变得容易。这对于模拟 C 语言中的函数重载非常有用,因为根据参数的类型可能需要不同的行为。

语法

C 语言中 _Generic 的语法是

下面是对每个组件的详细描述:

表达式 (Expression): 表达式代表要执行代码的类型控制。确定此表达式的类型,并将其与关联的给定类型进行比较。

type1, type2, ..., default: 这些是类型关联类型标签。每个类型标签后面都跟着如果表达式的类型与该特定类型匹配时将执行的代码,以及一个冒号 (:)。如果提供的类型都不匹配表达式的类型,则可选的 default 标签指示要执行的代码。

code1, code2, ..., codeN: 每个类型标签的代码块code1, code2,..., codeN。如果关联的类型标签与表达式的类型匹配,则将执行每个代码块中的语句。

_Generic 构造通常在您希望在编译时根据表达式的类型选择不同代码路径时使用。虽然它与C 宏没有直接关系,但可以通过在宏中使用它来实现类型泛型行为。

如何在 C 语言中使用 _Generic

_Generic 关键字可以在我们代码中任何需要泛型代码的地方使用。C 语言不像某些其他编程语言那样直接支持泛型。但是,通过采用特定策略,您可以开发类型泛型编程,并生成可与各种数据类型一起使用的函数。以下是一些在 C 语言中使用泛型的常见方法:

函数指针 (Function Pointers): 函数指针可用于实现类型泛型行为。您可以根据数据的类型调用多个函数,方法是将函数指针传递给泛型函数。下面是一个示例:

程序

输出

42
3.14

说明

包含头文件 (Include Header Files): 代码包含了标准输入输出库 (stdio.h),以便使用 printf()打印函数

定义函数指针类型 (Define Function Pointer Type): 代码声明了一个名为 PrintFunction 的新函数指针类型。当一个函数接受 void* 参数返回 void 时,其地址会存储在此函数指针类型中。我们可以创建一个可以接受各种数据类型的通用函数。

泛型函数定义 (Generic Function Definition): 泛型函数 printValue 可以接受两个参数

Void* data: 这是一个指向我们要打印的数据的指针,类型为 void*。使用 void* 类型,我们可以在不知道其类型的情况下获取指向任何数据类型的指针

PrintFunction print: 称为 PrintFunction函数指针变量 print 指示用于打印数据的精确函数print 函数指针属于我们之前定义的 PrintFunction 类型。

特定打印函数 (Specific print functions): 代码指定了两个不同的函数,printprintFloat,分别用于打印整数浮点数。这些函数接受一个 void* 参数,但是为了正确访问和打印值,我们需要将 void* 指针转换为相关数据类型(例如,对 print 使用 int*,对 printFloat 使用 float*)。

主函数 (Main Function):主函数中声明了两个变量。

float value: 一个浮点数变量,其初始值3.14

intValue: 一个整数变量,初始化为 42

调用泛型函数 (Calling the Generic method): 接下来,我们使用两种不同的数据类型调用 printValue 函数两次。

  • 第一次调用时,传递了 intValue 的地址和 print 函数指针。printValue 函数调用 print 函数,该函数输出整数 42
  • 第二次调用时,传递了 floatValue 的地址和 printFloat 函数指针。当 printValue 函数调用 printFloat 函数时,将打印浮点数值 3.14

复杂度分析

时间复杂度

提供的代码具有常数(O(1)) 时间复杂度。在main 函数中调用了两次 printValue 函数,该函数会调用 printprintFloatprintValue 函数会持续运行,因为它仅使用函数指针来调用正确的函数。两次函数调用需要相同的时间,与输入(数据)的大小无关。

空间复杂度

提供的代码具有常数(O(1)) 空间复杂度。所使用的内存量与输入的大小(intValuefloatValue)无关。主要原因是例程内部没有分配额外的内存来存储数据,而是将数据作为指针 (void*) 传递给函数。无论输入大小如何,函数调用和指针始终需要相同数量的内存。

宏中的泛型

是 C 语言中的预处理器指令,它们允许您直接在代码中定义重要的功能。宏函数允许程序员替换,因为它仅使用函数指针来调用正确的函数,并用在编译时展开的预定义表达式或代码行替换。根据宏输入生成类型特定的代码提供了一种定义泛型行为的方法。

语法

函数宏具有以下语法:

MACRO_NAME: MACRO_NAME 代表您用来调用的名称。

Arguments:接受的参数列表,用逗号分隔。这些类似于函数参数。

Generic macros for Arithmetic Operations: 宏在被调用时展开的代码表达式replacement_expression。它还可以包含其他代码和宏参数。

程序

输出

Result of integer addition: 15
Result of float addition: 5.85
Result of double addition: 2.195

说明

  • 在此示例中,我们定义了一个名为 ADD函数宏。要相加的值由该宏接受的两个参数xy 表示。
  • 我们在 ADD 宏中使用_Generic 关键字,根据 (x) + (y) 表达式的数据类型选择正确的函数。使用表达式 (x) + (y) 来确定泛型选择的类型。
  • _Generic 关键字将表达式 (x) + (y) 的类型转换为 add_int, add_floatadd_double 类型特定的函数。
  • 然后,我们提供了三个分别处理整数、浮点数和双精度浮点数加法的函数:add_int, add_floatadd_double
  • 我们在 main 函数中声明了变量 intResult, floatResultdoubleResult 来保存加法结果。
  • ADD 宏执行各种数据类型加法,然后将结果保存在相应的变量中。
  • 然后,我们打印每种数据类型的结果

复杂度分析

提供的 C 代码通过函数宏实现了多种数据类型的加法,其时间和空间复杂度如下:

时间复杂度

代码具有O(1) 时间复杂度。在 main 函数中使用 ADD 宏执行以下三个加法运算:

intResult = ADD(a, b);

floatResult = ADD(x, y);

doubleResult = ADD(m, n);

根据类型,每次加法运算会使用 add_int, add_floatadd_double 中的一个。每次加法运算所需的时间是固定的,与 a, b, x, y, mn 的值无关。无论操作次数或输入的数量如何,时间复杂度都保持固定

空间复杂度

代码的空间复杂度也为常数(O(1))。在应用程序执行期间使用的内存量始终保持不变,与输入数据的数量无关。它为函数参数变量intResult, floatResult, doubleResult, a, b, x, y, m, n)分配了内存,这些内存与输入的大小无关。递归函数调用和可能增加空间复杂度的动态分配数据结构不存在。

使用 _Generic (C11 及更高版本)

如前文所述,_Generic 关键字使您能够根据表达式的数据类型有条件地构建代码。这种能力使得类型泛型编程成为可能。

程序

输出

Square of 5: 25
Square of 3.14: 9.86
Square of 2.718: 7.388

说明

在此代码中,使用_Generic 关键字square 宏根据参数 x 的类型选择正确的表达式。该宏可以轻松计算整数、浮点数双精度浮点数的平方。当宏与整数参数一起使用时,其表达式为 (x) * (x),从而计算整数的平方。同样,当使用浮点数双精度浮点数参数调用时,它会使用适当的公式计算这些数据类型的平方。由于 _Generic 特性的类型泛型行为,我们可以通过一个宏处理多种数据类型。

  1. 包含头文件 (Include Header File): 代码包含了标准输入输出库 (stdio.h),以便使用 printf 等函数。
  2. 定义函数宏 (Define Function Macro): 代码创建了一个与平方相关的函数宏。我们要对其进行平方的数字由输入 x 表示,这是宏接受的唯一参数。
  3. 使用 _Generic (Making use of _Generic): square 宏利用了_Generic 关键字。基于表达式(在此情况下为 x)类型进行条件编译C11 标准提供的一个强大功能。
  4. 类型关联 (Type Associations): 使用冒号 (:),代码在 _Generic 关键字内为 x 表达式提供了各种类型关联:
    • int 类型对应的语句 (x) * (x) 表示计算整数的平方。
    • float 类型对应的语句 (x) * (x) 表示计算浮点数的平方。
    • double 类型对应的计算双精度浮点数平方的表达式为 (x) * (x)
  5. 主函数 (Main Function):主函数中,定义并初始化了 3 个变量:整数“intValue” 的值为 5,浮点数“floatValue” 的值为 3.14,双精度浮点数“doubleValue” 的值为 718
  6. 调用宏 (Calling the Macro): 使用square 宏计算每个变量的平方,然后使用 printf() 函数打印结果。
  7. 输出 (Output): 对于整数、浮点数双精度浮点数,代码以指定格式输出每个变量的平方。

复杂度分析

时间复杂度

代码具有O(1) 时间复杂度。在main 函数中调用了三次 printf() 函数,这与输入数据大小无关且是常数。诸如乘法之类的简单数学运算也包含在square 宏中,并且无论输入如何,它都需要固定的时间。代码中的所有操作都在固定时间内执行,与提供的输入数据大小无关。

空间复杂度

代码的空间复杂度也为常数(O(1))。程序执行时使用的内存量始终相同。在 main 函数的内存中使用了具有固定内存分配且独立于输入数据大小的变量,例如intValue, floatValuedoubleValueprintf() 函数也使用固定量的内存,与输入数据大小无关。

square 宏使用的内存很少且恒定,并且没有分配额外的内存。它直接执行数学运算,而不会增加大量内存开销。

联合体 (Unions)

在 C 语言中,联合体允许在同一内存地址存储不同的数据。联合体可以在特定情况下提供一种处理多种数据类型的机制,尽管它们并不完全是泛型。

程序

输出

Integer: 42
Float: 3.14
Double: 2.718

说明

包含头文件 (Include Header File): 代码包含了标准输入输出库 (stdio.h),以便使用 printf() 函数进行打印。

数据种类的枚举 (Enumeration for Data kinds): 定义了一个名为 DataType枚举来表示不同的数据类型。它指定了三个常量,INT, FLOATDOUBLE,分别代表双精度浮点数、浮点数整数数据类型

使用联合体的泛型数据容器 (Generic Data Holder using a Union): GenericData定义的结构体的名称。它作为一个通用的数据容器,用于存储不同种类的数据。它包含两个字段:

  • DataType type: 一个枚举字段,用于指定结构体可以容纳的数据类型。
  • Union: 一个基于 type 字段的联合体,可以存储不同种类的数据。它可以存储浮点数 (float)整数 (int)双精度浮点数 (double)

根据数据类型打印值的函数 (Print Values Based on Data Type function): 可以创建一个接受 GenericData 结构体作为输入的 printValue 函数。使用switch 语句,该函数会检查 GenericData 结构体type 字段,并根据数据类型打印存储在相应数据字段中的值。

主函数 (Main Function): 在主函数中,声明并使用特定的值和数据类型初始化了三个 GenericData 变量 (data1, data2, 和 data3)

调用函数 (Calling the Function): 调用了三次 printValue 函数,将每个 GenericData 变量作为参数传递。该函数根据各自的数据类型打印每个GenericData 结构体中的值。

输出 (Output): 算法根据每种数据类型输出 data1, data2data3 的值。

复杂度分析

时间复杂度

代码具有O(1) 时间复杂度。代码调用printValue 函数三次,这与输入数据大小无关且是常数。printValue 方法内部基于DataType 枚举switch 语句也以常数时间运行。因此,每次调用 printValueswitch 语句所需的时间相同,并且时间复杂度与输入大小无关。

空间复杂度

代码的空间复杂度也是常数(O(1))。程序执行期间使用的内存量始终相同。在main 函数中声明并初始化了三个 GenericData 变量 (data1, data2, 和 data3)。这些变量的内存需求是恒定的,并且独立于输入数据的数量。

printValue 方法使用的内存非常少且恒定。该函数仅为其GenericData 参数使用内存,其大小不受输入大小的影响。

此外,printValue 函数的 switch 语句根据数据类型值选择适当的 case,而不是分配更多RAM

结论

编写泛型 C 代码需要富有创意且谨慎地分析最佳方法,具体取决于独特用例。选择将基于代码的复杂性安全性要求的类型以及可维护性。提供的每种解决方案都有优点和缺点。在某些情况下,可能足以应对简单的任务。但是,对于更复杂的情况和更好的类型安全性,函数指针和 _Generic 的使用是更可取的。

在 C 语言中创建泛型行为时,开发人员应始终致力于编写清晰、可维护且无错误的 [代码](https://www.geeksforgeeks.org/c-programming-language/)。要编写可靠且有效的类型泛型代码,理解每种方法的缺点和潜在危险至关重要。