C|泛型机制:预编译指令define、泛型指针与共用体

C++通过模板支持泛型机制,C没有模板语法机制,通过预编译指令#define、泛型指针与共用体来实现一定程度上的泛型机制。

1 预编译指令#define

#define宏的参数没有类型限制,可以用此来作为泛型编程。

宏与类型无关。只要对参数的操作是合法的,它可以使用于任何参数类型。

#define TYPE int
#define SQUARE(x) ((x)*(x))
TYPE square(TYPE x){
    return x*x;
}
#define PRINT(FORMAT, VALUE) \
printf("The value of " #VALUE \
" is " FORMAT "\n", VALUE)

void test(){
    int x = 4;
    PRINT("%d",x+3);
}
#define MAX(a, b) ((a) > (b) ? (a) : (b))

带参宏和函数的不同之处:

2 泛型指针void*

对于高级编程语言而言,数据必须具有类型的属性,类型决定其存储单元的字节数,及与类型相关的编码与解码方式。void表示“空、无”,是C或C++的关键字:

void test(void){ // 这里的void表示"无”
    //void var; // error illegal use of type 'void'
    void * p; // 指针提供一个存储单元的蓝图,并提供首地址,需要类型决定存储单元的字节数,及编码解码方式,
    // 允许声明指针目标暂定为void(无类型),但最终解引用到值时,需要提供类型
    void **pp;
    p = *pp;
    int val=5;
    p = &val;
    //int a = *p; // illegal indirection
    int b = *(int *)p;
    // 类型未确定,即无法确定对应内存空间的长度和解码方案,
    // 强制类型转换,即重新确定长度和解码方案。
    const int c = 6;
    const void *cp = &c;
}

在C中指针变量拥有与其他变量一样的类型。之所以指针变量会有类型是因为当我们想解引用指针变量目标对象的值时, 编译器已经知道指针所指向的目标对象的数据的类型, 从而可以访间相应的数据。但是, 有些时候我们在数据处理的某一阶段并不关心指针所指向的目标对象的具体类型。在这种情况下,就可以使用泛型指针(generic pointer),泛型指针并不指定具体的数据类型。

C语言中的指针可以分为三类:指向数据对象的指针、指向函数的指针和指向“虚无”的指针(void *类型)。对它们可以进行的运算完全不同。对指向数据对象的指针可以进行*(间接访问)、+、-等运算;指向函数的指针则只有一元*运算,而不存在+、-等运算;至于指向void 类型的指针,*(间接访问)、+、-等几种运算都没有定义。指向数据对象的指针可能勉强而不精确地称为“变量的地址”,其他两种指针都跟变量没有什么关系。

通常情况下,C只允许相同类型的指针之间进行转换。例如:一个字符型指针sptr (个字符串)和一个整型指针iptr, 我们不允许把sptr转换为iptr或把iptr转换为sptr。但是,泛型指针能够转换为任何类型的指针, 反之亦然。因此, 如果有一个泛型指针gptr, 就可以把sptr转换为gptr或者把gptr转换为sptr。在C语言中, 通常声明一个void指针来表示泛型指针。

void通常用于表示类型暂定,对于强类型语言,为了实现一些运算符或函数的通用性,通常首先将类型表示为void,最终类型转换为具体类型。如malloc()、new、qsort()等。

2.1 泛型指针通常用做库函数或自定义函数参数

.很多情况下void指针都是非常有用的。例如,C标准函数库中的memcpy函数, 它将一段数据从内存中的一个地方复制到另一个地方。由于memcpy可能用来复制任何类型的数据,因此将它的指针参数设定为void指针是非常合理的。void指针同样也可以用到其他普通的函数中。例如交换函数swap2(),可以把函数参数改为vvoid指针,这样,swap2()就变成一个可以交换任何类型数据的通用交换函数, 代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int swap2(void *x, void *y, int size)
{
    void *tmp;
    if((tmp=malloc(size)) == NULL)
        return -1;
    memcpy(tmp,x,size);
    memcpy(x,y,size);
    memcpy(y,tmp,size);
    free(tmp);
    return 0;
}
int main()
{
    int a=3,b=4;
    swap2(&a,&b,sizeof(int));
    printf("%d %d\n",a,b);
    double c=3,d=4;
    swap2(&c,&d,sizeof(double));
    printf("%f %f\n",c,d);
    getchar();
    return 0;
}

