前言导读
嘿,学完C语言后对“泛型”还一脸懵逼的同学,举起你们的双手让我看看!别害羞,这就像是学游泳时呛了第一口水——90%的人都经历过。今天,就让我用最不正经的方式,给你讲清楚这个“正经”的概念。
一
什么是泛型?你的代码衣柜
想象一下你的衣柜。如果没有泛型,你的衣柜会是这样的:
// 专门放T恤的柜子
void tshirt_closet(char tshirts[100]) {
// 只能放T恤
}
// 专门放裤子的柜子
void pants_closet(int pants[50]) {
// 只能放裤子
}
// 专门放袜子的柜子
void socks_closet(float socks[200]) {
// 呃...为什么袜子是浮点数?
}
每次买新衣服都要买个新柜子!这就是没有泛型的世界——代码冗余到怀疑人生。
而泛型就像是你的万能衣柜:
// 理想中的泛型衣柜(可惜C语言原生不支持)
// GenericCloset<T> my_closet; // 这是C++/Java的写法,别急眼
在C语言里,我们要用各种“歪门邪道”来实现这个万能衣柜。准备好了吗?系好安全带,老司机要发车了!
二
void* —— C语言的“薛定谑的盒子”
void*是C语言里最著名的“我不知道里面是什么”指针。它就像你女朋友说“随便”时的心情——可以是任何东西,但也可能什么都不是。
#include <stdio.h>
#include <string.h>
// 一个能“装”任何东西的盒子
void* mystery_box;
int main() {
int my_int = 42;
float my_float = 3.14;
char my_string[] = "Hello World";
// 盒子里放个整数
mystery_box = &my_int;
printf("盒子里可能是: %d\n", *(int*)mystery_box);
// 同样的盒子,放个浮点数
mystery_box = &my_float;
printf("盒子里可能是: %.2f\n", *(float*)mystery_box);
// 放个字符串也没问题!
mystery_box = my_string;
printf("盒子里可能是: %s\n", (char*)mystery_box);
// 危险操作:你以为盒子里是整数,其实是字符串
mystery_box = my_string;
// printf("灾难发生: %d\n", *(int*)mystery_box); // 段错误警告!
return 0;
}
void*的问题在于:编译器完全放弃治疗了。它就像个甩手掌柜:“你想放什么就放什么吧,出错别来找我!”
三
宏函数 —— 代码复印机的艺术
如果void*是薛定谑的盒子,那么宏就是C语言的魔法复印机。不过这个复印机有点调皮,经常乱改你的作业。
#include <stdio.h>
// 普通函数:一次只能处理一种类型
int max_int(int a, int b) { return a > b ? a : b; }
float max_float(float a, float b) { return a > b ? a : b; }
// 写20个这样的函数后,你的手已经废了
// 宏函数:一键生成所有版本
#define MAX(type) type max_##type(type a, type b) { \
printf("调用" #type "版本\n"); \
return a > b ? a : b; \
}
// 让宏给我们打工
MAX(int) // 生成 max_int
MAX(float) // 生成 max_float
MAX(double) // 生成 max_double
// 甚至能处理自定义类型!
typedef struct { int x, y; } Point;
#define MAX_POINT(p1, p2) ((p1.x + p1.y) > (p2.x + p2.y) ? p1 : p2)
int main() {
printf("最大值: %d\n", max_int(5, 10));
printf("最大值: %.2f\n", max_float(3.14, 2.71));
Point p1 = {1, 2}, p2 = {3, 1};
Point p3 = MAX_POINT(p1, p2);
printf("更大的点: (%d, %d)\n", p3.x, p3.y);
return 0;
}
宏的坑爹之处:它只是文本替换,不做类型检查。比如这个经典翻车现场:
#define SQUARE(x) x * x
int main() {
int a = 5;
printf("5的平方: %d\n", SQUARE(a)); // 25,正确
printf("5+2的平方: %d\n", SQUARE(a+2)); // 5+2 * 5+2=17,错误!
printf("a++的平方: %d\n", SQUARE(a++)); // 灾难!a被加了两次
return 0;
}
改进方案:多打括号,保平安:
#define SAFE_SQUARE(x) ((x) * (x))
四
实战!打造泛型容器
理论说太多会睡着,来点实战!让我们用void*打造一个“什么都能装”的动态数组:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
// 泛型动态数组结构
typedef struct {
void* data; // 数据指针
size_t elem_size; // 每个元素的大小
size_t capacity; // 容量
size_t length; // 当前长度
} GenericArray;
// 创建数组
GenericArray* ga_create(size_t elem_size, size_t init_capacity) {
GenericArray* arr = malloc(sizeof(GenericArray));
arr->elem_size = elem_size;
arr->capacity = init_capacity > 0 ? init_capacity : 4;
arr->length = 0;
arr->data = malloc(elem_size * arr->capacity);
return arr;
}
// 追加元素
void ga_append(GenericArray* arr, void* element) {
if (arr->length >= arr->capacity) {
arr->capacity *= 2;
arr->data = realloc(arr->data, arr->elem_size * arr->capacity);
}
void* dest = (char*)arr->data + arr->length * arr->elem_size;
memcpy(dest, element, arr->elem_size);
arr->length++;
}
// 获取元素
void* ga_get(GenericArray* arr, size_t index) {
if (index >= arr->length) {
fprintf(stderr, "索引 %zu 超出范围 (0-%zu)\n", index, arr->length-1);
return NULL;
}
return (char*)arr->data + index * arr->elem_size;
}
// 释放数组
void ga_free(GenericArray* arr) {
free(arr->data);
free(arr);
}
// 打印数组的宏(需要知道类型)
#define PRINT_ARRAY(arr_ptr, type, format) do { \
printf("["); \
for (size_t i = 0; i < (arr_ptr)->length; i++) { \
type* elem = (type*)ga_get(arr_ptr, i); \
printf(format, *elem); \
if (i < (arr_ptr)->length - 1) printf(", "); \
} \
printf("]\n"); \
} while(0)
int main() {
printf("=== 泛型数组演示 ===\n\n");
// 1. 整数数组
printf("1. 整数数组:\n");
GenericArray* int_arr = ga_create(sizeof(int), 2);
for (int i = 1; i <= 5; i++) {
ga_append(int_arr, &i);
}
PRINT_ARRAY(int_arr, int, "%d");
// 2. 浮点数数组
printf("\n2. 浮点数数组:\n");
GenericArray* float_arr = ga_create(sizeof(float), 3);
float floats[] = {3.14, 2.71, 1.41, 0.99};
for (int i = 0; i < 4; i++) {
ga_append(float_arr, &floats[i]);
}
PRINT_ARRAY(float_arr, float, "%.2f");
// 3. 字符串数组(其实是char*数组)
printf("\n3. 字符串数组:\n");
GenericArray* str_arr = ga_create(sizeof(char*), 2);
char* names[] = {"小明", "小红", "老张", "程序员"};
for (int i = 0; i < 4; i++) {
ga_append(str_arr, &names[i]);
}
printf("[");
for (size_t i = 0; i < str_arr->length; i++) {
char** str_ptr = (char**)ga_get(str_arr, i);
printf("\"%s\"", *str_ptr);
if (i < str_arr->length - 1) printf(", ");
}
printf("]\n");
// 4. 结构体数组
printf("\n4. 学生结构体数组:\n");
typedef struct {
char name[20];
int age;
float score;
} Student;
GenericArray* student_arr = ga_create(sizeof(Student), 2);
Student s1 = {"张三", 20, 89.5};
Student s2 = {"李四", 21, 92.0};
Student s3 = {"王五", 19, 78.5};
ga_append(student_arr, &s1);
ga_append(student_arr, &s2);
ga_append(student_arr, &s3);
for (size_t i = 0; i < student_arr->length; i++) {
Student* s = (Student*)ga_get(student_arr, i);
printf(" 学生%d: 姓名=%s, 年龄=%d, 分数=%.1f\n",
(int)i+1, s->name, s->age, s->score);
}
// 清理内存
ga_free(int_arr);
ga_free(float_arr);
ga_free(str_arr);
ga_free(student_arr);
return 0;
}
五
C11的黑科技——_Generic
C11标准带来了_Generic,它是类型选择的智能开关,让泛型编程稍微体面了一点:
#include <stdio.h>
#include <math.h>
// 根据类型选择不同的打印函数
#define PRINT(x) _Generic((x), \
int: print_int, \
double: print_double, \
float: print_float, \
char*: print_string, \
default: print_unknown \
)(x)
void print_int(int x) { printf("整数: %d\n", x); }
void print_double(double x) { printf("双精度: %f\n", x); }
void print_float(float x) { printf("浮点数: %f\n", x); }
void print_string(char* x) { printf("字符串: %s\n", x); }
void print_unknown() { printf("未知类型\n"); }
// 更实用的例子:类型安全的比较
#define COMPARE(a, b) _Generic((a), \
int: compare_int, \
double: compare_double, \
char*: compare_string \
)(a, b)
int compare_int(int a, int b) {
if (a == b) return 0;
return a > b ? 1 : -1;
}
int compare_double(double a, double b) {
double diff = a - b;
if (fabs(diff) < 1e-10) return 0;
return diff > 0 ? 1 : -1;
}
int compare_string(char* a, char* b) {
return strcmp(a, b);
}
int main() {
printf("=== _Generic演示 ===\n\n");
PRINT(42); // 整数
PRINT(3.1415926); // 双精度
PRINT(2.71f); // 浮点数
PRINT("Hello"); // 字符串
printf("\n比较结果:\n");
printf("比较5和10: %d\n", COMPARE(5, 10));
printf("比较3.14和2.71: %d\n", COMPARE(3.14, 2.71));
printf("比较\"apple\"和\"banana\": %d\n", COMPARE("apple", "banana"));
return 0;
}
六
泛型排序算法——一招鲜吃遍天
真正的泛型威力在于算法复用。比如实现一个泛型快速排序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 泛型交换函数
void swap(void* a, void* b, size_t size) {
char temp[size]; // C99变长数组
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
// 泛型分区函数
int partition(void* arr, int low, int high, size_t elem_size,
int (*compare)(const void*, const void*)) {
char* array = (char*)arr;
void* pivot = malloc(elem_size);
memcpy(pivot, array + high * elem_size, elem_size);
int i = low - 1;
for (int j = low; j < high; j++) {
if (compare(array + j * elem_size, pivot) < 0) {
i++;
swap(array + i * elem_size, array + j * elem_size, elem_size);
}
}
swap(array + (i + 1) * elem_size, array + high * elem_size, elem_size);
free(pivot);
return i + 1;
}
// 泛型快速排序
void generic_quicksort(void* arr, int low, int high, size_t elem_size,
int (*compare)(const void*, const void*)) {
if (low < high) {
int pi = partition(arr, low, high, elem_size, compare);
generic_quicksort(arr, low, pi - 1, elem_size, compare);
generic_quicksort(arr, pi + 1, high, elem_size, compare);
}
}
// 比较函数
int compare_int(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int compare_double(const void* a, const void* b) {
double diff = *(double*)a - *(double*)b;
if (diff > 0) return 1;
if (diff < 0) return -1;
return 0;
}
typedef struct {
char name[20];
int score;
} Student;
int compare_student(const void* a, const void* b) {
return ((Student*)b)->score - ((Student*)a)->score; // 降序排列
}
int main() {
printf("=== 泛型快速排序演示 ===\n\n");
srand(time(NULL));
// 1. 排序整数数组
printf("1. 排序整数数组:\n");
int ints[10];
printf("排序前: ");
for (int i = 0; i < 10; i++) {
ints[i] = rand() % 100;
printf("%d ", ints[i]);
}
generic_quicksort(ints, 0, 9, sizeof(int), compare_int);
printf("\n排序后: ");
for (int i = 0; i < 10; i++) {
printf("%d ", ints[i]);
}
printf("\n\n");
// 2. 排序浮点数数组
printf("2. 排序浮点数数组:\n");
double doubles[8];
printf("排序前: ");
for (int i = 0; i < 8; i++) {
doubles[i] = (rand() % 1000) / 10.0;
printf("%.1f ", doubles[i]);
}
generic_quicksort(doubles, 0, 7, sizeof(double), compare_double);
printf("\n排序后: ");
for (int i = 0; i < 8; i++) {
printf("%.1f ", doubles[i]);
}
printf("\n\n");
// 3. 排序学生结构体数组
printf("3. 按成绩降序排列学生:\n");
Student students[5] = {
{"张三", 85}, {"李四", 92}, {"王五", 78}, {"赵六", 95}, {"钱七", 88}
};
printf("排序前:\n");
for (int i = 0; i < 5; i++) {
printf(" %s: %d分\n", students[i].name, students[i].score);
}
generic_quicksort(students, 0, 4, sizeof(Student), compare_student);
printf("\n排序后:\n");
for (int i = 0; i < 5; i++) {
printf(" %s: %d分\n", students[i].name, students[i].score);
}
return 0;
}
七
泛型的黑暗面与生存指南
黑暗面:
▶ 类型安全是传说:编译器完全放弃检查,运行时错误等着你
▶ 调试像侦探破案:错误信息基本没用,全靠printf大法
▶ 可读性差:void*满天飞,看代码像猜谜
▶ 性能损失:类型转换和函数指针调用有开销
生存指南:
▶ 做好防御:使用assert和边界检查
▶ 注释要详细:每个void*都要说明原本类型
▶ 写测试代码:各种类型都要测试一遍
▶ 考虑用C++:如果项目复杂,真的...考虑用C++吧
八
什么时候用泛型?(选择题)
A. 当你有多个相似数据结构时 ✅
B. 当你想装逼时 ❌
C. 当需要算法复用时 ✅
D. 当你不想写重复代码时 ✅
E. 所有情况下都用 ❌(记住,KISS原则:Keep It Simple, Stupid)
结语——泛型是你的工具,不是你的信仰
学完泛型,你现在有了两种超能力:
▲ 写出能处理任何类型的通用代码
▲ 写出能让你debug到凌晨3点的通用bug
C语言的泛型就像瑞士军刀——功能多,但每个功能都不如专用工具好用。它给了你自由,但也给了你足够多的绳子把自己吊死。
最后送大家一句话:泛型虽好,可不要贪杯哦! 在简单和通用之间找到平衡,才是好程序员的标志。
现在,去写点泛型代码吧!记得先保存,别问我为什么知道要提醒你这个...(血泪教训啊!)
关注我们

“三度编程”是一家专注于青少儿编程培训的教育机构,专业培训scratch、python、c++等少儿编程课程,旗下学员多人参加蓝桥杯、中国电子学会、ACT等知名编程赛事,多次获得国、省、市、区、校级竞赛奖状,被誉为“少儿编程十大优秀品牌”“诚信办学单位”“年度影响力青少儿编程品牌”“少儿编程金牌团队”。

微信号|sanducoding001
客服电话 | 15001141507
办公地址 | 苏州昆山吾悦广场
服务地区 | 苏州、上海、北京,及全国各地