一、共用体

在早期,共用体也被称为“联合体”,不过现在都习惯称为“共用体”

1.共用体 与 结构体的异同

相同点

操作语法基本是相同的,比如各自都可以使用.->来访问成员。

不同点

在结构体中,每个成员拥有的都是独立的空间,但是在联合体中,所有所有成员共用相同的空间,
整个联合体的空间大小为最大成员的大小,通过不同成员来访问时,会按照各自成员的类型来解释是空间

union my_un1
{
    int a;         //4字节
    long long b;   //8字节
    double c;      //8字节
}un1;

整个联合体的大小为8个字节


所有成员公用相同的空间,空间第一个字节地址为所有成员的地址,
联合体空间中所放的值,以最后一次的赋值为准。

un1.a = 10;
un1.c = 12.56;

2. 当初设计出联合的原因

联合体的特点是所有的成员共用同一个空间,可以很好的节省内存空间

在早期,内存空间比较精贵,为了节省内存,于是设计出了联合体这个玩意,随着集成电路的发展,
内存空间越来越大,节省空间这件事情已经不再像以前那么重要,
对于现在的开发来说,如何提高开发效率才是首要的

正是由于节省内存变的不再像以前那么重要,所以现在联合体用的越来越少,事实上就算没有联合体这个东西,
我们照样能开发,因为对于联合体来说,除了节省内存外,在功能上完全可以被结构体和指针所替代,
所以我们才会发现,在实际开发中就算没有使用过联合体,但是并没有觉得有什么不方便


3. 联合体的使用举例

由于联合体本来就是可有可无的,所以目前联合体的使用非常鲜见,
不过在一些库函数和OS API的传参中往往还能见到联合体的身影,那么联合体在传参中有什么作用呢?
我们这里就来讲一讲联合体在函数传参中的使用

(1)例子1
1)回顾以前的例子

#include <stdio.h>

void print_fun(int type, void *arg)
{
    if(1 == type)
    {
        printf("%d\n", *((int *)arg));
    }
    else if(2 == type)
    {
        printf("%f\n", *((float *)arg));
    }
    else if(3 == type)
    {
        printf("%s\n", (char *)arg);
    }
}

int main(void)
{
    int a = 100;
    float b = 20.45;
    char *str = "hello world";

    print_fun(1, (void *)&a);
    print_fun(2, (void *)&b);
    print_fun(3, (void *)str);

    return 0;
}

同一个形参需要面对多种实参时,可以使用void *来兼容,实际上我们也可以使用联合来实现

2)使用联合体来实现:

#include <stdio.h>

union Un_Arg
{
    int arg1;
    float arg2;
    char *arg3;
};

void print_fun(int type, union Un_Arg arg)
{
    if(1 == type)
    {
        printf("%d\n", arg.arg1);
    }
    else if(2 == type)
    {
        printf("%f\n", arg.arg2);
    }
    else if(3 == type)
    {
        printf("%s\n", arg.arg3);
    }
}

int main(void)
{
    int a     = 100;
    float b   = 20.45;
    char *str = "hello world";
    union Un_Arg arg;

    arg.arg1 = a;
    print_fun(1, arg);

    arg.arg2 = b;
    print_fun(2, arg);

    arg.arg3 = str;
    print_fun(3, arg);

    return 0;
}

显然就算没有联合体这个玩意,我们也可以使用void *等方式来代替

(2)例子2
我们举一个Linux OS API的例子。

int semctl(int semid, int semnum, int cmd, ...);

...表示是变参,参数是不确定,OS API此时就使用联合体来应对这个变参

union semun 
{
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
};

调用举例:

union semun arg4;
arg4.val = 2;

semctl(***, ***, ***, arg4.val);

struct semid_ds semid_buf = {***};
arg4.buf = &semid_buf;
semctl(***, ***, ***, arg4.buf);

所以,以后看到在传参中有使用联合体的话,不要感到惊讶,不要不理解人家为什么这么做


二、枚举

其实没有枚举这个东西也是可以的,很多人做了多年C开发,几乎就没有怎么用过枚举,但是也照样开发的很好,
但是在很多C的源码中,枚举的使用频率还是非常高的,因此需要理解枚举是什么,而且枚举也确实有自己的优点,
我们也鼓励大家在自己的代码中多使用枚举。在CC++Java等其它语言中都是有的

1. 枚举怎么来的?

枚举来自于宏定义的不够完美,当然有了枚举后,并不会因此就把宏定义给干掉了,因为在普通情况下,
宏定义使用起来还是非常方便的,不过在某些情况下,使用枚举则会更好些

宏定义的问题:宏定义是散列的

