结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
下面是结构体声明的形式
struct tag
{
member-list;
}variable-list;
例如现在要使用结构体来描述一个学生:里面就存放了一个学生所具有的基本信息,如姓名、年龄、性别、身高
struct Stu {
char name[20]; //姓名
int age; //年龄
char sex[2]; //性别
float height; //身高
};
或者用结构体来描述一本书,里面存放了:书名、作者、定价、书号
struct Book {
char name[20];
char author[20];
double price;
int id;
};
不仅如此,结构的成员还可以是标量、数组、指针,甚至是其他结构体
接下去我来介绍一种特殊的结构体声明形式,也就是【匿名结构体】,什么是匿名结构体呢?也就是没有结构体名称
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
p = &x;
原因其实就在于这个【匿名结构体】
有关结构体自引用这块,就要说到数据结构里面的相关知识了,可能有的读者没听过,这也没关系,准备发车
struct Node
{
int data;
struct Node next;
};
struct Node
{
int data;
struct Node* next;
};
struct Node list;
但你是否有觉得上面这种形式太麻烦了,每次在定义一个结构体变量的时候都要在前面加上一个
struct
,如果可以不加该多好
typedef
关键字即可,然后再定义变量的位置为其重命名一下,那在定义结构体变量的时候就不需要再加上struct
关键字了typedef struct Node
{
int data;
struct Node* next;
}Node;
Node list;
【友情提示】:可不能把结构体定义成下面这样,Node* next;
这种写法是错误的,因为到这行为止,结构体还不认识Node
,所以是不可以使用它的
typedef struct Node
{
int data;
Node* next;
}Node;
有了结构体类型,那如何定义变量,其实很简单。
s1、s2、s3
指的的都是一个学生,而且它们属于全局变量struct Stu {
char name[20]; //姓名
int age; //年龄
char sex[2]; //性别
float height; //身高
}s1, s2, s3; //全局变量
int a
前面的int
struct Stu
就是这个结构体的类型,不要忘记加上前面的struct这个修饰符了。下面的【ss】定义在外面,那就是全局变量;【su】定义在函数内部,那就是局部变量struct Stu ss; //全局变量
int main(void)
{
struct Stu su; //局部变量
return 0;
}
struct Stu
太麻烦了,也是有办法了,那就是为其进行一个重命名,一般我们直接在结构体最前面加上一个typedef
关键字,然后在定义处为其做一个重命名typedef struct Stu {
char name[20]; //姓名
int age; //年龄
char sex[2]; //性别
float height; //身高
}S;
因此可以说 S == struct Stu,定义方式就简洁了许多
struct Stu su;
S su2;
接下去来讲讲结构体如何初始化
{ }
括起来,里面就可以对结构体的成员进行一个初始化,分别意义对照进行初始化即可struct Stu su = { "zhangsan", 20, "男", 180 };
struct Point {
int x;
int y;
};
struct Point p = { 10, 20 };
但是现在我又有了一个结构体,这个结构体内部呢又有一个结构体,就是上面这个【点】,这该如何初始化呢?
struct MyStruct
{
char c;
struct Point p;
double d;
char str[20];
};
struct MyStruct ms = { 'c', {40, 80}, 3.14f, "haha" };
来看看初始化后的结果
[.]
操作符然后选择对应的成员变量进行初始化即可struct MyStruct ms2 = {.d = 6.28, .str = "abcdef", .c = 'cc'}; //乱序初始化
来看看这样初始化后的结果为多少。可以观察到没有被初始化到的变量就取为默认值,也就是这个【点】
结构变量的成员是通过点操作符
[.]
访问的。点操作符接受两个操作数
[->]操作符
的形式进行访问,这一点我们在指针章节就有说到过,所以我们可以可以称其为结构体指针struct Stu
{
char name[20];
int age;
};
int main(void)
{
struct Stu s1 = { "zhangsan", 20 };
printf("普通形式访问:%s %d\n\n", s1.name, s1.age);
struct Stu* ss = &s1;
printf("指针形式:%s %d\n", ss->name, ss->age);
return 0;
}
struct S {
char name[20];
int age;
};
int main(void)
{
struct S s = { .age = 22, .name = "zhangsan" };
}
s.age = 30
;来说是没有问题的,但是呢对于姓名的修改来说其实是存在问题的,在数组章节我们有说到过对于【数组名】来说指的就是首元素地址
s.age = 30;
s.name = "zhangsanfeng";
strcpy(s.name, "zhangsanfeng");
在结构体章节,我们掌握了结构体的基本使用,但是现在我要你去计算一个结构体的大小,你会怎么做呢?
c1
、c2
、i
三个成员变量,那此时分别去计算它们两个结构体的大小, 最后的结果会是多少呢?会是一样的吗struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
int main(void)
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
c1
的类型为【char】,是1个字节;i
的类型是【int】,是4个字节c2
的类型为【char】,是1个字节;1 + 4 + 1 = 6B
,可事实呢,原不止这些。。。offsetof
,它可以用来计算结构体成员相对于起始位置的偏移量
它的第一个参数是结构体类型,第二个参数是结构体成员
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
为什么会出现上面这样的现象呢?对于结构体内存对齐的规则是怎样,让我们继续看下去
知晓了上面这些规则后,我们再来回顾一下上面这个结构体的大小该如何计算
ss
,它的起始地址就从0开始,所以根据第一条规则,第一个成员变量在与结构体变量偏移量为0的地址处,而且它的类型还是char
,所以只占1个内存单元i
,其为整型所以在内存中就需要存储4个字节的大小,此时便要拿其和VS下默认对齐数8去进行比较,取较小的值4i
是从4的位置开始放的,中间空出来的位置就不会再放置其他成员变量了,那么这个3个空间也就浪费了c2
,char类型的变量为1个字节,和8比较取小就是1,那就要将其放到1整数倍的地址处,那其实任何空间都是可以的,直接放到这个【8】的位置就行看完了,这个结构体后,还记得结构体S2吗,我再来讲一道,当然你也可以试着自己写写画画看
c1
放在这个与结构体变量偏移量为0的地址处,而且它的类型还是char
,所以只占1个内存单元char
所占的字节为1B,与8去进行比较一下就可以知道1来得小,那我们直接放在偏移处为1的地方就可以了,此时在内存中也只占了2个字节通过上面两道例题的讲解,相信你对如何去计算结构体大小一定有了一个自己的认识,接下去就让我们趁热打铁来做两道题目再练一练,看看自己是否真的掌握了
练习①
你可以先试着自己做一做,然后和我对一下是否正确
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
【分析】:
double
类型的数据在内存中占8个字节,所以一直占用偏移处为7的地方char
,所以在内存中占用1个字节,那直接放在偏移量为8的地址处即可
运行结果如下:
也可以通过【offsetof】来验证一下
练习②
接下去再来做一道练习,涉及结构体嵌套的问题,对应的需要使用到规则4,忘记了可以翻上去看看
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3; //成员变量为另一个结构体
double d;
};
因为本题的结构体比较大,所以就标出4的整数倍所在的地址
char
,所以只占一个字节的空间double
类型的数据在内存中占8个字节,所以一直到31的地址处运行结果如下:
可以通过【offsetof】再来验证一下
经过了两道例题和两道练习题的训练,相信你对如何计算结构体的大小一定是心中有数了,但在阅读的过程中你是否有疑惑为什么会存在这个【结构体内存对齐】呢?有什么实际意义吗?
① 平台原因(移植原因)
c
和i
,然后要在内存中存储它们,我分为了两种,一个是【无内存对齐】,呈现的是紧密存放;一个是【内存对齐】,需要考虑到最大对齐数c
,但是若要全部读取完i
,就还需要再读取一次,那访问到所有的成员变量就需要两次;c
和i
互不干扰,此时再看到成员变量i,从它的初始地址处开始读取,一次读4个字节,那么读1次就刚刚好可以读完这个变量了,而不是像上面那样还需要再读一次总体来说:
结构体的内存对齐是拿空间来换取时间的做法
了解了为什么会存在内存对齐之后,我们再回到一开始的这两个结构体,你是否有想过为什么两个结构体的成员变量都一模一样但是大小却是一个【12】,一个【8】呢?
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
之前我们见过了 #pragma 这个预处理指令
#pragma comment
,用来链接函数的静态库。这里我们再次使用,可以改变我们的默认对齐数
#pragma pack(1)
就可以设置默认对齐数为1,#pragma pack()
就可以取消设置的默认对齐数,还原为默认。到它为止的默认对齐数还是被修改后的对齐数#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
return 0;
}
运行结果如下:
可以通过【offsetof】再来验证一下
结论:
在上面的每一个结构体计算后,我都使用到了
offsetof
这个宏,和我画出来的内存分布图完全就是一致的,那它的原理到底是怎样的呢?马上来探究一下
曾经有一年的百度笔试题就考到了有关offsetof
的实现原理
【原题】:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
那要如何去实现呢?如果对宏不是很了解的读者可以看看详解程序环境和预处理
我们通过上面的结构体S1进行讲解。列出3个成员变量放置的初始地址,其实【offsetof】计算的也就是每个变量在内存中的起始地址相较于首地址偏移了多少,那将它们进行一个相减就可以得出0
、4
、8
这三个结果
c1
这块地址设置为0,那么
&c1 - 0
&i - 0
&c2 - 0
知道了上面这些我们就可以使用【宏】来实现每个成员变量偏移量的计算了
#define OFFSETOF(m_type, m_name) (int)&(((m_type *)0)->m_name)
m_type
是结构体变量;m_name
是结构体成员
#define OFFSETOF(m_type, m_name) (m_type *)0
printf("%d\n", OFFSETOF(struct S1, c1));
m_name
#define OFFSETOF(m_type, m_name) ((m_type *)0)->m_name
#define OFFSETOF(m_type, m_name) &(((m_type *)0)->m_name)
#define OFFSETOF(m_type, m_name) (int)&(((m_type *)0)->m_name)
下面是流程图:
下面是运行结果:
结构体怎么对齐? 为什么要进行内存对齐?
如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
#pragma pack(3)
便可以将默认对齐数修改为3,其他的也是同理,因为结构体默认对齐数发生了变化,此时就会导致结构体大小发生变化【总结一下】
在模块主要是介绍了如何去计算一个结构体的大小,最重要、最核心的还是开头的4条规则,我们再来回顾一下
有了规则之后,将它们灵活地运用到实际的题目中,只要掌握了方法,就感觉其实计算结构体的大小也没有那么复杂,就是对于【嵌套结构体】的规则有些复杂,要考虑到另一个结构体中的最大对齐数
接下去,我们就谈到了为什么在计算这些结构体的时候会存在内存对齐的现象,对于了设置与不设置内存对齐便观察到这是【空间换时间】的做法
谈了很久的offsetof()
,但是不清楚原理是什么这不,百度笔试题就考到了,于是我们就去自己通过一个宏实现了一下这个偏移量的求解,虽然过程很复杂,但是在我一步步的细讲下,相信聪明的你一定有所理解在理解了结构体内存对齐的各方面之后,面对两道面试题也是毫不畏惧
最后我们再来说说有关结构体的传参
直接上代码
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;
}
上面的 print1 和 print2 函数哪个好些?
——> 如果不了解这一块的可以看看我的函数栈帧一文
【总结一下】:
结构体传参的时候,要传结构体的地址
以上就是本文要介绍的所有内容,感谢您的阅读