buffer是一个指向用千保存数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数, 当然stream是数据读取或写入的流。 buffer参数被解释为一个或多个值的数组。count参数指定数组中有多少个值,所以读取或写入一个标量时,count的值应为1。函数的返回值是实际读取或写入的元素(并非字节)数目。如果输入过程中遇到了文件尾或者输出过程中出现了错误, 这个数字可能比请求的元素数目要小。

fread函数用于读取二进制数据,fwrite函数用于写入二进制数据。它们的原型如下所示:

size_t fread(void *buffer, size_t size, size_t count FILE *stream);
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);

2.2 在数据结构中使用泛型指针

void指针在用来实现数据结构时是非常有用的,因为可以通过void指针存储和检索任何类型的数据。我们再来看一下之前提到过的链表结构ListElmt,回想一下,这个结构包含两个成员:data和next。如果data被声明为一个void指针,那么data就可以指向任何类型的数据。从而,我们就可以使用ListElmt结构来建立各种不同数据类型的链表。

假设定义了一个链表的操作函数list_ins_next,它的功能是将一个指向data的指针元素插入链表中:

typedef struct ListElmt_ {
    void               *data; // 泛型指针
    struct ListElmt_   *next;
} ListElmt;
typedef struct List_ {
    int                size;
    int                (*match)(const void *key1, const void *key2); // 函数指针使用泛型指针
    void               (*destroy)(void *data); // 函数指针使用泛型指针
    ListElmt           *head;
    ListElmt           *tail;
} List;
int *iptr;
int list_ins_next(List *list, ListElmt *element, void *data);

要将指针iptr引用的整数插入名为list的整型链表中,element引用的元素后面,使用以下调用。C语言允许将整型指针iptr赋值给参数data,因为data是一个void指针。

retval = list_ins_next(&list, element, iptr);  

当然,当从一个链表中删除数据时,必须使用正确的指针类型来检索要删除的数据。这样做是为了保证当我们想要对数据进行操作时数据的类型是正确的。例如从一个链表中删除元素的函数list_rem_next,它的第三个参数是一个指向void指针的指针:

int list_rem_next(List *list, ListElmt *element, void **data);  // remove

想要从list中element引用的元素后面删除一个整型变量,用如下调用方式。当函数返回时,iptr指向已删除的数据。这是由于此操作改变了指针本身,使其指向已删除的数据,因此传递iptr指针的地址:

int *iptr;  // 要产生对一级指针的副作用,函数参数要使用一个二级指针
retval = list_rem_next(&list, element, (void **)&iptr);

同时,此函数调用包含一个将iptr临时转换为指向void指针的指针的过程。正如我们之后要讲到的,类型转换是C语言中一种特殊的转换机制,它允许我们临时把一种类型的变量转换为另一种类型的变量。在这里,类型转换是必须的,因为C语言中虽然一个void指针与其他类型的指针相兼容,但一个指向void指针的指针并不一定与其他类型的指针兼容。

2.3 泛型指针与类型转换

要将类型为T的变量t转换成S类型,只需要在t前加上用圆括号括上的S。例如,要将一个整型指针iptr转换为一个浮点型指针fptr,在整型指针前面加上一个用圆括号括起来的浮点指针即可,如下所示:

fptr = (float *)iptr;

(通常来说,将一个整型指针转换为一个浮点型指针是一种危险的做法,但是在这里仅仅用这个例子做一个类型转换的实例而已。)在类型转换之后,iptr与fptr都指向同一块内存地址。但是,从这个地址取到什么类型的值是由我们用什么类型的指针访问它所决定的。

