前言
今天这篇文章,我们来学习自定义类型中的结构体类型
之前我们就初步了解过结构体类型,知道他是用来描述复杂类型的
像之前的short、int、long之类的称为C语言的内置类型
而如结构体、枚举、联合类型称为自定义类型
初识结构体
在正式学习前,我们先来回忆一下之前的知识
定义
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
结构体类型的声明
struct tag { //struct是关键字,tag是自定义的标签
member - list //成员列表
}variable - list;//变量列表
例子
struct Stu {
char name[20];
int age;
};//;不可以省略
结构体变量的创建
方式一
struct Stu {
char name[20];
int age;
}s3, s4, s5;
struct Stu s6;
int main()
{
struct Stu s1;
struct Stu s2;
return 0;
}
注意
s3, s4, s5,s6都是全局结构体变量
方式二:使用typedef简化
使用typedef将node重新命名为node
typedef struct node
{
char arr[20];
struct node* next;
}node;
创建变量的方式就有两种了
int main()
{
struct node s1;
node s2;
return 0;
}
特殊的创建方式
匿名结构体类型(不建议这么使用)
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
这种创建方式,只能创建全局变量,不能创建局部变量,因为这个结构体没有标签(名字)
注意1
当尝试使用p = &a进行赋值时,程序会报错
因为二者的类型不同,即他们虽然都隐去了标签,但编译器认为他们是属于不同的类型
注意2
当使用匿名结构体类型时,是无法使用typedef进行简化的
结构的自引用
引用自己的变量(错误)
不可以在结构体的成员列表处创建自己的结构体变量,如下
struct node
{
char arr[20];
struct node n;
};
这跟函数递归一点关系都没有,这么写是错误的
引用指针(正确)
正确的方式是引用一个地址,方便找到下一个元素存储在哪里
(就是链表的实现方式)
struct node
{
char arr[20];
struct node* next;
};
结构体变量的定义和初始化
示例
struct S
{
char c;
int a;
double d;
char arr[20];
};
int main()
{
struct S s = { 'c', 100, 3.14,"hello world"};
printf("%c %d %lf %s\n", s.c, s.a, s.d, s.arr);
return 0;
}
结构体嵌套初始化
结构体嵌套(结构体包含结构体的)初始化方式如下
要使用{}
struct T
{
int age;
double weight;
};
struct S
{
char c;
int a;
double d;
char arr[20];
struct T st;
};
int main()
{
struct S s = { 'c', 100, 3.14,"hello world", {20, 80.0} };
printf("%d\n", s.st.age);
return 0;
}
结构体内存对齐
计算结构体内存大小
引入
下面代码的结果是什么
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
int a;
char c1;
char c2;
};
int main()
{
struct S1 s1 = { 0 };
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
return 0;
}
运行结果
这是为什么?下面我们就来讲解它
对齐规则
代码就是上面“引入”中的代码
1
第一个成员在与结构体变量偏移量为0的地址处。
意思就是:结构体的第一个成员就存储在结构体变量所处的地址处
偏移量为0,就是重合
2
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
gcc中无明确默认值
先解释对齐数
以a作为例子,a为整型,大小是4,小于8,所以对齐数就是4
再解释整数倍
对齐数是4,那a这个变量就应该存储在4的倍数的地址处,
意思就是:从4的倍数的地址处开始存储,前面的就空着,不存储
那么根据前两条规则,结构体S1的大小是9,输出结果是12,那这是为什么呢
3
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
以结构体变量s1为例,最大对齐数是4,所以就再浪费三个字节的空间,将大小扩展到12个字节
用前三条规则也可以得出s2的大小是8个字节
4
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
在“例题2”处,会做详细解释
例题1
下面结构体变量的大小是多少
struct S3
{
double d;
char c;
int i;
};
根据前三条规则可以轻易得出:
8+1+3+4 == 16
大小就是16个字节
例题2
下面结构体的大小是多少
struct S4
{
char c1;
struct S3 s3;
double d;
};
这里就需要用到第四条规则了:结构体嵌套
s3根据上一题可知,字节大小是16,
存储开始位置:嵌套结构体(此处就是s3)自己的最大对齐数的整数倍处
对于s3来说,最大的就是double的8个字节,也就是从8的整数倍处开始存储
并且结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
所以,变量s4大小就是:
1+7+16+8 == 32
内存对齐存在的原因(了解即可)
用空间换时间
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
比如:32位的机器,一次可以读取4个字节的数据,如果不对齐,那就可能分两次读取,如果对齐,读取一次就够了
改进
所以为了节省空间和满足对齐条件
我们在创建结构体变量的时候,应该像“引入”中的变量s2一样:让占用空间小的成员尽量集中在一起
修改默认对齐数
使用预处理指令
#pragma
设置默认对齐位为4
#pragma pack(4)
取消设置的默认对齐位
#pragma pack()
结构体传参
注意:
结构体传参,分为两种:传值调用、传址调用
不修改变量成员可以使用传值调用和传址调用,建议使用传址调用,因为传过去的是地址,字节大小是固定的
要修改成员(如,初始化)就不能使用传值调用,只能传址调用
传值调用
struct S
{
int data[100];
int num;
};
void print1(struct S tmp)
{
printf("%d\n", tmp.num);
}
int main()
{
struct S s = { {1,2,3}, 100 };
print1(s);
print2(&s);
return 0;
}
传址调用
如果担心ps指向的对象被修改时,只需要用const修饰即可
struct S
{
int data[100];
int num;
};
void print2(const struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3}, 100 };
print1(s);
print2(&s);
return 0;
}
原因(网上搜的)
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。
结语
结构体的初步介绍就到这里了,希望对你有帮助
下一篇文章我们会学习位段,我们下次见~