C语言中的自定义类型之结构体篇
引入
YY是一位制作月饼的大师,市面上提供的月饼模具已经不能满足YY大师的追求,为了做出更加诱人的月饼,YY大师决定自己制作模具
不同的数据类型就像是各种做月饼的模具。C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学的信息,这时单⼀的内置类型是不⾏的。
描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = { 1, 2 };
printf("%d %d", p.x, p.y);
return 0;
}
以上代码声明了一个结构体类型Point,创建了结构体变量p,并且初始化p
结构体的创建和初始化
结构体的一般声明
我们重点关注结构体的语法形式
struct tag//结构体类型的名称
{
member-list;//成员变量,用来描述结构体包含的信息
}variable-list;//变量列表,用来创建结构体变量
来个具体的例子感受结构体变量的创建和初始化
#include <stdio.h>
struct Point
{
int x;
int y;
}p1 = { 1 ,2 };//创建并初始化结构体变量的第一种方式
struct Point p2 = { 2,3 };//创建并初始化结构体变量的第二种方式
int main()
{
struct Point p3 = { 3,4 };//创建并初始化结构体变量的第三种方式
return 0;
}
结构体变量可通过三种方式进行创建,代码中已体现,我们需要注意的是第一种和第二种方式创建的变量为全局变量,第三种方式为局部变量
指定顺序初始化结构体
下面我们再看一种初始化的方式:指定顺序初始化,并且与顺序初始化做了比较
struct P
{
int x;
int y;
};
int main()
{
struct P p1 = { 1,2 };
struct P p1 = { p1.x = 1, p1.y = 2 };
struct P p1 = { p1.y = 1, p1.x = 2 };//err
struct P p1 = { .y = 1, .x = 2 };
printf("%d %d", p1.x, p1.y);
return 0;
}
前两种方式为顺序初始化,程序可以正常执行
后两种方式为指定顺序初始化,只有第四种可以合法,第三种C语言是不支持的
//这里相当于提供给读者一个鄙人踩过的坑
结构体的不完全声明
最后是一种特殊的结构体声明,结构体的不完全声明
struct //没有结构体的名字
{
int x;
int y;
}p1;//可以在这里直接初始化
int main()
{
p1.x = 1, p1.y = 2;//直接访问结构体成员进行初始化
printf("%d %d", p1.x, p1.y);
return 0;
}
结构体的自引用
该部分在数据结构中极为重要,这里了解一下即可
struct Node
{
int Data;
struct Node* next;//创建结构体指针,存放结构体地址
};
将每一个节点分为数据域和指针域,数据域用来存放数据,指针域用来存放下一个节点的地址,通过结构体自引用的方式来实现链表等复杂的数据结构
结构体重命名
typedef struct Node
{
int Data;
struct Node* next;//这里不能直接写成Node* next 因为还没有完成重命名
}Node;//给struct Node重新命名为Node
结构体变量的访问
. 操作符
语法形式: 结构体变量 . 结构体成员名
->操作符
在结构体传参时使用,语法形式:结构体指针->结构体成员名
结构体内存对齐
YY同学已经掌握了结构体的使用方式,她还想知道自己定义的结构体占用多少字节该怎么办腻?
结构体在内存的存储方式
想要探讨结构体在内存中的存储方式就要知道C语言定义的一个宏 offsetof
从C++官网上可以查阅到,该宏的用处是观测每个结构体成员的偏移量(后面有解释),头文件为 <stddef.h> ,参数分别为类型和成员变量,并且返回值为size_t类型
struct P
{
char ch1;
char ch2;
int a;
};
int main()
{
printf("%zd\n", offsetof(struct P, ch1));//0
printf("%zd\n", offsetof(struct P, ch2));//1
printf("%zd\n", offsetof(struct P, a));//4
printf("%zd\n", sizeof(struct P));//8
return 0;
}
YY:啥是个偏移量啊??
我们假设结构体存放时有一个起始位置,偏移量就是指:结构体变量存放时 距离起始位置的字节长度
存放时的规则(对齐规则)如下:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到对⻬数的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩ 的 较⼩值。
VS 中默认的值为 8,Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
来解析一下这个图片的含义:对齐规则的第一条得,ch1存放在地址为0的位置,并且占用一个字节单位,由规则第二条,其他成员变量存放在 默认的对齐数 与 该成员变量⼤⼩ 的 较⼩值也就是1,的整数倍的地址处,所以,ch2存放在地址为1的位置,接下来a同理,较小值为4,所以a存放在地址为4的位置。最后依照第三条,结构体总大小就为8个字节
再来练习一个
struct s1
{
double d;
char c;
int i;
};
struct s2
{
char ch1;
struct s1 s1;
double d;
};
int main()
{
struct s2 s2;
printf("%zd", sizeof(struct s2));
return 0;
}
结果为32个字节,在内存中的存放形式如下
为什么存在内存对齐??
YY:那2,3位置的内存不就浪费掉了吗?为啥要这样存储啊???
好滴 我们下面就来解释一下YY同学的疑问
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。- 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
了解即可哈,接受C语言的设定
修改默认对齐数
YY:这么多空间都被白白浪费掉了,有啥办法咩~
- 让占⽤空间⼩的成员尽量集中在⼀起
struct S1
{
char ch1;
char ch2;
int a;
};
struct S2
{
char ch1;
int a;
char ch2;
};
int main()
{
printf("%zd\n", sizeof(struct S1));//8
printf("%zd\n", sizeof(struct S2));//12
return 0;
}
- 修改对齐数的默认值
只要在括号里填上想要的默认对齐数即可修改
#pragma pack(1)
struct S1
{
char ch1;
int a;
char ch2;
};
int main()
{
printf("%zd", sizeof(struct S1));//6
return 0;
}
结构体传参
和所有参数传递的情况一样,结构体传参也有两种方式,分别为传值调用和传址调用
//声明放在使用之前
struct S
{
int arr[100];
char n;
char m;
};
//传值调用
void print1(struct S tmp)
{
for (int i = 0; i < 10; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("\n");
printf("%c\n", tmp.n);
printf("%c\n", tmp.m);
}
//传址调用
void print2(struct S* p)
{
for (int i = 0; i < 10; i++)
{
printf("%d ", p->arr[i]);
}
printf("\n");
printf("%c\n", p->n);
printf("%c\n", p->m);
}
int main()
{
struct S s = { {1,2,3,4,5,6,7,8,9,10}, 'a', 'b' };
print1(s);
print2(&s);
return 0;
}
周知:传值调用是形参是实参一份临时拷贝,会再次开辟空间从而造成空间和时间上的浪费。因此,我们首选传址调用
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结构体实现位段
YY同学现在想存储一种信息,这个信息的取值只有0,1,2,3四种情况,你是内存空间,你要给他分配多少的字节呢?
我的答案是两个比特位足矣
这就涉及到位段的相关知识,位段中的位就是指比特位,我们看以下代码
struct A
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
int main()
{
printf("%zd", sizeof(struct A));
return 0;
}
}
按照对齐规则来讲,结构体中包含四个字符型,因占四个字节,但是通过使用位段,该代码只占用了3个字节
什么是位段
位段的成员必须是 int ,unsigned int 或 char 并且位段的成员名后边有⼀个冒号和⼀个数字
位段在内存中的存储方式
假设数据存储时从右向左存储,也就是比特位在使用时先使用右边后使用左边,内存使用时由低地址到高地址
并且规定:位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的
前提:位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段,下文以VS为例
解释:首先存放a,开辟一个字节空间的大小,也就是八个比特位,10的二进制代码为1010,但是位段中规定只给a分配三个比特位,所以只存放了010,之后存放b,占四个比特位,此时还剩五个比特位,所以不用开辟内存空间,直接存放b的二进制代码1100,然后存放c,此时只剩下一个比特位所以开辟内存空间,申请了八个比特位,c占用五个比特位,再次开辟内存空间存放d,占用四个比特位,至此数据存储完毕,总共占用三个比特位
使用位段的注意事项
通过上述的代码我们注意到,a和b是共同占用一个字节的,那么a和b的地址就是相同的。聪明的YY同学又提问了
YY:占用同一个地址要怎么使用scanf来给a或者b赋值呢?
struct A
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
int main()
{
struct A sa = { 0 };
scanf("%d", &sa._b);//err
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
由于位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
总结
以上是C语言中结构体相关的知识,我们要像捡贝壳一样捡到自己的小篮子里哦,有不明白的地方欢迎留言,作者水平有限,文章不妥部分还请各位读者指正。