1. 联合体

在 C 语言中,联合体又叫共用体(Union)是一种特殊的数据类型,定义联合体的语法如下:

1
2
3
4
5
6
union 联合体名 
{
成员类型 成员名1;
成员类型 成员名2;
...
};

定义联合体变量的语法如下:

1
union 联合体名 变量名;

从语法上来看联合体和结构体非常的类似只是将关键字struct替换成了union,但是,二者在使用的时候有很大区别,其特点如下:

  • 联合体的不同成员共享同一块内存,它们的值互相覆盖。
  • 联合体的大小由最大成员的大小确定。

下面是一个示例,展示如何定义联合体并使用联合体变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 定义一个联合体类型
union Data
{
unsigned char c;
unsigned int i;
unsigned short s;
char str[20];
};

int main()
{
// 定义联合体变量
union Data data;

// 打印所有成员的地址
printf("data.c address = %p\n", &data.c);
printf("data.i address = %p\n", &data.i);
printf("data.s address = %p\n", &data.s);
printf("data.str address = %p\n", &data.str);
// 打印联合体大小
printf("union size = %zd\n", sizeof(data));

// 设置联合体成员的值并打印
data.i = 10;
printf("整数值: %d\n", data.i);
data.s = 314;
printf("浮点数值: %d\n", data.s);
strcpy(data.str, "Hello, Dabing!");
printf("字符串值: %s\n", data.str);

data.i = 0xffabcd98;
printf("data.c = %x\n", data.c);
printf("data.s = %x\n", data.s);
printf("data.i = %x\n", data.i);
printf("========================\n");
data.c = 0;
printf("data.c = %x\n", data.c);
printf("data.s = %x\n", data.s);
printf("data.i = %x\n", data.i);

return 0;
}

程序输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data.c address = 000000875EEFFB38
data.i address = 000000875EEFFB38
data.s address = 000000875EEFFB38
data.str address = 000000875EEFFB38
union size = 20
整数值: 10
浮点数值: 314
字符串值: Hello, Dabing!
data.c = 98
data.s = cd98
data.i = ffabcd98
========================
data.c = 0
data.s = cd00
data.i = ffabcd00

通过输出的结果可以证明以下结论:

  • 联合体中各个数据成员共用同一块内存,因为他们的起始地址是相同的
  • 联合体各个数据成员之间会发生数据覆盖
  • 联合体的大小等于占用存储空间最大的成员的大小

另外,还有一个细节需要说明:联合体中的各个成员的类型可能不同,所以在取数据的时候操作的内存大小也不尽相同。

  • data.i = 0xffabcd98; 给整形成员赋值,使用了4个字节的内存
  • printf("data.c = %x\n", data.c); 从一个字节的内存中读数据
    • 0xffabcd98低地址位的一个字节中存储的数据是0x98
  • printf("data.s = %x\n", data.s);从两个字节的内存中读数据
    • 0xffabcd98低地址位的两个字节中存储的数据是0xcd98
  • printf("data.i = %x\n", data.i);从四个字节的内存中读数据

联合体应用:验证当前主机的大小端(字节序)

大小端(Endianness)指的是在多字节数据类型(如整数)在内存中存储时的字节顺序。

  • 大端字节序(Big Endian)是指高位字节存储在低地址
  • 小端字节序(Little Endian)是指低位字节存储在低地址。

举个例子,假设我们有一个 4 字节的整数值 0x12345678 在内存中存储的情况如下:

  • 大端字节序:地址由低到高的顺序依次存储为 12 34 56 78
  • 小端字节序:地址由低到高的顺序依次存储为 78 56 34 12

不同的处理器架构在存储数据时可能采用不同的字节序。例如,x86 架构使用小端字节序,而 PowerPC 架构使用大端字节序。因此,在处理跨平台数据交换时需要注意字节序的问题。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

union MyData
{
unsigned int data;
struct
{
unsigned char byte0;
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
}byte;
};

int main()
{
union MyData num;
num.data = 0x12345678;
if (0x78 == num.byte.byte0)
{
printf("Little endian\n");
}
else if (0x78 == num.byte.byte3)
{
printf("Big endian\n");
}
return 0;
}

由于联合体内部的databyte成员是共用同一块内存,并且二者占用的内存大小相同,所以通过结构体成员byte就可以非常轻松的取出data中各个字节的值,程序中是对最高位的字节值(byte.byte3)和最低位的字节值(byte.byte0)进行了判断。

2. 枚举

枚举(Enumeration)是一种在编程语言中表示一组具名常量的数据类型。枚举常常用于定义一组相关的离散值,比如颜色、星期几、月份等。在 C 语言中,可以使用 enum 关键字定义枚举类型。

定义枚举类型的语法如下:

1
enum 枚举名 {1, 值2, ... };

枚举值默认从 0 开始,依次递增。可以显式指定枚举值的值,如 MONDAY = 1, TUESDAY = 2, ...。并且枚举值是常量,不能在程序中用赋值语句再对它赋值。

下面是一个示例,展示如何定义和使用枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>

// 定义一个枚举类型
enum Weekday
{
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};

