写 C 的时候有时候需要用到交换两个变量,但是针对不同类型的变量要写不同的函数,很不方便。那么如何做到“泛型”交换两个变量呢?
我们都知道C中的变量有地址这个概念,使用&变量
操作就可以参看地址值,不用多说,要交换两个变量首先得找到这两个变量的地址,先来看最简单的通过指针交换两个整型变量:
void swap_int(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
代码很简单,相信学过指针的都能看懂。
泛型版交换两个变量
现在的任务是交换两个不知道具体类型的变量(注意这里虽然不知道这两个变量的类型,但是这两个变量的类型都必需相同,比如都为int
型),首先还是要找到变量的地址,这时候我们就不能像上面的代码那样直接定义int *a
了,因为如果这样写的话就把指针类型写死了。
为此,我们可以使用 C 中的“void *指针”,这里的void *
并不代表该指针指向void
类型,而是代表该指针的类型是“不确定的”,比如用到malloc
时,你可能会这样写:
// 创建了一个大小为5的整型数组
// 这里的强制类型转换在C中是不必要的,
// 但是在C++中是硬性要求,所以还是建议写上
int *array = (int *)malloc(sizeof(int) * 5);
这里malloc
函数的返回值类型就是void *
,你可以把void *
当成一个贴纸,贴给(赋值给)哪种类型都行。
有了void *
的基础,可以将上面的交换两个整型的函数改写为如下形式:
void swap_obj(void *a, void *b);
但是在赋值时会出现问题——void *
指针必须转为其他类型的指针(比如int *
)才能进行解引,直接解引用void *
指针会报错。
其实这里我们并不需要知道void *
指向的具体类型,我们只要知道它指向的变量的大小即可。前面说了在 C 中变量有地址的概念,但是光只有个地址是不够的,如果把地址比作变量在内存中的门牌号,那么变量的值就是门牌号对应的房屋,有这两者才能完整地表示一个变量。最后还要补充一点,在计算机内,变量是以二进制的形式存储的,不同类型的变量存储的二级制值可能相同,但是在使用和表示时可能又是另一种完全不同的形式。
说了那么多,具体该怎么做呢?来看下面的函数:
void swap_obj(void *obj1, void *obj2, int obj_size){
// 遍历变量的每一位
for(int i=0; i<obj_size; i++){
// char 在内存中只占一字节,在这里我们要取出变量的
// 每一位,刚好可以使用 char 进行强转。
char temp = *((char *)obj1 + i);
// 交换对应位的二进制值
*((char *)obj1 + i) = *((char *)obj2+i);
*((char *)obj2 + i) = temp;
}
}
上面的代码,先把void *
指针转为char *
指针,然后解引这个字符指针,这样就能避免解引void *
指针的语法错误。又由于char
在内存中只占一个字节,所以这里的交换值实际上交换的是对象的部分二进制位,这样就无须知道变量的具体类型了。
泛型版复制变量
有了泛型版交换两个变量的基础,很容易写出泛型版复制变量,代码如下:
void * copy(void *obj, int obj_size){
void *new_obj = malloc(sizeof(obj_size));
for(int i=0; i<obj_size; i++){
*((char *)new_obj + i) = *((char *)obj + i);
}
return new_obj;
}
首先第一个参数仍然是一个void *
指针,由于这里只是复制一个变量所以第二个参数为该变量的大小,然后我们根据传入的变量大小创建一个新的变量,再遍历传入变量的每一位并赋值给刚才创建的新变量,最后返回创建的新变量即可。
这里用结构体做演示:
void * copy(void *obj, int obj_size){
void *new_obj = malloc(sizeof(obj_size));
for(int i=0; i<obj_size; i++){
*((char *)new_obj + i) = *((char *)obj + i);
}
return new_obj;
}
typedef struct _NODE{
int val;
int item;
}NODE, *PNODE;
int main(){
NODE node = {1, 10};
PNODE new_node = (PNODE)copy(&node, sizeof(node));
// 10 1
printf("%d %d\n", new_node->item, new_node->val);
free(new_node);
return 0;
}
对于复制两个变量我们需要注意一下两点:
- 新创建的变量是分配在堆内存中的,因此用完了一定要free掉,否则会发生内存泄漏。
- 这里的复制操作事实上是浅复制,也就是说当传入的变量(比如结构体)内含有指针时,复制完成后的变量中的指针仍然和传入的变量中的指针指向的值相同。
基于此,我们可以实现泛型链表等结构,对此需要在链表结构体中新增一个用来保存元素大小的数据域,代码如下:
// C 中的泛型链表部分代码
// 元素
typedef void * ITEM;
// 链表结点
typedef struct _NODE{
ITEM item;
struct _NODE *next;
}NODE, *PNODE;
// 链表
typedef struct _LIST{
PNODE head;
PNODE tail;
size_t item_size; // 用来保存链表数据元素的大小
size_t size;
}LIST, *PLIST;
// 创建一个新的item,并将原先的item的值复制进来
// 由于这里是泛型版,所以会麻烦一点
// static 修饰符在这里表示函数仅当前文件作用域,你也可以理解为Java中的private修饰符
static ITEM copy(ITEM item, size_t size){
ITEM temp = malloc(size);
for(int i=0; i<size; i++){
// 根据二进制位复制元素,无需知道元素的类型
*((char *)temp + i) = *((char *)item + i);
}
return temp;
}
全部代码
#include <stdio.h>
#include <stdlib.h>
void swap_int(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
void swap_obj(void *obj1, void *obj2, int obj_size){
for(int i=0; i<obj_size; i++){
char temp = *((char *)obj1 + i);
*((char *)obj1 + i) = *((char *)obj2+i);
*((char *)obj2 + i) = temp;
}
}
void * copy(void *obj, int obj_size){
void *new_obj = malloc(sizeof(obj_size));
for(int i=0; i<obj_size; i++){
*((char *)new_obj + i) = *((char *)obj + i);
}
return new_obj;
}
typedef struct _NODE{
int val;
int item;
}NODE, *PNODE;
int main(){
int a = 1, b = 2;
printf("before: a = %d, b = %d\n", a, b);
swap_obj(&a, &b, sizeof(a));
printf("after: a = %d, b = %d\n", a, b);
NODE node = {1, 10};
PNODE new_node = (PNODE)copy(&node, sizeof(node));
printf("%d %d\n", new_node->item, new_node->val);
free(new_node);
return 0;
}