结构体是一种复合数据类型,结构体将不同的数据组合成一个整体的自定义数据类型,它可以包含不同的类型成员变量,整型、浮点型、字符型等这些成员按照一定的顺序存储在内存中,每个成员都有对应的内存地址和大小。
结构体的定义通过 struct
关键字,和大括号 {};
定义结构体。
在C语言中结构体的格式如下:
struct tag//结构体名
{
数据类型 成员名;
数据类型 成员名;
……
};
==例1:==使用结构体定义了一个学生 Student
类型的变量,这个变量包含一个学生的基础信息。
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
};
==例2:==使用结构体定义了一个链表
struct NodeList
{
int val;
struct NodeList* next;
};
==例3:==定义了一个结构的同时,声明了一个结构体变量。stu1和结构体指针stu2是全局变量。
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
}stu1, *stu2;
关键字 struct
和结构体名称,放在一起是一个类型名 struct Student
,后面跟上变量名即可,初始化的方法于数组类似使用花括号({})将内容包裹在一起。
例1:
使用大括号,在声明的时候,按照结构体成员变量一一赋值
struct Student stu1 = {2024, xiaoli, 18};
例2:
也可以先声明,在赋值
struct Student stu1;
stu1.name = zhangsan;
例1:
若每次使用结构体类型的变量,感觉类型名过长,这里可以使用 tepedef
关键字对类型进行重命名。
可以在结构体定义时重命名:将 struct Student
重命名为 Student
typedef struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
}Student;
例2:
在结构体定义后重命名:将 struct Student
重命名为 Student
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
};
typedef struct Student Student;
例3:
以下这种使用方法时错误的:
还没有执行完typedef就将Node当作结构体类型名来使用,提前使用Node将会导致编译器报错,这是不被允许的。前后顺序混乱。
typedef struct NodeList
{
int val;
Node* next;
}Node;
结构体里还可以引用自己,但只能自引用指针类型的。
struct NodeList
{
int val;
struct NodeList* next;
};
若不是指针类型的,结构体的大小将无法计算,sizeof(struct NodeList)
,结构体里包含着一个同类型的结构体变量,结构体大小将会膨胀,无穷大。
struct NodeList
{
int val;
struct NodeList next;
};
结构体嵌套,结构体里还可以定义结构体。
访问结构体通过 点运算符(.
)、箭头运算符(->
)进行访问。
点运算符(.
),用于对结构体成员的直接访问,是双目操作符。使用方法:结构体变量名.成员名
struct point//声明结构体变量
{
int x;
int y;
};
int main()
{
struct point p1 = { 10, 20 };//初始化
printf("%d %d\n", p1.x, p1.y);
struct point p = { p.x = 10, p.y = 5 };//指定顺序初始化
printf("%d %d", p.x, p.y); //访问结构体内的变量
return 0;
}
箭头运算符(->
),用于间接访问结构成员。对结构体指针p使用 (->
)进行访问,还可以赋值
struct stu
{
char name[20];//姓名
int age;//年龄
int num;//学号
};
int main()
{
struct stu s = { "wangwu", 19, 202405027};
struct stu* p = &s;
printf("%s %d %d\n", p -> name, p -> age, p -> num);
return 0;
}
例1:
匿名结构体,在对结构体初始化是并未进行命名。想要使用该结构体只能在声明结构体的同时声明一个对应的结构体变量。而在后续使用该结构体类型时,也只能使用这几个变量,无法重新声明。
struct
{
int x;
int y;
}N1, N2, N3;
这里定义了一个存放坐标的结构体,使用N1,N2,N3来存储x y,但也只能用N1、N2、N3这三个坐标。
int main()
{
printf("请输入x,y的坐标: ");
scanf("%d %d", &N1.x, &N1.y);
printf("%d %d\n", N1.x, N1.y);
N2 = N1;//同类型结构体 赋值。
printf("%d %d\n", N2.x, N2.y);
return 0;
}
例2:
这两个结构体属于同种类型的结构体吗?
struct
{
int x;
int y;
}N1;
struct
{
int x;
int y;
}N12;
int main()
{
printf("请输入x,y的坐标: ");
scanf("%d %d", &N1.x, &N1.y);
N2 = N1;// error C2440: “=”: 无法从“”转换为“”
printf("N1:%d %d\n", N1.x, N1.y);
printf("N2:%d %d\n", N2.x, N2.y);
return 0;
}
虽然这两个你匿名结构体的成员一致,但它们在编译器眼里,并不是同一种结构体,两者无关联,也就无法赋值。
offsetof
— 宏,用于计算结构体成员相较于结构体变量起始位置的偏移量。
结构体的偏移量(offset)是指从结构体的起始地址开始,到特定成员的起始地址的距离。
offsetof(type, member)
//头文件--<stddef.h>
//返回值--偏移量
//返回类型--无符号整形
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("S1:\n");
printf("%zd\n", offsetof(struct S1, c1));
printf("%zd\n", offsetof(struct S1, i));
printf("%zd\n", offsetof(struct S1, c2));
printf("S2:\n");
printf("%zd\n", offsetof(struct S2, c1));
printf("%zd\n", offsetof(struct S2, c2));
printf("%zd\n", offsetof(struct S2, i));
return 0;
}
S2
如图:根据结构体S2在内存中的偏移量,可以的出结构体S2在内存中所占字节个数。c1、c2是字符类型,占一个字节,i为整型类型,占4个字节。
根据偏移量得出,成员c1从0的位置开始向后占1个字节,成员c2从1的位置开始向后占1个字节,成员 i 从4的位置开始向后占4个字节。计算的出该结构体占8个字节。根据上图可以发现,S2结构体浪费了2个字节的空间。
S1
如图:根据结构体S1在内存中的偏移量,可以的出结构体S1在内存中所占字节个数。
根据偏移量得出,成员c1
从0的位置开始向后占1个字节,成员i
从4的位置开始向后占4个字节,成员c1
从8的位置开始向后占1个字节。看似S1
结构体占9个字节大小,实际上该结构体占12个字节。而且还浪费了6个字节大小的空间。
出现上述问题的,是因为结构体成员的存在着对齐现象。
上述结构体S1,更具对齐规则:
练习:计算结构体S3、S4的大小。
struct S3
{
double d;
char c;
int i;
};//16字节
struct S4
{
char c1;
struct S3 s3;
double d;
};//32字节
平台原因(移植):
并不是所有硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些待定类型的数据,否则会抛出硬件异常。
性能原因:
访问未对齐的内存,处理器需要作两次访问,而对齐的内存仅需依次访问。结构体的内存对齐是那空间换时间的做法
现在在32位的机器上,它每次读取内存只能读取4个字节
struct S
{
char c;
int n;
}
未对齐的情况下。32为机器一次读取4个字节,第一次读取四个字节,int还剩1个字节没有读取,读取完int需要读取两次。
对齐的情况下。读取int只需要读取一次。
设计结构体时,既要内存对齐,又要节省空间,可以这样做:
struct S1
{
char c1;
int i;
char c2;
};//12字节
struct S2
{
char c1;
char c2;
int i;
};//8字节
使用 #pragma
预处理指令,可以修改vs编译器的默认对齐数。一般设置为2的n次方
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//重置默认对齐数,恢复成8
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
结构体传参,有两种形式:传地址、传参。
#include <stdio.h>
struct S
{
int arr[1000];
int n;
};
void print1(struct S a)
{
for (int i = 0; i < 10; i++)
{
printf("%d", a.arr[i]);
}
printf("\n");
printf("%d", a.n);
}
void print2(struct S* pa)
{
for (int i = 0; i < 10; i++)
{
printf("%d", pa->arr[i]);
}
printf("\n");
printf("%d", pa->n);
}
int main()
{
struct S a = { {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 10 };
print1(a);
print2(&a);
return 0;
}
这里当然是print2效能上更好,调用print1函数,在为函数开辟栈帧时,为结构体占用字节较大,开辟的一千多个整形大小的空间,而print2不需要,它只需要开辟一个整形指针大小的空间即可。
结构体传参的时候,要传结构体的地址。
位段的声明和结构体时相似的。
int unsigned int 或 signed int
,在C99中位段成员的类型也可以选择其它类型。
struct S1
{
int n;
int m;
int i;
};
struct S2
{
int _n : 2;//占2个比特位
int _m : 4;//占4个比特位
int _i : 30;//占30个比特位
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
位段的实现,本质上是节省空间,将12个字节大小的节省到8个字节。
位段可能会受到编译器的内存对齐规则的影响,导致实际占用的内存可能比位段的总位数更多,使用位段给了2个字节左右的空间,它的大小却是8个字节。
例:
struct S
{
int _a : 3;
int _b : 4;
int _c : 5;
int _d : 4;
};
int main()
{
struct S s;
s._a = 9;
s._a = 12;
s._c = 5;
s._d = 2;
printf("%zd\n", sizeof(struct S));
return 0;
}
一共3个字节大小。0
而存放值的时候,空间不够从最高位开始舍弃,多余的部分补0。
在main函数中,需要将9、12、5、2的值存放的对应的变量中,首先将它们转换为二进制位,再存入其中。
9:1001 12:1100 5:0101 2:0010
int
位段被当为有符号数还是无符号数是不确定的。根结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但存在跨平台的问题。
位段的几个成员共有一个字节,内存中为每一个字节分配地址,而比特位不分配,也就是说,位段部分成员是不存在地址的,也就不可以使用取地址操作符(&),使用scanf对位段成员输入值。
位段的实际运用,目前的级别还接触不了。
节大小。0
而存放值的时候,空间不够从最高位开始舍弃,多余的部分补0。
在main函数中,需要将9、12、5、2的值存放的对应的变量中,首先将它们转换为二进制位,再存入其中。
9:1001 12:1100 5:0101 2:0010
[外链图片转存中…(img-Ox3GvsSd-1723216315121)]
int
位段被当为有符号数还是无符号数是不确定的。根结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但存在跨平台的问题。
位段的几个成员共有一个字节,内存中为每一个字节分配地址,而比特位不分配,也就是说,位段部分成员是不存在地址的,也就不可以使用取地址操作符(&),使用scanf对位段成员输入值。
位段的实际运用,目前的级别还接触不了。