int main()
{
// 定义枚举变量
enum Weekday today;
// 设置枚举变量的值
today = WEDNESDAY;

// 使用 switch 语句根据枚举变量的值进行处理
switch (today)
{
case MONDAY:
printf("Today is Monday.\n");
break;
case TUESDAY:
printf("Today is Tuesday.\n");
break;
case WEDNESDAY:
printf("Today is Wednesday.\n");
break;
case THURSDAY:
printf("Today is Thursday.\n");
break;
case FRIDAY:
printf("Today is Friday.\n");
break;
case SATURDAY:
printf("Today is Saturday.\n");
break;
case SUNDAY:
printf("Today is Sunday.\n");
break;
default:
printf("Invalid day.\n");
}

return 0;
}

在上述示例中,使用 enum 关键字定义了一个名为 Weekday 的枚举类型,其中包含了一周的七个枚举值。然后,在 main 函数中,声明了一个名为 today 的枚举变量,并通过赋值将其设置为 WEDNESDAY。接着,使用 switch 语句根据枚举变量的值进行处理,并打印出对应的结果。

其实枚举类型就是一种特殊的整形,使用枚举类型在程序中可以增加代码的可读性,使得相关常量的含义更加清晰。

3. typedef

typedef 是 C 语言中的一个关键字,用于为已存在的数据类型定义新的名称(alias)不能创建新类型。它能够简化复杂的类型声明,提高代码的可读性和可维护性。

typedef 关键字的语法:

1
typedef 原类型 新类型名;

typedef #define用法上有相似之处,但是也有很多不同:

  • typedef 仅限于数据类型,而不能是表达式或具体的值
  • #define发生在预处理,typedef发生在编译阶段

3.1 基础类型

下面是一个示例,展示如何使用 typedef 来创建新的类型别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;

typedef struct type
{
UBYTE a;
INT b;
T_BYTE c;
}TYPE, * PTYPE;

int main()
{
TYPE t;
t.a = 254;
t.b = 10;
t.c = 'c';

PTYPE p = &t;
printf("%u, %d, %c\n", p->a, p->b, p->c);

return 0;
}

在上面的示例代码中给一些基础数据类型定义了别名,然后再基于这些别名定义了相关的变量,很容易理解,不再过多进行解释。

3.2 复合类型

除此之外还可以给结构体、联合体和枚举等数据类型创建新的类型别名。

当使用typedef来定义结构体时,可以为结构体类型提供一个简短、易于使用的别名。下面是一个示例,展示如何使用typedef定义结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

typedef struct
{
int x;
int y;
} Point;

int main()
{
// 使用结构体别名创建变量
Point p1;
p1.x = 10;
p1.y = 20;

// 输出结构体变量的值
printf("Point: (%d, %d)\n", p1.x, p1.y);

return 0;
}

在上述示例中,使用了匿名的结构体,即在定义结构体时未指定结构体名字。然后,我们使用typedef关键字将这个匿名结构体定义的结构体类型起一个别名Point

使用tpedef定义联合体别名和定义结构体别名语法完全相同,这里就不再举例子了,下面在列举一个定义枚举别名的例子,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

typedef enum
{
RED,
GREEN,
BLUE
} Color;

int main()
{
// 使用枚举别名创建变量
Color c = RED;

// 输出枚举变量的值
switch (c) {
case RED:
printf("Color: RED\n");
break;
case GREEN:
printf("Color: GREEN\n");
break;
case BLUE:
printf("Color: BLUE\n");
break;
}

return 0;
}

在上述示例中,我们使用typedef关键字将enum定义的枚举类型起一个别名Color。枚举类型中包含了三个枚举常量REDGREENBLUE

通过使用typedef来定义结构体的别名,可以使代码更加简洁和可读性更高,特别是在结构体类型较为复杂或频繁使用时。

3.3 函数指针

在C语言中,typedef可以用来给一个已有类型起别名,其中也包括函数指针类型。使用typedef来定义函数指针类型可以简化代码,并使其更加易读。下面是一种常见的使用方法:

1
typedef int (*FuncPtr)(int, int);

上述代码定义了一个名为FuncPtr的函数指针类型,该函数指针可以指向返回类型为int接受两个int类型参数的函数。可以根据实际需求调整返回类型和参数类型。

可以使用该函数指针类型来定义函数指针变量并赋值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

typedef int (*FuncPtr)(int, int);
int add(int a, int b)
{
return a + b;
}

int main()
{
FuncPtr ptr = add;
int result = ptr(3, 4);
return 0;
}
  • 第11行:定义函数指针变量,并将add函数的地址赋值给函数指针ptr
  • 第12行:调用函数指针ptr,相当于调用add函数

回调函数是一种常见的编程技术,它允许你将一个函数作为参数传递给另一个函数,并在需要的时候被调用。

在C语言中,回调函数通常与函数指针一起使用。你可以将一个函数的指针作为参数传递给另一个函数,在适当的时候调用该函数指针,以执行特定的操作。

下面是一个简单的示例,演示了回调函数的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

// 定义函数指针别名
typedef int (*funcPtr)(int, int);
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

int result(funcPtr func, int a, int b)
{
int res = func(a, b);
res += 100;
return res;
}

int main()
{
int res = result(add, 100, 200);
printf("Result of addition: %d\n", res);
res = result(subtract, 500, 200);
printf("Result of subtraction: %d\n", res);

return 0;
}

通过使用回调函数,你可以将特定的操作从一个函数中分离出来,并通过回调函数的方式动态地执行这些操作。这使得代码更加灵活和可扩展。