自定义类型:结构体
在日常生活中,常常会遇到比较复杂的事务,而简单的使用C语言提供的内置类型:int、char、float等类型不足以描述,比如当我们描述一个学生时,需要描述姓名、年龄、学号等等
1.结构体类型
1.1 结构的声明
结构体的原型如下:
struct tag//类型名
{
member-list;//成员列表
}variable-list;//变量列表(是全局变量)
例如:描述一个学生
struct Stu
{
char name[20];
int age;
int id[10];
};//分号不要丢!!!
- 其中struct Stu是该结构体的类型名,就像int、char一样是个类型,只不过结构体的类型是自定义的
- 结构体内的是成员变量,就是一个复杂个体的细分,用来描述复杂个体,成员变量是可以用不同类型的变量
- 而最后面的变量列表可以不列出,在需要的时候再定义
1.2 结构体成员的定义和初始化
下面以实例来演示结构体成员的定义和初始化
struct Stu1
{
char name[20];
int age;
int id[10];
}s1 = {"zhangsan", 20, 453789};
//一般情况下,需按照成员的顺序依次进行初始化
struct Stu1 s2 = { "lisi", 18, 5793168 };
//如果想乱序进行初始化,可以用成员访问符来指认将哪个成员进行初始化
struct Stu1 s3 = { .age = 35, .id = 861753, .name = "wangwu" };
struct Stu2
{
char name[20];
int age;
struct Stu2* next;//指针
struct Stu1;//嵌套结构体
}s4 = { "haha", 36, NULL, {"hehe", 20, 4586317} };
1.3 结构体成员访问操作符
结构体里有很多的成员,如果我们需要将他们或他们中的一部分取出来用的时候,该怎么做呢?这时候就需要用到了结构体成员访问操作符
1.3.1 直接访问
结构体直接访问操作符为“.”,通过“.”我们可以直接访问到结构里的某一个成员,使⽤⽅式:结构体变量.成员名
比如:
#include <stdio.h>
struct Stu1
{
char name[20];
int age;
int id[10];
};
int main()
{
struct Stu1 s = { "zhangsan", 20, 861753 };
printf("%s\n", s.name);
printf("%d\n", s.age);
return 0;
}
1.3.2 间接访问
同样的,我们可以用指针的形式来间接访问结构体里的成员,这时就需要用到“->”操作符,使用方式:指向结构体变量的指针->成员
比如:
#include <stdio.h>
struct Stu1
{
char name[20];
int age;
int id[10];
};
int main()
{
struct Stu1 s = { "zhangsan", 20, 861753 };
struct Stu1* ps = &s;
printf("%s\n", ps->name);
printf("%d\n", ps->age);
return 0;
}
1.4 匿名结构体
什么是匿名结构体呢?匿名的又是什么呢?匿名结构体有什么用途吗?
我们先来看看匿名结构体长什么样
struct
{
char name[20];
int age;
int id[10];
}s1;
这就是匿名结构体,有发现它与一般的结构体有什么区别吗?我们不难发现匿名结构体没有标签名(tag),这就意味着,结构体类型不完整,不能在后续使用该结构体,如果要用匿名结构体定义变量时,只能在结构体末尾处定义,就像上面定义s1一样。所以,如果没有用typedef将结构体重命名的话,该匿名结构体类型只能使用这一次
struct s2 = { "zhangsan", 16, 915765 };
而s2定义方式就是错误的!
1.5 结构的自引用
如果在结构里包含一个类型为结构体类型的成员,这样做可以吗?
比如,定义一个链表的节点:
struct Node
{
int data;
struct Node next;
};
其实这样是不可以的,结构体中不能包含自身类型的成员变量,因为这样会导致结构体的大小无限增长,编译器无法确定结构体的大小。如果想实现链表,应该使用指针来指向下一节点,而不是直接包含下一个节点。正确的代码如下:
struct Node
{
int data;//当前节点的数据
struct Node* next;//下一节点的地址
};
画图来演示一下该链表:
基本形式:
串起来:
typedef重命名自引用类型
除此之外,使用typedef重命名自引用结构体类型,也容易引起问题
分析下面的代码,可行吗?
typedef struct
{
int data;
Node* next;
}Node;
答案是不可行的,这是因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用了Node类型来创建了成员变量,这是不可行的!
因此, 匿名结构体是不能实现这种的结构体自引用的效果的!!!
正确的使用方法:
typedef struct Node
{
int data;
struct Node* next;
}Node;
注:typedef重命名结构体时的典型错误分析
错误示例:
typedef SL sl;
typedef struct SeqList
{
int a;
}SL;
分析:在这段代码中,结构体SeqList被定义在类型别名SL之后,这样在定义sl类型时,编译器并不知道SL是什么。这可能导致编译器报错或警告。
正确示例:
- 在结构体定义后再重命名结构体
typedef struct SeqList
{
int a;
}SL;
typedef SL sl;
- 写原始结构体名,有了struct,typedef就会知道重命名的是结构体
typedef struct SeqList sl;
typedef struct SeqList
{
int a;
}SL;
2. 结构体的内存对齐
当我们了解了结构体的基本概念后,在日常使用中我们还会使用到结构体的大小。这时,我们需要先了解结构体的内存对齐,才能计算出结构体的大小
结构体内存对齐规则:
- 结构体的第一个成员必须对齐到结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到它的对齐数的整数倍数处
对齐数—编译器默认的对齐数和该成员类型大小的较小值
在VS中,默认对齐数是8 - 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数,最大对齐数就是所有对齐数中最大的)的整数倍
- 如果嵌套了结构体,那么它的对齐数就是——嵌套结构体里各成员中最大的对齐数
这些概念看起来都是比较模糊的,下面我们用实例和画图来加深对概念的理解
例题1
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("S1:%zd\n", sizeof(struct S1));
return 0;
}
分析:
- c1是第一个成员变量,从编号为0的位置开始往下存储,又因为是char类型,占一个字节
- i是int类型,大小为4,编译器默认对齐数为8,两者较小值为4,因此i的对齐数为4,所以i的起始位置在为4的整数倍处,即从编号为4的位置开始存储,又因为i是int类型,占四个字节
- c2是char类型,大小为1,编译器默认对齐数为8,两者较小值为1,因此c2的对齐数为1,所以c2的起始位置在为1的整数倍处,即从编号为8的位置开始,又因为c2是char类型,占一个字节
- 各成员变量“存储”完成后,看一共占了几个字节,看是否为最大对齐数的整数倍,在这个结构体中,各成员变量对齐数分别为1、4、1,最大对齐数为4,原共占9个字节,不是最大对齐数的整数倍,那么继续往后占用,直至编号为11的位置,这时一共占了12个字节,为最大对齐数4的整数倍
注: 为了方便讲解,上述的“编号”都对应于概念中的“距初始位置的偏移量”
图示如下:
故结果为12
例题2
#include <stdio.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("S1:%zd\n", sizeof(struct S2));
return 0;
}
运行结果为8,请读者拿起笔画表来演算
例题3
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("S1:%zd\n", sizeof(struct S3));
return 0;
}
再来演算一道,运行结果为16
图示:
例题4
下面我们来看看有嵌套结构体的例子
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("S1:%zd\n", sizeof(struct S4));
return 0;
}
分析:
- 由例2可知,结构体S3的大小为16,结构体中S3的最大对齐数为double类型——为8,所以struct S3的对齐数为8
图示:
故结果为32
在设计结构体时,我们可以将占用空间小的成员集中在一起,这样既能满足对齐,又能节省空间
修改默认对齐数
我们在前面了解到了不同地编译器,它的默认对齐数是不一样的,例如:在VS中,默认对齐数是8,如果结构体的对齐方式不合适的时候,我们可以自己修改它的默认对齐数,这时就需要用到 #progma 这个预处理指令
例如:
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
这时的运行结果就不再是12了,而是6了!
3. 结构体实现位段
位段的声明
位段的声明和结构体是类似的,但是又与结构体有以下的区别:
- 位段成员的类型必须是int、signed int、unsigned int、char
- 位段成员后面有一个冒号和数字,而这个数字就是要保存数字的二进制位的位数
- 位段的空间是按照需要以4个字节(int)或者1个字节(char类型)的方式来开辟的
例如:
struct A
{
int a : 2;
int b : 5;
int c : 8;
};
计算位段的大小
了解位段的概念后,那么位段的大小是如何计算的呢?是否与结构类似呢?同样地,我们带着疑问,通过实例和图示来分析。下面以VS编译器环境下为例
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd\n", sizeof(struct S));
return 0;
}
图示:
由上图可知,结构体一共占据了三个方框,也就是3个字节,故代码运行结果应该为3!
位段的跨平台问题
在前面的例子中,是在VS编译器下执行得到的结果,但是在不同的编译器下,运行结果可能不同,这是因为位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目是不能确定的。(16位机器最大是16,32位机器最大是32,写成27,在16位机器会出问题)
- 位段中的成员在内存中是从左向右分配,还是从右向左分配
- 当一个结构包含两个位段时,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
在VS编译器底下,位段中的成员在内存中从左向右分配的;当一个结构包含两个位段时,第二个位段成员比较大,无法容纳第一个位段剩余的位时,舍弃剩余的位
位段的不可取地址性
由于位段中一个字节可能存在好几个成员,而内存中每一个字节分配一个地址,并且一个字节内部的比特位是没有地址的,这时就会存在部分成员没有地址,因此位段是不能进行取地址操作的!
那么,如果我们想给位段里的成员通过scanf输入值,该怎么办呢?这时,我们可以通过间接赋值来给各成员输值
例如:
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
int temp = 0;
scanf("%d", &temp);
s.a = temp;//通过temp间接给位段成员输值
printf("%d\n", s.a);
return 0;
}