文章目录
结构体,联合体等类型是C语言诞生到目前为止的经典数据结构,使用这些结构,可以极大程度的提高我们的编程的生产力,结构体和链表有着非同一般的关系,学好结构体,后面对链表的掌握也就轻而易举。
一、结构体
数组是一组具有相同类型的数据集合。在实际开发编程中,我们往往需要一组类型不同的数据,如学生登记表,姓名为字符串,学号是整数或字符串,年龄为整数,所在班级为字符串。因为数据类型不同,所以C语言引申出了结构体的数据类型,结构体是一些值的集合,这些值被称作成员变量,成员变量可以是不同的数据类型。
1.结构体类型的声明
//结构体定义形式
strcut 结构体名
{
成员变量;
};
//结构体例子
struct student//类型声明
{
char* name;//姓名
char id[15];//学号
int age;//年龄
};
在上面我们声明了一个学生类型,它包含3个成员,分别为name,id,age。他们是不同的数据类型,结构体成员的定义方式与变量和数组的定义方式相同,但是不可以初始化。
注意:大括号后面的分号不能少,这是一个完整的语句。
2.结构体的自引用
结构体是一种可自定义的数据类型。它可以包含int,char等基础数据类型,也可以包含结构体。
struct A
{
int a;
int b;
};
struct B
{
struct A a;//a为结构体A类型
char c;
};
结构体可以包含其他结构体,结构体可以包含自身这个结构体吗?
答案是肯定的,不过要用指针来存储。
struct List
{
int val;
struct List* next;
};
我们如果不用指针存储就会无法正常开辟空间,因为在开辟空间是会计算结构体的空间大小,不用指针储存会出现套娃现象,这个结构体无法正常开辟空间。所以我们需要用结构体指针记录下一个节点的位置,通过指针来访问下一个节点,这也是链表的雏形。
3.结构体的定义和初始化
struct student
{
char* name;
char id[15];
int age;
};
struct student s = {
"张三","123456789",20 };//初始化
struct A
{
int a;
int b;
};
struct B
{
struct A a;
char c;
};
struct B b = {
{
1,2},'b' };//结构体嵌套初始化
int main()
{
printf("student1 name:%s id:%s age:%d\n", s.name, s.id, s.age);
printf("%d %d %c\n", b.a.a, b.a.b, b.c);
return 0;
}
结构体成员访问通过.(点运算符),指针形式的结构体通过->(箭头运算符)。
4.结构体内存对齐
结构体对齐规则:
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到对齐数的整数倍的地址处。
对齐数为编译器默认的对齐数与该成员大小的较小值,VS中默认为8,gcc中没有默认对齐数。。
3.结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐整数倍的地址处,结构体的整体大小就是所有最大对齐数的整数倍(含嵌套结构体的对齐数)。
下面通过3个例子深刻了解一下:
struct A
{
char a;
int b;
char c;
};
struct B
{
char a;
char c;
int b;
};
struct C
{
int b;
struct A a;
char c;
};
int main()
{
printf("A:%d\n",sizeof(struct A));
printf("B:%d\n",sizeof(struct B));
printf("C:%d\n",sizeof(struct C));
return 0;
}
如图所示:结构体A的位置在空间上对应的位置,字符a在偏移量为0的地址处。整形b的对齐数为4,与编译器默认的对齐数相比,整形字节较小,所以对齐数为4。b要到对齐数的整数倍的地址处,所以在4的位置。字符c的对齐数为1(与编译器默认的对齐数相比,字符字节较小),所以位置从8开始。结构体的总大小为最大对齐数(整形的对齐数)的整数倍,所以为12。
我们可以通过offsetof()函数来证明:
#include<stddef.h>
struct A
{
char a;
int b;
double c;
};
int main()
{
//offsetof();//计算结构体偏移的函数
printf("%d\n",offsetof(struct A,a));
printf("%d\n",offsetof(struct A,b));
printf("%d\n",offsetof(struct A,c));
return 0;
}
如图所示:
结构体B的位置在空间上对应的位置,字符a在偏移量为0的地址处。字符c的对齐数为1(与编译器默认的对齐数相比,字符字节较小),所以位置从1开始。整形b的对齐数为4(与编译器默认的对齐数相比,整形字节较小),所以对齐数为4。b要到对齐数的整数倍的地址处,所以在4的位置。结构体的总大小为最大对齐数(整形的对齐数)的整数倍,所以为8。
我们变量设置的位置的改变,可能改变结构体的大小。
结构体C与之类似操作,可以把结构体嵌套的结构体看成一个整体开辟空间,结构体总大小为所有最大对齐数的整数倍(含嵌套结构体的对齐数)。
修改默认对齐数:
#include<stddef.h>
#pragma pack(1)//设置默认对齐数为8
struct A
{
char a;
int b;
char c;
};
#pragma pack()//取消设置默认对齐数
int main()
{
printf("%d\n",offsetof(struct A,a));
printf("%d\n",offsetof(struct A,b));
printf("%d\n",offsetof(struct A,c));
printf("结构体大小:%d\n",sizeof(struct A));
return 0;
}
#pragma pack()可以修改默认对齐数,把默认对齐数修改为括号里面的对齐数,第二次#pragma pack()为恢复默认对齐数。
为什么要内存对齐:
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。因为为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体的内存对齐是拿空间换时间的做法。
5.结构体传参
结构体传参传参也有传值调用和传址调用。
struct A
{
char a;
int b;
char c;
};
void change1(struct A num)
{
num.a = 'z';
num.b = 8;
num.c = 'x';
printf("调用后:A(a):%c A(b):%d A(c):%c \n", num.a, num.b, num.c);
}
void change2(struct A* num)
{
num->a = 'z';
num->b = 8;
num->c = 'x';
printf("调用后:A(a):%c A(b):%d A(c):%c \n", num->a, num->b, num->c);
}
int main()
{
struct A num = {
'a',4,'c' };
printf("传递前:A(a):%c A(b):%d A(c):%c \n", num.a, num.b, num.c);
change1(num);//传值调用
printf("传递后:A(a):%c A(b):%d A(c):%c \n", num.a, num.b, num.c);
printf("-----------------------------------------------------------------\n");
printf("传递前:A(a):%c A(b):%d A(c):%c \n", num.a, num.b, num.c);
change2(&num);//传址调用
printf("传递后:A(a):%c A(b):%d A(c):%c \n", num.a, num.b, num.c);
return 0;
}
如果我们要修改结构体里面的值我们要进行传递址,如果我们要打印等其他操作用什么传递呢?
结构体传参时,要传结构体的地址。
原因如下:在函数传参时,参数是要压栈的,会有时间和空间上的系统开销,当传递一个结构体对象的时候,结构体过大,参数压栈的系统开销就比较大,会导致性能下降。如果我们不希望改变结构体的内容,我们可以在参数前加const。
6.结构体实现位段
什么是位段:位段的声明和结构体类似,但也有不同的地方。
1.位段的成员必须是int,unsigned int和signed int,char等。
2.位段的成员函数后边有一个冒号和一个数字。
例如:
struct A
{
int a : 4;
int b : 5;
int c : 20;
int d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
为什么是8个字节呢?
因为冒号后的数字代表占的比特位的个数,a,b,c三个总共29个比特位,加上d会大于32位,所有d单独在开一个整形字节,所以总共8个字节。
位段的内存分配:
1.位段的空间上是按照需要以4个字节(int)或一个字节(char)的方式来开辟的。
2.位段有很多不确定因素,位段是不可以跨平台的。
二、枚举
如果一个变量只有几种可能的值,则可以将其定义为枚举类型。所谓的枚举类型是把可能的值一一列举出来。变量的值只限于列举出来的枚举值的范围。
1.枚举类型的定义
enum 枚举类型
{
枚举成员列表(成员之间以逗号隔开,最后一个后面没有逗号)
};
enum WEEK //一周的枚举定义
{
MON,
TUE,
WED,
THU,
FRI,
STA,
SUN
};
在没有显示说明的情况下,第一枚举常量的值为0,第二个为1,以此类推。
2.枚举的使用
enum WEEK //一周的枚举定义
{
MON = 1,
TUE,
WED,
THU,
FRI,
STA,
SUN
};
int main()
{
enum WEEK now;
int sum = 0;
printf("请输入今天星期几\n");
scanf("%d", &sum);
switch (sum)
{
case MON:
printf("tomorrow is TUE\n");
break;
case TUE:
printf("tomorrow is WED\n");
break;
case WED:
printf("tomorrow is THU\n");
break;
case THU:
printf("tomorrow is FRI\n");
break;
case FRI:
printf("tomorrow is STA\n");
break;
case STA:
printf("tomorrow is SUN\n");
break;
case SUN:
printf("tomorrow is MON\n");
break;
}
return 0;
}
使用枚举需要注意以下几点:
1.同一枚举类型中的枚举常量名字必须互不相同。
2.不能对枚举常量进行赋值操作(定义枚举类型时除外)。
3.枚举常量可以用于判断语句
4.一个整数不能直接赋值给一个枚举常量,必须使用该枚举常量所属的枚举类型进行类型强制类型转化。
5.在使用枚举变量时,我们不关心其值的大小,而是其表示的状态。
3.枚举的优点
1.增加代码的可读性和可维护性。
2.防止命名污染(封装)
3.有类型检查,更加的严谨。
4.便于调试。
5.方便使用,一次可以定义多个常量。
三、联合体
1.联合体类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合体也叫共用体)。
例如:
union UN
{
char a;
int b;
};
int main()
{
union UN un;
printf("%d\n", sizeof(union UN));
printf("%p\n", &un);//联合体的地址
printf("%p\n", &(un.a));//联合体中a的地址
printf("%p\n", &(un.b));//联合体中b的地址
return 0;
}
从上述代码可以看出联合体的特点:共用同一块空间。
2.联合体大小的计算
1.联合体的大小至少是最大成员的大小。
2.当成员大小不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍。
总结
自定义类型是我们学习C语言中重要的内容,它们可以为我们后期实现链表等数据结构起到关键性的作用。
大家要多多实例练习。加深C语言的学习。