1. 结构体
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。(这里我们可以联想到数组,数组是一组相同类型元素的集合)
1.1 结构体的声明
struct tag //tag是结构体标签名,根据实际情况来定
{
member-list; //成员列表,里面每一个叫成员变量
}variable-list; //变量列表,可以省略根据需要定义,分号一定要有
例如用结构体描述一个学生类型:
struct Stu //Stu就是标签名
{
char name[20]; //姓名
int age; //年龄
char id[20]; //学号
}s1,s2,s3; //在创建时定义三个结构体变量s1,s2,s3,注意分号不要丢
这里的是s1,s2,s3可以不直接创建,我们还可以在定义好结构体后再创建,如
struct Stu s1,s2,s3; //和上面效果一样
除此之外,结构体还有特殊的声明,那就是在声明结构体的时候,不完全声明,这种结构被称为匿名结构体类型。比如:
struct //省略了标签名
{
int a;
char b;
}x; //由于省略了标签名,这里只能直接在后面创建变量
struct
{
int a;
char b;
}*p; //定义一个该结构体的指针,可以存放该结构体变量的地址
问:上面两个结构体类型成员一样,可以*p=&x;吗?
答:不可以,编译器不会认为这两个结构体是一种类型,会把它们当成两种结构体类型,不建议使用这种类型,更好的方法是写上标签名
1.2 结构体内存对齐(结构体大小计算)
结构体内存对齐规则
1.结构体的第一个成员放在相较于结构体变量起始位置的偏移量为0的位置
2.第二个成员开始,后面的每个成员都要对齐到某个对齐数的整数倍处
对齐数:每个结构体成员自身大小和默认对齐数的最小值
在有些编译器上会有默认对齐数,例如VS上默认对齐数是8,而gcc编译器上没有默认对齐数,那么直接取成员对齐数的最小值
3.结构体的总大小:必须是最大对齐数的整数倍
最大对齐数:所有成员对齐数的最大值
4.如果结构体内嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面通过两个例子来理解:
计算该结构体类型大小
struct S1
{
char c1;
int i;
char c2;
};
答:12字节,由图可以知道会有一部分浪费的空间(棕色部分)
printf(“%d”, sizeof(struct S1)); //我们也可以用这个语句打印出来该结构体的大小
第二个题:同样计算该结构体的大小
struct S2
{
char c1;
char c2;
int i;
};
答:8字节
S1和S2类型的成员一模一样,但是他们所占的空间却不一样,所以我们在设计结构体的时候,既要满足对其,也要尽量节省空间,让占用空间小的成员尽量集中在一起,就会更加节省空间。
另外,对于数组,例如int arr[3];我们只需要把它当成三个int型变量放进内存即可,计算方式一样。
若是对齐方式不合适时,我们也可以自己更改默认对齐数,使用如下语句即可
#pragma pack(1); //设置默认对齐数为1,一般我们都设置成2n
#pragma pack(); //取消设置的默认对齐数,还原为编译器上的默认值
若默认对齐数为1,意味着什么?说明不需要再对齐,直接连续存放即可,可以当成没有对齐数了
为什么会有内存对齐?
查阅到相关资料,有两个原因
(1)平台原因:并不是所有的硬件平台都能够在任意地址上访问任意类型的数据。某些硬件平台对于数据的访问有特定的要求,即数据必须存储在某些特定的地址上,如果数据没有正确对齐,则可能会导致硬件异常或者降低性能。例如,一些处理器要求某些类型的数据必须对齐到特定的边界上(如2字节边界、4字节边界等),否则可能会触发错误或警告。
(2)性能原因:为了提高数据访问的速度,现代处理器通常支持在自然边界上对齐的数据的快速访问。如果数据不对齐,处理器可能需要多次内存访问才能完成一次数据读取或写入操作,这降低了处理速度。相反,如果数据在自然边界上对齐,处理器可以一次性完成数据的读取或写入,提高了访问效率。
对于性能原因
假设有char型变量c和int型变量i在内存中如图所示
对于32位机器,字长就是32位,字长决定了一次读写数据是32bit,也就是4个字节(一字节等于8bit)
1.3 结构体的自引用
如何在结构体中包含一个类型为该结构体本身的成员?
方法一:
struct Node
{
int data;
struct Node next;
};
方法二:
struct Node
{
int data;
struct Node*next;
};
问:上面哪种方法可行?若可行,该结构体类型大小如何如何计算?
答:方法二可行,对于方法一,无法算出第二个成员next的大小,next是一个struct Node类型,一个next里面包含data和next,而所包含的这个next又包含一个data和next,由此下去无穷无尽,无法计算出next成员的大小,而方法二中,next变量是一个指向struct Node变量的指针,我们知道指针的大小为4或8个字节,是可以计算的。
总结:结构体类型里想包含同类型结构体变量,应该用同类型的结构体指针,指针指向需要找到的下一个结构体变量。
有时我们觉得结构体类型名太复杂,用起来不方便,可以使用关键字typedef来重定义类型名
typedef struct S1
{
char c1;
int i;
char c2;
}S1; //当有了typedef后,这里的S1就不是定义的结构体变量,而是重新定义的结构体类型名
S1 stu1,stu2,stu3; //重定义后便可以直接使用S1来定义变量
下面这个代码正确吗?
typedef struct Node
{
int data;
Node* next;
}Node;
答:错误,不能提前使用Node,只能在定义好之后使用
1.4 结构体的定义和初始化
struct SN
{
char c;
int i;
}sn1={‘q’,100}, sn2={.i=200,.c=‘w’}; //全局变量
//sn1是直接按照结构体成员的顺序初始化,而sn2是不按照顺序,两种方法都可以
int main()
{
struct SN sn3,sn4; //局部变量,sn3,sn4没有初始化
printf(“%c %d”, sn2.c, sn2.i); //打印格式
return 0;
}
结构体嵌套的初始化
struct SN
{
char c;
int i;
}sn1={‘q’,100}, sn2={.i=200,.c=‘w’};
struct S
{
double d;
strucr SN sn; //一个结构体类型里包含另一个结构体类型成员
int arr[3];
int main()
{
struct S s={ 3.14, {‘a’,99}, {1,2,3} };
printf(“%lf %c %d\n”, s.d, s.sn.c, s.sn.i);
int i=0;
for(i=0;i<3;i++)
{
printf("%d ",s.arr[i]);
}
return 0;
}
1.5 结构体传参
我们知道向函数传参有值传递和地址传递,若想把结构体传递给函数我们选择哪一种方式呢?是直接传递结构体变量还是传结构体变量的地址?
struct S
{
int num;
char c;
}s={5, ‘a’};
void print1(struct S s) //结构体变量传参,接收结构体变量,这里的s是形参
{
printf(“%d\n”.s.num);
}
void print2(struct S* p) //结构体地址传参,这里的p是结构体指针变量
{
printf(“%d\n”,p->num);
}
int main()
{
print1(s); //传结构体变量
print2(&s); //传结构体变量的地址
return 0;
}
上面两种传参方式都可以,但是第二种方式更好。若直接传变量,函数会使用同类型的形参来接收,而形参是实参的临时拷贝,也就是说函数会在内存上开辟一块空间来拷贝s变量,这个过程既需要时间也需要空间,若结构体过大,系统的开销也会更大,可能会导致性能的下降。若传地址,就不需要开辟大量的空间,直接根据地址去找到原结构体的位置进行操作。
总结:结构体传参的时候,要传结构体的地址
以上就是结构体的全部内容,还有枚举和联合的相关内容,未完待续…