第十一章-自定义类型详解
结构体
结构体的基本知识在我之前C语言初阶的部分已经有详细说明过了,这里知识稍微体积之后便进入更为深层的方面。
结构体的声明
结构体声明的声明及使用是十分简单且方便的,使用以下的语法我们即可声明一个自定义的结构体。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a;
char b[1024];
}Test;
int main()
{
Test test;
test.a = 10;
strcpy(test.b, "Main");
printf("%d, %s\n", test.a, test.b);
}
[misaki@localhost test]$ ./Main
10, Main
匿名结构体
但是有一种较为特殊的结构体,这种结构体没有名称,因此它在声明结束后无法再次定义变量,只能一次性使用,我们称这种结构体为匿名结构体。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct
{
int a;
}test;
int main()
{
test.a = 10;
printf("%d\n",test.a);
}
[misaki@localhost test]$ ./Main
10
这种匿名结构体只能供我们一次性使用,声明结束后不能再定义其他的结构体变量。
结构体的自引用
结构体的自引用一般大多数用在链表,图,树等数据结构中,之后在数据结构有关章节中我们会经常用到链式存储结构,这时候就要用到我们的结构体的自引用。1
2
3
4
5
6
7
8
9
10
11
12
13#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a;
char b[10];
struct test* next;//一般用于指向下一个相同类型的结构体
}Test;
int main()
{
Test test;
}
在这个结构体的定义中我们就将结构体本身的指针包含进了结构体中,这个在链式存储中一般用于指向下一个结构体起到寻址的作用,但是我们要注意在自引用中只能包含其本身的指针,不能直接自引用,否则将是不合法的。
结构体内存对齐
这个部分是结构体中十分重要的一个知识点,在面试和笔试中也是常考点。
我们之前计算结构体大小都是将结构体各成员变量大小之和相加就得出了结构体的总大小,但是我们看接下来这个例子。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
short a;
char c2;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}
[misaki@localhost test]$ ./Main
6
从以上这个例子可以看出这个结构体占了6个字节,但是个成员加起来应该就是4个字节啊,这是为什么呢?
其实结构体大小的计算与内存对齐有着很大的关系,以下是内存对齐的基本规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 Linux中的默认值为4。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
接下来我们用以上的规则来计算一下刚才这个结构体的总大小。首先char
类型放在地址为0的地方,占一个字节。short
的大小为2,我的环境是Linux
所以默认对齐数为4,short
小于它所以short
对齐为2,于是要放在地址为2的整数倍的地方,于是舍弃1这个地址放在2处,占两个字节,此时的总大小为1 + 1(补齐)+ 2 = 4。之后还要再放一个char
同理得对齐数为1,放在任意地址处即可,于是放在5处,占一个字节。由此所有变量都放完了,但是根据规则中第三条我们还要让总大小为最大对齐数得整数倍,在这个结构体中最大对齐数为short
的对齐数,为2,于是此时末尾还要再补齐一个字节,于是总大小为1 + 1(补齐)+ 2 + 1(补齐) = 6。由此这个结构体的大小才算是真正得出。
但是为什么会出现内存对齐这个规定呢?
这是为了某些平台的原因及性能的原因,其中牵扯到更为底层的知识,为了性能和平台牺牲这点内存换来更高的效率是完全值得的。
接下来还有几个例子以供练习。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
int a;
char c2;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}
[misaki@localhost test]$ ./Main
12
1 + 3(补齐)+ 4 + 1 + 3(补齐) = 12。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
char c2;
int a;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}
[misaki@localhost test]$ ./Main
8
1 + 1 + 2(补齐) + 4 = 8。
从以上这个例子可以看出相同的成员变量就连不同的声明顺序也会导致结构体大小的不同。
当然我们也可以修改系统默认的对齐数,通过一个宏即可。
#pragma pack(对齐数)
这样即可修改默认的对齐数。
位段
位段是规定结构体中各部分成员所占空间的一种语法,由此来达到节省占用空间的效果。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a : 2;//只占两位
int b : 5;//只占五位
}Test;
int main()
{
Test test;
test.a = 3;
test.b = 31;
printf("%d,%d\n", test.a, test.b);
}
[misaki@localhost test]$ ./Main
-1,-1
我们只要给他们赋予超过存储空间的值就会出现问题。
枚举
枚举类型是一种类似于用宏定义常量的自定义类型。
枚举的定义
枚举定义与结构体的声明十分类似,不过我们要谨记中间的每个常量要用,
隔开。1
2
3
4
5
6
7
8
9
10
11
12
13#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE,
FEMALE,
UNKNOWN,
};
int main()
{
printf("MALE = %d,FEMALE = %d, UNKNOWN = %d\n", MALE, FEMALE, UNKNOWN);
}
这样我们就定义了一个枚举类型,其中的每个变量都是常量,我们可以直接使用。如果我们并没有给他们进行赋值的话他们会从上到下从0开始一次增长1自动赋值,但是如果我们给钱一个数赋值了的话它们会从前一个数的值开始依次增长1自动赋值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE = 2,
FEMALE,
UNKNOWN,
};
int main()
{
printf("MALE = %d,FEMALE = %d, UNKNOWN = %d\n", MALE, FEMALE, UNKNOWN);
}
[misaki@localhost test]$ ./Main
MALE = 2,FEMALE = 3, UNKNOWN = 4
同时我们还可以用枚举类型定义变量,这样定义的变量的值就必须是枚举常量中出现的常量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE,
FEMALE,
UNKNOWN,
};
int main()
{
enum Sex sex = MALE;
printf("sex = %d\n", sex);
}
[misaki@localhost test]$ ./Main
sex = 0
这样定义的变量就不得不使用枚举中的常量,否则会进行报错,多了一步验证。
为什么使用枚举
1、枚举类型有验证检查,更加严谨。
2、枚举类型一次可以定义多个常量,十分方便。
3、枚举类型可以防止命名污染。
联合体
联合体也叫共用体,是一种较为特殊的自定义类型。
声明与使用
联合体与结构体体定义起来的语法完全一致,但他们有一点不同的地方就在于,结构体声明了很多个成员变量,在使用的时候可以同时使用里面所有的成员变量,但联合体则更倾向于多选一,定义了很多的成员变量,但是只能使用其中一个,而且其大小也是根据其中最大的成员变量决定的,因此联合体是一个很抠门的类型,但是它也可以有效节省空间,在一些情况下有很大作用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#include <stdio.h>
#include <stdlib.h>
#include <string.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un;
printf("%lu\n",sizeof(un));
un.i = 4;
un.c = 'a';
printf("%d\n", un.i);
}
[misaki@localhost test]$ ./Main
4
97
从以上这个例子可以看出共用体所占空间的确与结构体是不一样的,并且当我们同时给两个成员变量赋值时另一个成员变量会扰乱其他成员变量的赋值,由此可见它们确实是存储在同一块内存空间上的。
其实共用体除了节省空间这一项优点上其实它有很多其它作用,可以帮助我们很方便的完成一些不用共用体就很难做到的东西,这都和它独特的特性有关。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#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
union Ip
{
uint32_t a;
struct
{
char d1;
char d2;
char d3;
char d4;
};
};
int main()
{
union Ip ip;
ip.a = 0x1;
printf("%d.%d.%d.%d\n", ip.d1,ip.d2,ip.d3,ip.d4);
}
[misaki@localhost test]$ ./Main
1.0.0.0
用这种方式则可以轻松做到ip地址间两种表示形式的转换,同时用联合体还可以做到我们之前写过的大小端的验证。