对于泛型指针来说类型转换非常重要,因为只有告诉泛型指针是通过何种类型来访问地址时,泛型指针才能取到正确的值。这是由于泛型指针不会告诉编译器它所指向的是何种类型数据,因此编译器既不知道多少个字节要被访问,也不知道应该如何解析字节。当将泛型指针赋值给其他类型的指针时,使用类型转换也是一种很好的代码自注释方法。尽管这里的转换并不是必需的,但这样做能够大大提高程序的可读性。

void*类型可以将不同指针类型统一起来进行计算,例如:

	int a;
	char c;

&a和&c是不同类型指针,所以&a-&c是非法的,但(void*)&a – (void*)&c是合法的,表示两个变量内存间距。下面的函数可以顺利完成这个任务:

	long mem_distance(void *a,void *b)
	{
		return a-b;
	}

作为形参的void*可以接收任何类型的指针,实参的指针将会转换为void*类型。

当转换指针时,我们对内存中的数据对齐方式必须特别注意。具体来说,我们需要知道,指针的类型转换会破坏计算机本身的对齐方式。很多计算机对对齐方式有要求,以便某些硬件的优化可以使访问内存更有效率。例如,一个系统可能要求所有整数按字边界对齐。所以,如果有一个非按字对齐的void指针,当将它转换为一个整型指针并试图获取它的值时,程序可能在运行时出现异常。

对于void指针,二级void指针可以解引用到一级void指针,一级void指针不能引用,因为你就只有一个地址,没有类型,就不知道需要解引用多少字节?用什么方式解码字节。

2.4 void*与char*、内存按字节操作

内存按字节编码,内存操作按字节操作,而char类型的长度是一个字节的长度,所以内存按字节操作可以用char*类型操作来模拟。内存操作函数通常以void*为参数,在函数体中转换为char*来操作。

/*
memmove() copies a source memory buffer to a destination memory buffer.This routine
recognize overlapping buffers to avoid propogation.For cases where propagation is not
a problem, memcpy() can be used.
memmove()由src所指定的内存区域赋值count个字符到dst所指定的内存区域。 src和dst所指内
存区域可以重叠,但复制后src的内容会被更改。函数返回指向dst的指针。
*/
void * my_memmove(void * dst,const void * src,int count)
{
    void * ret = dst;
    if(dst <= src || (char *)dst >= ((char *)src + count))
    {
        while(count--)
        {
            *(char *)dst = *(char *)src;
            dst = (char *)dst + 1;
            src = (char *)src + 1;
        }
    }
    else
    {
        dst = (char *)dst + count - 1;
        src = (char *)src + count - 1;
        while(count--)
        {
            *(char *)dst = *(char *)src;
            dst = (char *)dst - 1;
            src = (char *)src - 1;
        }
    }
    return(ret);
}
int main()
{
    char a[12];
    puts((char *)my_memmove(a,"ammana_babi",16));
    system("pause");
    return 0;
}

3 共用体

一个存储单元可以是枚举的各种数据类型共用一体。

一个联合的所有成员都存储于同一个内存位置。通过访问不同类型的联合成员, 内存中相同的位组合可以被解释为不同的东西。联合在实现变体记录时很有用, 但程序员必须负责确认实际存储的是哪个变体并选择正确的联合成员以便访问数据。联合变量也可以进行初始化, 但初始值必须与联合第1个成员的类型匹配。

3.1 基本数据类型共用一体

一个存储单元可以是枚举的各种基本数据类型共用一体。

#include <stdio.h>

typedef struct Datatype{
    enum{
        character,integer,floating_point
    }vartype;
    union{
        char c;
        int i;
        double f;
    };
}datatype;

void print(datatype *dt){
    switch(dt->vartype){
    case character:
        printf("character type: %c\n",dt->c);
        break;
    case integer:
        printf("integer type: %d\n",dt->i);
        break;
    case floating_point:
        printf("floating_point type: %f\n",dt->f);
        break;
    }
}