#define SUN 0
#define MON 1
#define TUE 2
#define WEN 3
#define THU 4
#define FRI 5
#define SAT 6

这些宏定义是散列的,事实上对于一般的宏定义来说,相互之间没有什么关系,所以散列着并没有什么大不了的,
但是对于相互间有关联的宏来说,散列着不利于代码的辨识

比如一周的7天,一年的12个月、一个月的30天等宏定义,相互间是有关联的,
散列着不利于体现它们之间的相互关系


2. 宏定义传参问题

前面“星期宏”的类型为int型,在传递这些宏时,形参类型就为整形

void fun(int weekDay)
{
    pritnf("%s\n", weekDay);
}

fun(SUN);
fun(TUE);
...
fun(SAT);

fun(100)也是没有问题的,因为100也为int,但是100并不是一周的某一天,
并不符合我们传参需求,假如传递100是提醒我们,参数不符合要求,那是不是更好呢?
显然宏是做不到的,但是枚举能做到


3. 枚举的优势

宏定义散列的问题,传递宏时参数不符合要求的问题,都可以被枚举解决

枚举类型的定义

enum week   
{
    SUM,   // 与#define SUM 0 等价
    MON,   // 与#define MON 1 等价
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
};

以上为一个为枚举类型
(1)enum:枚举关键字,类似 unionstruct
(2)week:枚举类型的名字
(3){...}{}中的为枚举元素(枚举值)

每一个枚举元素等效于#define定义,比如里面的SUM等效于#define SUM 0
但是与宏不同的是,SUMMON等被{}集中在了一起,不再是散列的,
枚举的这种集中化的管理,可以让我们代码具有更高的识别度,让代码变的更加的优美

通过week这个名字,我们一看就知道SUMMON等就是一周的每一天


3. 枚举元素

enum week{SUM, MON, TUE, WEN, THU, FRI, SAT};

都没明确给值时,默认情况为,第一个SUM0,第二个MON1,后面的依次类推即可

enum week{SUM=2, MON, TUE=5, WEN, THU=10, FRI, SAT};

没有定义值的,就是前一个值递增加1

MON=3
WEN=6
FRI=11
SAT=12

枚举元素的值必须为整形

// 编译时会报错
enum week{SUM, MON=12.56, TUE, WEN, THU, FRI, SAT};

这也是枚举的一个缺点,不能指定浮点、字符串、等等,但是宏定义可以

#define A   12.56
#define B   "hello world"
#define C   PI

宏和枚举各有优缺,是一个相互弥补的关系


每一个枚举元素与宏定义一样,可以被直接使用的,比如

printf("%d\n", SUM);

int a = SUM + SAT;

每一个枚举值与#define是等效的,所以每一个枚举值和宏定义一样,
都是一个符号常量,不能被赋值

SUM = 100;
SAT = 200;

都无法编译通过


4. 枚举变量

如何定义枚举变量

enum week
{
    SUM,
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
}weekday;

或者:

enum week weekday;

weekday为枚举变量


给枚举变量赋值:可以通过初始化或者赋值来给值

一般情况下,枚举变量只能给自己类型中的枚举值,比如:

enum week weekday = SUM;  //初始化
weekday = MON;            //赋值
weekday = FRI;
...

不能给枚举类型以外的值,比如:

weekday = 1; //编译器会报错

比如使用VC++这个IDE来测试,weekday = 1是不能编译通过的

正是由于这一特点,传参时如果将宏替换为枚举值的话,如果传递是“非枚举值的话”,就会帮我们报错,比如:

#include "stdafx.h" //这个是VC++特有的头文件,在codeblocks下不需要
#include <stdio.h>

enum Week
{
    SUM,
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
};

void fun(enum Week weekday)
{
    printf("今天是周%d\n", weekday);
}

int main(void)
{
    fun(WEN);

    return 0;
}

如果将fun(WEN)改为fun(100)的话,是无法编译通过的,可以帮我们预防传递错误参数,提高排错效率

不过不幸的是,枚举变量以上的这一特点,只有某些编译器才支持,而有些编译器不支持,
VC++使用的是windows编译器,支持这一特点,但是gcc编译器并不支持,

所以将weekday = 1复制到codeblockslinux下时,
确可以编译通过,所以在gcc编译器下,fun(100)这个传参也是能够编译通过

5. 总结:何时使用枚举

当值为整形,而且相互间有关联时,建议使用枚举,你非要使用宏定义也是可以的,
但是如果是字符串、浮点数等等,就不能使用枚举,只能使用宏定义

至于传参这个问题,由于不同的编译器会有不同的情况,因此传参时我们不做硬性规定,传递宏定义和枚举都可以