目录
[1. 结构体类型的声明](#1. 结构体类型的声明)
[1.1.1 结构体如何声明](#1.1.1 结构体如何声明)
[1.1.2 结构体变量的创建和初始化](#1.1.2 结构体变量的创建和初始化)
[1.2 结构的特殊声明](#1.2 结构的特殊声明)
[1.3 结构的自引用](#1.3 结构的自引用)
[2.1 对齐规则](#2.1 对齐规则)
[2.2 为什么存在内存对齐?](#2.2 为什么存在内存对齐?)
[2.3 修改默认对齐数](#2.3 修改默认对齐数)
[3. 结构体传参](#3. 结构体传参)
[4. 结构体实现位段](#4. 结构体实现位段)
[4.1 什么是位段](#4.1 什么是位段)
[4.2 位段的内存分配](#4.2 位段的内存分配)
[4.3 位段的跨平台问题](#4.3 位段的跨平台问题)
[4.4 位段的应用](#4.4 位段的应用)
[4.5 位段使用的注意事项](#4.5 位段使用的注意事项)
自定义类型有结构体,联合体,枚举三种类型,这篇博客会讲述结构体。
1. 结构体类型的声明
1.1结构体
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
数组也是一些值的集合,只不过数组是相同类型元素值的集合,而结构体可以是不同类型元素值的集合。
1.1.1 结构体如何声明
cpp
struct tag
{
member-list;
}variable-list;
//struct 结构体关键字
//tag 结构体标签/结构体名字
//member-list 成员列表
//variable-list 结构体变量列表
例如:描述一本书
cpp
//描述书: 书名,作者,定价,书号
//结构体类型
struct Book
{
char book_name[20];//书名
char auyhor[20];//作者
float princ;//定价
char id[19];//书号
};//分号不能丢
1.1.2 结构体变量的创建和初始化
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//结构体
//描述书: 书名,作者,定价,书号
//结构体类型
struct Book
{
char book_name[20];//书名
char auyhor[20];//作者
float princ;//定价
char id[19];//书号
}b8;//可以在结构体后面创建变量,b2是全局变量,
int main()
{
//b1为局部变量,创建变量后直接赋值,也可以不赋值
struct Book b1 = {"黎相思的博客","黎相思",20.8f,"lxs20030913"};
//那如果不想按结构体里面的成员顺序初始化呢?
struct Book b2 = { .id = "lxs20030812",.auyhor = "Aurora",.princ = 26.5f,.book_name = "Aurora的博客" };
//打印,使用.操作符
printf("%s %s %f %s\n", b1.book_name, b1.auyhor, b1.princ, b1.id);
printf("%s %s %f %s\n", b2.book_name, b2.auyhor, b2.princ, b2.id);
return 0;
}
执行结果:
小数在内存中为什么不能精确保存呢?
浮点数判断是否相等
浮点数在比较相等的时候不能用等号比较 ,用等号比的时候可能会有问题。
1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明,可以将名字删掉。
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
/*
匿名结构体类型,在这个结构体声明的时候直接用结构体创
建结构体变量可以创建一个,也可以创建多个。
匿名结构体只能使用一次,就是在声明的时候直接创建变量
*/
struct//匿名结构体类型,这个结构体没有名字
{
int i;
char c;
float a;
}s1,s2;
struct
{
int i;
char c;
float a;
}* ps;
int main()
{
return 0;
}
上⾯的两个结构在声明的时候省略掉了结构体名字。
那么我在上面的代码的main函数中写ps = &s1;这个代码合法吗?
cpp
int main()
{
ps = &s1;
return 0;
}
虽然两个都是结构体匿名类型,两个结构体的成员变量都一样,但是不行,编译器都会报警告。
编译器会认为这是两个不同的类型,具体的名字都没有,s1是一种结构体类型,取它地址的话是一种结构体类型的指针,吧这个地址赋给一个结构体指针变量ps,那ps也没有名字,编译器会认为两个结构体不一样,两个类型虽然成员一摸一样但是类型编译器会认为不一样,所以结构体指针也是不一样的,编译器报的两边类型不兼容。
注意:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用⼀次。
1.3 结构的自引用
那结构体的自引用到底是什么意思呢?
下面举个例子:
下面写一个结构体
cpp
struct Node
{
int data;//存放数据
struct Node next;//下一个节点
};
data存放我们的数据,next存放下一个节点的信息,有点像递归,这样写是不对的,这样写的话这个结构体的大小是多少?这个结构体里面有一个data四个字节,还有一个我自己,这样一层一层套下去的话这样一个结构体类型是多大呢?这个大小是无法计算的,肯定也是不对的。
我们想让结构体找到下一个节点,叫做结构体的自引用,结构体找到同类型的下一个节点,还是上面画的那个图,我想找到下一个节点,那我除了存放数据,还存放下一个结构体的地址就可以了,所以我们要写成同类型下一个节点的指针就可以了。
cpp
struct Node
{
int data;//存放数据
struct Node* next;//下一个节点
};
那么这个结构体都是可算的,一个整型是4个字节,一个结构体的指针是4或者8个字节。
总结:
一个节点里面存放数据,又能存放下一个节点的地址,那么里面有需要一个指针,这种实现方式可以让这个结构体找到下一个同类型的结构体的对象,这就叫结构体的自引用。
结构体的重命名:
重命名产生一个名字叫Node,next的类型为Node*,还是这样一个结构体的指针next,这样写行不?那我们编译一下。
编译器会报错,这样写是不合适的,因为是对前面的结构体类型命名产生Node,也就是说有了结构体里面的东西才能重命名成Node,而结构体里面就使用了重命名的Node,这样是不行的,还没有产生Node之前里面就用了Node,这就类似于先有鸡还是先有蛋的问题。
那应该怎么解决呢?
cpp
typedef struct Node
{
int data;
struct Node* next;
}Node;
这样写编译没有问题。
也有的时候会写出下面的代码
cpp
typedef struct
{
int data;
Node* next;
}Node;
会把结构体的名字省略,对匿名结构体类型重命名叫Node,这样还是不行的,还是得先有这个结构体类型才能使用重定义的名字,类型还没有的时候想使用是绝对做不到的。
在结构体里面用同类型的结构体,这个时候一定不能用匿名。
2.结构体内存对齐
计算结构体的大小。
结构体内存对⻬。
2.1 对齐规则
举个例子:
cpp
struct stu1
{
char c1;
char c2;
int n;
};
struct stu2
{
char c1;
int n;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct stu1));
printf("%zd\n", sizeof(struct stu2));
return 0;
}
这里我创建了两个结构体成员一摸一样之后后面两个成员换了一下顺序的结构体,试问,这两个结构体的大小是否一样。
这里我们可以简单分析一下,第一个结构体stu1中char c1占一个字节,char c2占一个字节,int n占4个字节,总共6个字节,第二个结构体stu2中char c1占一个字节,int n占4个字节,char c2占1个字节,总共是6个字节,两个结构体的大小都是6个字节,那么这里分析的对不对呢?代码运行一下。
这里为什么一个是8,一个是12呢?
这里的char确实是1个字节,int也确实是4个字节,但是在内存中可不只是开辟这么大就行了,这就涉及到内存对齐,结构体的成员在内存中是存在内存对齐现象的。
偏移量:
我现在要使用结构体创建一个变量,并且赋值为0,也就是说在内存中开辟好空间了,那么我的char,int类型分别在内存的哪里呢?
offsetof - 宏
计算结构体成员相较于结构体变量起始位置的偏移量
我们我们利用这个宏计算一下我们的stu1里面的三个成员的偏移量。
cpp
#include <stdio.h>
#include <stddef.h>
struct stu1
{
char c1;
char c2;
int n;
};
int main()
{
printf("%ld\n",offsetof(struct stu1,c1));
printf("%ld\n",offsetof(struct stu1,c2));
printf("%ld\n",offsetof(struct stu1,n));
return 0;
}
为什么运行结果是这样的呢?
接下来我们看看结构体stu2的每个成员的偏移量。
cpp
#include <stdio.h>
$include <stddef.h>
struct stu2
{
char c1;
int n;
char c2;
};
int main()
{
struct stu2 s2 = { 0 };
printf("%d\n", offsetof(struct stu2, c1));
printf("%d\n", offsetof(struct stu2, n));
printf("%d\n", offsetof(struct stu2, c2));
return 0;
}
运行出的结果和之前不一样了。
那么它们到底是怎么对齐的呢?
⾸先得掌握结构体的对⻬规则:
-
结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
-
其他成员变量要对⻬到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。
-
VS 中默认的值为 8
-
Linux中 gcc 没有默认对⻬数,对⻬数就是成员自身的大小
-
结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的 整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体成员对⻬到自己的成员中最大对齐数的整数倍处,结构 体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
下面我们画图看看stu1结构体在内存中到底是怎么计算的。
下面我们画图看看stu2结构体在内存中到底是怎么计算的。
练习:
cpp
struct S3
{
double d;
char c;
int i;
};
这个结构体是多大呢?
运行结果是16个字节,我们画图来看一下。
前面我们只看了前三条规则,那最后一条呢?
举个例子:
cpp
#include <stdio.h>
struct stu3
{
double d;
char c;
int i;
};
struct stu4
{
char c1;
struct stu3 s3;
double d;
};
int main()
{
struct stu4 s4 = { 0 };
//printf("%zd\n", sizeof(s4));//这里写结构体或者结构体变量名都可以
printf("%zd\n", sizeof(struct stu4));
return 0;
}
结构体里面嵌套了一个结构体变量,stu3我们前面算出的是16个字节,现在这个结构体stu4是多大呢?
代码运行出来是32,那这里为什么是32呢?下面我们画图来看一下
2.2 为什么存在内存对齐?
-
平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常,比如取出一个int类型的数据,只能访问4的倍数的地址处,其他地址访问不了。
-
性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对齐。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
举个例子:
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
起始尽量将空间小的集中在一起就可以了。
比如下面代码:
cpp
struct stu1//占8个字节
{
char c1;
char c2;
int n;
};
struct stu2//占12个字节
{
char c1;
int n;
char c2;
};
上面的两个结构体成员都一样,但是大小不一样,就像stu1中的c1和c2,对齐数都是1,相比于stu2就把浪费的空间合理的利用起来了, 如果我们在设计结构体的时候,尽量把占用空间较小的成员集中在一起来放,那这样的话原来可能要浪费的空间现在就会用起来,这样既能满足对齐,又能节省空间。
2.3 修改默认对齐数
vs上的默认对齐数其实是可以修改的。
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
这个结构体的大小是12个字节,下面我们修改一下默认对齐数,看看会有怎么样的结果。
那我们修改默认对齐数为2看看结果:
我们在未来写代码的时候觉得默认对齐数不合适的话可以修改我们的默认对齐数,使用#pragma这个指令,一般我们修改默认对齐数都是2的次方数,比如1,2,4,8,16,这些数字,3,5,7这些都是不可取的。
3. 结构体传参
结构体传参分为传结构体和传地址
传结构体变量:
cpp
#include <stdio.h>
struct stu
{
int arr[1000];
int n;
char ch;
};
void print(struct stu x)
{
for (int i = 0; i < 10; i++)
printf("%d ", x.arr[i]);
printf("\n%d\n", x.n);
printf("%c\n", x.ch);
}
int main()
{
struct stu s1 = { {1,2,3,4,5,6,7,8,9,10},128,'w' };
print(s1);
return 0;
}
这里是将结构体变量s1传参使用。
传结构体变量地址:
cpp
#include <stdio.h>
struct stu
{
int arr[1000];
int n;
char ch;
};
void print(struct stu* x)
{
for(int i = 0;i < 10;i++)
printf("%d ",x->arr[i]);
printf("\n%d\n",x->n);
printf("%c\n",x->ch);
}
int main()
{
struct stu s = {{1,2,3,4,5,6,7,8,9,10},128,'w'};
print(&s);//传的是地址
return 0;
}
上⾯的传结构体变量和传结构体变量的地址哪个好些?
传结构体变量:
传结构体地址:
答案就是传结构体变量的地址。
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
传址调用的时候形参的修改会影响实参,如果不想形参被修改,前面加上const。
当我加上const,修改形参的时候会报错。
结论: 结构体传参的时候,要传结构体的地址。
4. 结构体实现位段
位段是和结构体相关的。
4.1 什么是位段
位段的声明和结构是类似的,有两个不同:
-
位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
-
位段的成员名后边有⼀个冒号和⼀个数字。
例如:
cpp
struct S//结构体
{
int _a;
int _b;
int _c;
int _d;
};
struct A//位段
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
我们可以看到结构体和位段是很相似的。
注意:后面给的位数不能超过自身大小,int是4个字节,32个位,如果给33个位会报错。
为什么要使用位段:
使用位段可以节省空间,就拿结构体来说4个成员,每个成员都是int类型,按对齐规则来说也得16个字节,那么对于位段来说都限制了它的大小,那它占几个字节呢?会不会节省空间呢?
我们可以看到把结构体设计成位段的时候它会在对齐的基础上节省空间。
_a是2个比特位,_b是5个比特位,_c是10个比特位,_d是30个比特位,一共是47个比特位,一个字节8个比特位,那么这里只需要6个字节就可以了啊,为什么sizeof返回的是8个字节呢?
这个时候就涉及到位段的内存分配。
4.2 位段的内存分配
-
位段的成员可以是 int unsigned int signed int 或者是 char 等类型
-
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
-
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
例子:
cpp
#include <stdio.h>
struct S//位段
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s;
return 0;
}
这样的位段在内存中是怎么开辟空间的呢?
我们计算的是三个字节,代码运行看看和我们算的一样不。
结果是一样的,那到底是不是上面画图那么呢?我们写个代码来验证一下。
cpp
#include <stdio.h>
struct S//位段
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };//设置成每个字节为0,每个比特位为0
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(struct S));
return 0;
}
通过画图计算出来的是0x620304,下面我们调试一下代码看看合适不。
通过调试代码,确实是0x620304,就说明是从右向左开始放,一个字节的剩下空间不够放下一个成员的时候这些空间是浪费掉的。
4.3 位段的跨平台问题
1.int位段被当成有符号数还是无符号数是不确定的。
-
位段中最大位的数目不能确定。16位机器最大16,32位机器最大32,写成30,在16位机器会出问题。在早期的16位机器中sizeof(int)占两个字节,32位机器中sizeof(int)占四个字节,所以上面的代码在16位机器中会报错,在32位机器上就可以。
-
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
-
当⼀个结构包含两个位段,第⼆个位段成员比较大,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4 位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。
4.5 位段使用的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输⼊值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。
这个字节里面有a和b,但都不在起始位置,a和b是没有地址的,对a和b取地址是万万做不到的,
位段是不能取地址的,会报错。
这样写就可以,创建一个第三方变量,对第三方变量输入,然后赋给段位的变量。
完。