int main()
{
    datatype a;
    a.vartype = character;
    a.c = 'c';
    print(&a);
    
    datatype b;
    b.vartype = integer;
    b.i=2;
    print(&b);
    
    datatype c;
    c.vartype = floating_point;
    c.f=3.14;
    print(&c);
    getchar();
    return 0;
}
/*
character type: c
integer type: 2
floating_point type: 3.140000
*/

用C++的类就优雅多了:

#include <iostream>
using namespace std;
class datatype{
    enum{
        character,integer,floating_point
    }vartype;
    union{
        char c;
        int i;
        double f;
    };
public:
    datatype(char ch){
        vartype = character;
        c = ch;
    }
    datatype(int ii){
        vartype = integer;
        i=ii;
    }
    datatype(double d){
        vartype = floating_point;
        f=d;
    }
    void print();
};
void datatype::print(){
    switch(vartype){
    case character:
        cout<<"character type: "<<c<<endl;
        break;
    case integer:
        cout<<"integer type: "<<i<<endl;
        break;
    case floating_point:
        cout<<"floating_point type: "<<f<<endl;
        break;
    }
}
int main()
{
    datatype a('c'),b(16),c(3.14);
    a.print();
    b.print();
    c.print();
    getchar();
    return 0;
}

3.2 复合数据类型共用一体

一个存储单元可以是枚举的各种复合数据数据类型共用一体。

在结构体中共用结构体,在Pascal和Modula中被称为变体记录(variant record):

#include <stdio.h>
#include <string.h>
#define MAXPARTS 12

struct Parts{ // 零件
	int cost;
	char supplier[12];
	char unit[12] ;
};

struct Assembly{ // 装配件
	int n_parts;
	struct {
		char partno[12];
		short quan;
	}parts[MAXPARTS];
};

struct Inventory{ // 存货类型,或是零件,或是装配件
	char partno[10];
	int quan;
	enum{PART,ASSEMBLY}type; // 存货类型
	union {
		struct Parts parts;
		struct Assembly assembly;
	}info;
};

int main()
{
	struct Inventory screen;
	strcpy(screen.partno,"p001");
	screen.quan = 12;
	screen.type = Inventory::PART;
	screen.info.parts.cost = 122;
	strcpy(screen.info.parts.supplier,"hw");
	strcpy(screen.info.parts.unit,"pcs");
	
	struct Inventory shell;
	strcpy(shell.partno,"a001");
	shell.quan = 4;
	shell.type = Inventory::ASSEMBLY;
	shell.info.assembly.n_parts=22;
	strcpy(shell.info.assembly.parts[0].partno,"d001");
	shell.info.assembly.parts[1].quan = 5;
	int costs;
	if(shell.type == Inventory::ASSEMBLY)
		costs = shell.info.assembly.n_parts;
	
	printf("%d\n",costs); //22
	getchar();
	return 0;
}

另一个实例:

#include <stdio.h>
#include <string.h>

struct date
{
    int year;
    int month;
    int day;
};

struct marriedState
{
    struct date marriedDay;
    char spouseName[20];
    int child;
};

struct divorceState
{
    struct date divorceDay;
    int child;
};

union maritalState // 婚姻的三种状态
{
    int single;
    struct marriedState married;
    struct divorceState divorce;
};

struct employee
{
    char name[20];
    char sex;
    int age;
    int marryFlag;
    union maritalState marital;
};

int main()
{
    struct employee wwu;
    strcpy(wwu.name,"wwu");
    wwu.sex = 'M';
    wwu.age = 18;
    wwu.marryFlag = 0;
    wwu.marital.single = 1;
    
    printf("%d\n",wwu.marital.single);

    getchar();
    return 0;
}

ref

https://www.learncpp.com/cpp-tutorial/void-pointers/

-End-

原文链接:,转发请注明来源!