008 结构体基础与计算结构体大小——“C”
目录
一、什么是结构
二、结构体
结构体的声名
成员变量的初始化
结构体传参
三、计算结构体的大小
引入
四、位段
一、什么是结构
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
二、结构体
结构体的声名
创建类型的同时创建结构体变量
struct S //类型声名
{
int a;
char c;
}s1; //全局变量,创建类型的同时创建变量
struct S s3;//全局变量,直接拿类型创建变量
struct S——>结构体类型
S——>结构体的名字,又称标识符
s1——>结构体变量
a,c——>成员变量
创建结构体变量:
1、在已有struct S结构体类型的基础下 } (变量) ;此处的s1既为结构体变量又为全局变量
2.在结构体的外部,用结构体类型创建变量——>s3
注意:此处的 a,c是结构体的成员变量,要和结构体变量区分
使用typedef定义的名称是一种新类型的别名,它与原始类型或结构体类型具有相同的语义和行为。
typedef struct Stu
{
int age;
char *name;
} S;
上述代码定义了一个结构体,使用typedef为这个结构体类型定义了一个新的名字 S。
这里的Struct stu等价于S,两个都是结构体类型,用来创建结构体变量
如下的情况,使用typedef也是为结构体类型重新定义了一个名字S
如下的情况就会报错,因为此结构体没有类型名称,也没有typedef重新为此结构体定义一个结构体类型名称,因此这里的大S只是结构体变量,不是结构体类型名称,因此不能拿来创建结构体变量
特殊类型:匿名结构体类型
struct
{
char c;
int a;
double d;
}s1,s2;
成员变量的初始化
实例1
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
实例2
struct S2
{
char c1;
int a;
char c2;
};
struct S3
{
char c1;
int a;
char c2;
char c3;
};
struct B
{
float f;
struct S s; //又嵌套了一个结构体
};
struct S
{
int a;
char c;
}s1;
全局结构体变量s1,创建类型的同时创建变量
int main()
{
struct S s2 = { 100,'q' }; //用结构体类型struct S创建s2结构体变量,并给成员变量赋值
与数组初始化类似,里面放的不是一个元素,s2里面有两个成员
struct S s3 = { .c = 'r',.a = 2000 };
结构的指定初始化:使用点运算符和成员名
没有按先赋值a后赋值c的顺序赋值,而是按照想要的顺序赋值
struct B sb = {3.14f, { 200,'w' }}; sb为struct B类型的变量,sb里面包含了 一个float类型的数据和一个结构体S,结构体S又包含了一个,又要用{}初始化,里面包含了一个整形和字符型
printf("%f,%d,%c", sb.f, sb.s.a, sb.s.c);//用运算符点访问成员变量
return 0;
}
实例3
struct S
{
char name[100];
int* ptr;
};
int main()
{
int a = 100;
struct S s = { "abcdef",NULL };
类型 变量 赋值
可以类比于数组,{}里面放的不仅仅是一个元素
int arr[3]= {1,2,3};
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;
}
结构体传参:函数传参的时候,参数需要压栈,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销,比较大,
所以导致性能下降,所以结构体传参的时候尽量传结构体的地址
结构体变量与成员变量与结构体类型的区分
1、
struct S //类型声名
{
int a;
char c;
}s1; //全局变量,创建类型的同时创建变量
struct S s3;//全局变量,直接拿类型创建变量
2、
typedef struct Stu
{
int age;
char *name;
} S;
小结:
有了结构体类型我们可以创建结构体变量,有了结构体变量,我们可以通过传结构体变量访问成员变量
1、传结构体变量给指针,指针->访问成员变量
2、传结构体变量,用结构体变量 . 访问成员变量
这三者各不相同,注意不要混淆了
此外还要注意有没有typedef重定义,1中无typedef,s1就是通过创建结构体类型的同时创建了结构体变量
2中有typedef,2中的S不是结构体变量,二是typedef重定义的一个结构体类型的名字,相当于Fan是一个人的名字,而fan是他的小名,也是他的名字
三、计算结构体的大小
先观察以下代码:
struct s1
{
int a;
char c;
};
struct s2
{
char c1;
int a;
char c2;
};
struct s3
{
char c1;
int a;
char c2;
char c3;
};
int main()
{
printf("%d\n", sizeof(struct s1)); 求结构体所占字节大小
printf("%d\n", sizeof(struct s2));
printf("%d\n", sizeof(struct s3));
return 0;
}
为什么不是这样呢
struct s1:int a(4字节)+char c(1字节)=5…
引入
在 c 语言中,对齐数(alignment)是指在内存中某个数据类型的变量所占用的字节数。这个字节数取决于计算机体系结构和编译器的实现。c 语言中不同的数据类型有不同的对齐数,通常为 1、2、4 或 8 字节。
偏移量(offset)指的是一个变量相对于其所在数组或结构体开头位置的字节偏移量。
例如,有一个int类型的数组arr,数组中每个元素占用4个字节。那么,arr[0]相对于数组首地址的偏移量为0,arr[1]相对于数组首地址的偏移量为4,依此类推。
那么首先要掌握结构体内存的对齐规则:
1.结构体的第一个成员永远都放在0偏移处
2.从二个成员开始,以后的每个成员都要对齐到对齐数的整数倍的地址处
这个对齐数是:成员自身大小和编译器默认对齐数的较小值
备注:
Vc环境下,默认对齐数是8
gcc环境下没有默认对齐数,对齐数就是成员自身的大小
3.当成员全部存放进去后,结构体的总大小必须是所有成员的最大对齐数的整数倍,如果不够,则浪费空间补齐
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍
struct s1
struct s2
struct s3
结构体嵌套结构体
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
为什么会存在内存对齐呢,又或者说结构体大小为什么不是我们一开始看来类型字节大小简单地相加呢?
一个32位cpu可以每次访问4个字节(32位=4字节),而一个64位cpu可以每次访问8个字节(64位=8字节)
结构体内存对齐是为了优化内存访问速度。计算机内存是按照字节寻址的,也就是说每次读写内存都是以字节为单位的。如果结构体中的成员变量紧密地排列在一起,那么访问这些成员变量时需要多次读写内存,这将降低程序的性能。
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
四、位段
位段的定义:位段(Bit field)是一种数据结构,用来表示一个数据类型中的位域(bits)或位段(bit fields)。通过使用位段,可以将一个数据类型中的若干位组合在一起,并分别命名。这样做的好处是可以有效地利用内存空间,因为位段只占用必要的位数,从而减小了数据类型的大小。
1.位段的成员必须是int 、unsigned int ,signed int 或者是char
2. 位段的成员名后边有一个冒号和一个数字
3.位段的空间上是按照需要int 4个字节或者char 1个字节的方式来开辟的
实例1
struct a
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
//47个比特位
};
int main()
{
struct a sa = { 0 };
printf("%d\n", sizeof(sa)); //占8字节 通过位段优化内存空间,最终节省17个比特位 本来是16个字节,已经v大大节省了
return 0;
}
以上述例子来谈:int类型。先开辟4个字节,32个比特位,分给_a,_b,_c后还剩余15个比特位,不够分给_d,再开辟4个字节,32个比特位给_d,剩余2个bit位
实例2
struct S
{
char a:3; //设立分配到的bit位
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;
总结:位段能起到与结构一样的作用,可以有效地节省空间,但是存在跨平台问题
跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。