在之前我们已经探讨了C语言中的自定义数据类型和数组,链接如下:C语言中的数据类型(上)_c语言数据类型-优快云博客
目录
接下来我们来探讨C语言中的自定义数据类型结构体。
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚⾄是其他结构体。
一、结构体的声明
结构体的声明如下所示:
struct tag{member- list1 ;member- list2 ;......}variable- list ;
例如,描述一个学生:
struct Stu{char name[ 20 ]; // 名字int age; // 年龄char sex[ 5 ]; // 性别char id[ 20 ]; // 学号}; // 分号不能丢
二、结构体变量的定义和初始化
结构体变量的定义有两种方式:
第一种方式是在声明结构体的时候创建变量,如下所示:
struct Point // 类型声明{int x;int y;}p1; // 声明类型的同时定义变量 p1struct Stu // 类型声明{char name[ 15 ]; // 名字int age; // 年龄}s1;
第二种方式是使用结构体类型来创建,如下所示:
struct Point p2; //定义结构体变量p2
struct Stu s1;
结构体变量的初始化同样有两种方式:
第一种方式是创建变量的时候按照顺序初始化,如下所示:
struct Point p3 = { 10 , 20 };struct Stu s1 = { "zhangsan" , 20 };
第二种方式是创建变量的时候用 “ . ” 这个符号来制定变量来初始化,如下所示:
struct Stu s2 = {.age= 20 , .name= "lisi" }; // 指定顺序初始化
结构体是可以嵌套使用的,如下所示:
struct Node{int data;struct Point p ;struct Node * next ;}n1 = { 10 , { 4 , 5 }, NULL }; // 结构体嵌套初始化struct Node n2 = { 20 , { 5 , 6 }, NULL }; // 结构体嵌套初始化
三、结构体成员的访问
3.1 结构体成员的直接访问
# include <stdio.h>struct Point{int x;int y;}p = { 1 , 2 };int main (){printf ( "x: %d y: %d\n" , p.x, p.y);return 0 ;}
使用方式:结构体变量.成员名
3.2 结构体成员的间接访问
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针。如下所示:
# include <stdio.h>struct Point{int x;int y;};int main (){struct Point p = { 3 , 4 };struct Point * ptr = &p;ptr->x = 10 ;ptr->y = 20 ;printf ( "x = %d y = %d\n" , ptr->x, ptr->y);return 0 ;}
使用方式:结构体指针->成员名
四、结构的特殊声明
struct{int a;char b;float c;}x;struct{int a;char b;float c;}a[ 20 ], *p;
// 在上⾯代码的基础上,下⾯的代码合法吗?p = &x;
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
五、结构体内存对齐
我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
这也是⼀个特别热⻔的考点: 结构体内存对⻬
5.1 对齐规则
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对齐数就是成员⾃⾝的大小
3. 结构体总大小为最⼤对齐数(结构体中每个成员变量都有⼀个对其数,所有对齐数中最⼤的)的 整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
让我们来看以下练习:
#include<stdio.h>//练习1struct S1
{
char c1;
int i;
char c2;
};
int main() {
printf("%zd\n", sizeof(struct S1));
return 0;
}


//练习2
struct S2
{
char c1;
char c2;
int i;
};
int main() {
printf("%zd\n", sizeof(struct S2));
return 0;
}


//练习3
struct S3
{
double d;
char c;
int i;
};
int main() {
printf("%d\n", sizeof(struct S3));
return 0;
}


//练习4-结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main() {
printf("%zd\n", sizeof(struct S4));
return 0;
}


5.2 为什么存在内存对齐
大部分的参考资料都是这样说的:
1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让占⽤空间⼩的成员尽量集中在⼀起,如下所示:
// 例如:struct S1{char c1; 5 int i;char c2;};struct S2{char c1;char c2;int i;};
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。
5.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。如下所示:
# include <stdio.h># pragma pack(1) // 设置默认对齐数为 1struct S{char c1;int i;char c2;};int main (){//输出的结果是什么?printf ( "%d\n" , sizeof ( struct S));return 0 ;}
结构体在对齐方式不合适的时候,我们可以⾃⼰更改默认对齐数。
六、结构体传参
让我们来看以下代码:
struct S{int data[ 1000 ];int num;};struct S s = {{ 1 , 2 , 3 , 4 }, 1000 };// 结构体传参void print1 ( struct S s){printf ( "%d\n" , s.num);}// 结构体地址传参void print2 ( struct S* ps){printf ( "%d\n" , ps->num);}int main (){print1(s); // 传结构体print2(&s); // 传地址return 0 ;}
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
七、结构体实现位段
结构体讲完就得讲讲结构体实现位段的能力。
7.1 什么是位段
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
- 位段的成员名后边有⼀个冒号和⼀个数字。
后面的数字表示的是这个变量占几个比特位。
如下所示:
struct A{int _a: 2 ;int _b: 5 ;int _c: 10 ;int _d: 30 ;};
7.2 位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
// ⼀个例⼦struct S{char a: 3 ;char b: 4 ;char c: 5 ;char d: 4 ;};struct S s = { 0 };s.a = 10 ;s.b = 12 ;s.c = 3 ;s.d = 4 ;// 空间是如何开辟的?
7.3 位段的跨平台问题
- int 位段被当成有符号数还是⽆符号数是不确定的。
- 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题)。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
7.4 位段的应用
7.5 位段使用的注意事项
struct A{int _a : 2 ;int _b : 5 ;int _c : 10 ;int _d : 30 ;};int main (){struct A sa = { 0 };scanf ( "%d" , &sa._b); // 这是错误的//正确的⽰范int b = 0 ;scanf ( "%d" , &b);sa._b = b;return 0 ;}