C语言 自定义类型---结构体(1)
目录
1.结构体类型的声明
2.结构体变量的定义和初始化
3.结构体内存对齐
1.结构体类型的声明
之前在讲解操作符的文章中,已经学习了结构体的概念,这里稍微复习一下。
1.1结构体回顾
结构是一种集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。
1.1.1结构体的声明
struct tag
{member-list;
}variable-list;
例如描述一个学生:
struct Stu
{char name[20];//名字int age;char sex[5];//性别char id[20];//学号
};//分号不能丢
2.结构体变量的定义和初始化
struct Stu
{char name[20];//名字int age;char sex[5];//性别char id[20];//学号
};int main()
{// 结构体变量的创建和初始化struct Stu s = {"张三", 20, "男", "2023081001"};printf("Name: %s\n", s.name);printf("Age: %d\n", s.age);printf("Sex: %s\n", s.sex);printf("Id: %s\n", s.id);struct Stu *ps = &s;printf("Name: %s\n", ps->name);printf("Age: %d\n", ps->age);printf("Sex: %s\n", ps->sex);printf("Id: %s\n", ps->id);return 0;
}
在这块代码的printf语句中, . 和 -> 都是用于访问结构体成员的操作符,但使用场景有所不同:
2.1点操作符(. )
• 用法:当你有一个结构体变量时,使用点操作符来访问它的成员。语法格式为 结构体变量名.成
员名 。
• 示例:假设有如下结构体定义:
struct Point {int x;int y;
};
struct Point p;
p.x = 10; // 使用点操作符给结构体变量p的成员x赋值
p.y = 20; // 使用点操作符给结构体变量p的成员y赋值
2.2箭头操作符(-> )
• 用法:当你有一个指向结构体的指针时,使用箭头操作符来访问结构体的成员。语法格式为 结构
体指针名->成员名 。它等效于 (*结构体指针名).成员名 ,本质上是先通过指针找到结构体变量,
再访问其成员。
• 示例:
struct Point {int x;int y;
};
struct Point *ptr;
struct Point p = {10, 20};
ptr = &p;
ptr->x = 30; // 使用箭头操作符通过指针ptr给结构体成员x赋值
ptr->y = 40; // 使用箭头操作符通过指针ptr给结构体成员y赋值
总的来说,. 用于直接通过结构体变量访问成员,而 -> 用于通过结构体指针访问成员,合理使用这
两个操作符能方便地操作结构体数据。
2.3结构的特殊声明
在声明结构的时候,可以不完全的声明
比如:
//匿名结构体类型
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}*p;
上面的两个结构在声明的时候省略了结构体标签(tag)。
那么问题来了:
// 在上面代码的基础上,下面的代码合法吗?
p = &x;
警告:
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名结构体类型,如果你有相同的结构体类型要声明的话,基本上要使用同一次。
2.4结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表的节点:
struct Node
{int data;struct Node next;
};
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的
大小就会无穷的大,是不合理的。
正确的自引用方式:
struct Node
{int data;struct Node* next;
};
在结构体自引用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看下面
的代码,可行吗?
typedef struct
{int data;Node* next;
}Node;
答案是不行的,因为 Node 是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内提前
使用 Node 类型来创建成员变量,这是不行的。
解决方案如下:定义结构体不要使用匿名结构体了
typedef struct Node
{int data;struct Node* next;
}Node;
3.结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点:结构体内存对齐
3.1对齐规则
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
1. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的
整数倍。
2. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
3.1.1练习
struct S1
{char c1;int i;char c2;
};
printf("%d\n", sizeof(struct S1));
结构体第一个成员 c1 是 char 类型,占1字节, 对齐到偏移量为0 的地址处, 此时已用1 字节。
第二个成员 i 是 int 类型 , 大小为4 字节 , 对齐数取编译器默认对齐数8 和int 类型大小4 中的较小
值,即4 。所以 i 要从4 的整数倍地址开始存储,前面 c1占了1 字节,因此要填充3字节,使得
i 从偏移量为4 的地址开始存储 , i 存储后 , 共占用4 + 4 = 8 字节。
第三个成员 c2 是 char 类型,占1 字节 , 对齐数为1(char 类型大小),从当前偏移量8 处存储,占
用1 字节。
最大对齐数是4 (int 类型的对齐数) , 当前总占用9 字节,要满足是最大对齐数4 的整数倍,需
再填充3 字节。
得出结果:所以 sizeof(struct S1) 的结果是12 字节。
struct S2
{char c1;char c2;int i;
};
printf("%d\n", sizeof(struct S2));
第一步:根据规则1,第一个成员c1为 char 类型,占1 字节,对齐到偏移量为0 的地址处,此时占
用1 字节。
第二步:第二个成员 c2 也是 char 类型,占1 字节,对齐数为1 ( char 类型自身大小 ),紧接在
c1 后面存储,从偏移量为1 的地址开始,此时共占用2 字节。
第三步:第三个成员 i 是 int 类型,大小为4 字节,对齐数取编译器默认对齐数8 和 int 类型大小4
中的较小值,即4 。所以 i 要从4 的整数倍地址开始存储,前面已占用2 字节,因此需填充2 字
节,使得 i 从偏移量为4 的地址开始存储, i 存储后共占用4 + 4 = 8 字节。
第四步:依据规则3,最大对齐数是4( int 类型的对齐数) , 当前总占用8 字节,8 是4 的整数倍,
无需额外填充。
结论:
所以 sizeof(struct S2) 的结果是8 字节。若运行包含上述结构体定义和 printf("%d\n",sizeof(struct
S2)); 语句的C 语言程序,会输出 8 。
3.2为什么存在内存对齐?
1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
//例如:
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
输出结果:12
8
1. 分析struct S1大小
在常见编译器(如VS , 默认对齐数为8)下,依据结构体内存对齐规则:
- 第一个成员c1是 char 类型,占1 字节,对齐到偏移量为0 的地址处 ,此时占用1 字节。
- 第二个成员 i是 int 类型,大小为4 字节 ,对齐数取编译器默认对齐数8 和 int 类型大小4 中的较
小值,即4 。所以 i 要从4 的整数倍地址开始存储,前面 c1 占了1 字节,因此要填充3 字节,使
得 i 从偏移量为4 的地址开始存储, i 存储后,共占用4 + 4 = 8 字节。
- 第三个成员 c2 是 char 类型,占1 字节,对齐数为1( char 类型大小 ),从当前偏移量8 处存
储,占用1 字节。
- 最大对齐数是4 ( int 类型的对齐数 ),当前总占用9 字节,要满足是最大对齐数4 的整数倍,
需再填充3 字节。所以 sizeof(struct S1) 为12 字节。
2. 分析 struct S2 大小
- 第一个成员 c1 为 char 类型,占1 字节,对齐到偏移量为0 的地址处,占用1 字节。
- 第二个成员 c2 也是 char 类型,占1 字节,对齐数为1 ,紧接在 c1 后面存储,从偏移量为1
的地址开始,此时共占用2 字节。
- 第三个成员 i是 int 类型,大小为4 字节,对齐数为4 。前面已占用2 字节,需填充2 字节,使
得 i 从偏移量为4 的地址开始存储, i 存储后共占用4 + 4 = 8 字节。最大对齐数是4 ,8 是4 的
整数倍,无需额外填充 。所以 sizeof(struct S2) 为8 字节。
可见,虽然 S1 和 S2 成员相同,但成员顺序不同,导致内存对齐方式有别,所占空间大小也就
不同 。
所以在设计结构体的时候,我们既要满足对齐,又要节省空间,应该:
让占用空间小的成员尽量集中在一起
在之前关于操作符的文章中,我们只是粗略的介绍了解了结构体,今天正式认识了结构体,包括结构体的声明,初始化,以及内存对齐,希望大家能理解,感谢大家的观看!