结构体简介:
c语言里int 、float、double、等等类型来表示一个对象,但有时也有未能表达的对象,比如表示一个人的类型,这个类型里有人的身高、体重、年龄等等,这就需要很多个类型来拼凑,这就很不方便。于是,c语言就提供了一个新的自定义类型结构体,可以由自己的需求定义,它是由基本类型组成(可以是不同类型,也可以是相同类型)是一个集合,比如,一个结构体里可以有int、floa、char、int[num]等等,他和数组一样都是一个集合,但不同的是数组只能由相同类型的数据构成,结构体可以有不同的类型构成。
结构体的声明:
1.每一个结构体成员后面必须有一个;(分号),注意这个分号是不能省略的
2.结构体的成员最少一个,不可以一个成员也没有
像上面这种定义就是错误的
3.末尾有一个分号,也不能去掉
举例:
struct Stu
{
char name[20];//姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
}; //注意分号不能掉
结构体成员访问操作符:
. 操作符: 结构体变量 . 成员名
->操作符: 结构体指针->成员名
结构体变量的创建和初始化:
结构体变量的创建
结构体变量的创建和其他数据类型一样,类型+变量名(正常创建),但不同的是他也可以在定义的时候就创建
结构体变量的创建可以在定义的时候就创建
正常创建
结构体变量的初始化(可以在创建的时候初始化,使用初始化列表),可以按照顺序初始化,也可以使用操作符指定初始化
按照顺序初始化
#include <stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
struct Stu _stu = { "wang",18,"man",202415789 };
return 0;
}
指定初始化(使用初始化列表)
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
struct Stu _stu = { .age = 18,.id = "21456",.name = "wang",.sex = "man" };
return 0;
}
初始化注意点
关于基本类型可以直接使用=操作符,可以不使用初始化列表
对于数组,不能直接向上面这样初始化
上面这两种初始化方法都是错误的:
第一个拿到的是数组id首元素的地址,数组的地址是一个常量地址,他的定义是(数组元素类型)int* const ,const加在*号的后面修饰,修饰的是指针本身(不是指针指向的对象),指针本身不能被改变。
第二种有两个错误,首先拿到的是数组的第20个元素位置,char(字符类型),而"wang"是一个字符串类型,把一个字符串类型赋值给一个字符类型,这属于类型不匹配错误。其次,数组的长度为20,第一个下标为0,最后一个下标为19,数组的下标范围为0-19,没有20下标,这属于数组越界错误,访问了不属于你空间的数据。
结构体的特殊声明(匿名声明)
如图,上面这种代码并没有结构体类型名,但一样可以创建变量,这种声明的方式叫匿名申明,是合法的。
如图,上面代码有错误吗?虽然两个结构体的匿名申明是合法的,并前结构体成员也是一模一样,但其实编译器会把这两种结构体认为是两个不同的类型。因为是匿名申明,所以编译器也不知道,所以就会自动认为这是两种不同的类型。
匿名申明一般的使用场景是只使用一次,他只能申明的时候创建变量,因为他没有结构体类型名,使用很局限。
结构体的自引用
结构体不能自己包含自己
看上去很像递归,但其实大错特错,自己包含自己,又不像递归有限制条件,那他的的内存多大呢?无限大吗?所以这是不合法的。
像这种可以(顺序表),它包含了一个自己类型的指针,指针的大小为4/8,所以他是合法的。
结构体的重命名
在申明的时候可以进行重命名,这样子在创建变量的时候就很方便。
上面的代码是非法的,因为编译器在翻译代码是顺序执行的,所以在申明的时候就不能使用(c语言不能,但是c++中可以,因为c++中升级成了类)
结构体内存(内存对齐)
结构体的内存计算,并不只是简单的成员变量内存相加,而是有一个对齐规则,为了高效读取。
内存对齐
对齐知识
偏移量:结构体中的某个成员相对于结构体其实地址的字节偏移量,偏移量从0开始,规定第一个成员对齐到偏移量为0的地址处
对齐数:编译器中默认的默认对齐数与该成员变量大小的较小值,默认对齐数一般看架构32位/64位,每一个成员变量都有一个自己的对齐数,所以对齐数有最大和最小概念之分。
vs中32位,默认对齐数是4字节,64位下则是8字节,这和机器传输的线数有关。拿32位机器举例:32位机器可看作成由32根线组成,由32根线传输数据,32位即32bit,一共4个字节,所以一次可以传输4个字节(这里不代表只能读取4个字节,(理论上)也可以读取1个字节,这里是拿空间换时间,为了效率),所以把默认对齐数设置成为4个字节,可以使得读取数据时最高效。
Linux下的gcc编译器没有默认对齐数
对齐规则
对齐规则分为成员对齐和结构体整体对齐
成员对齐:规定第一个成员对齐到偏移量为0的地址处,其他成员对齐到对齐数的整数倍地址处
结构其整齐对齐:结构体的内存大小必须为最大对齐数的整数倍(每一个成员变量都有一个自己的对齐数)
如图()
如下分析
这里浪费了2个字节,是拿空间换时间
为什么存在内存对齐
平台原因
不是所有的平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因
上面也提到过,是拿空间换时间,数据结构(尤其是栈),应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次的内存访问;而对齐的内存仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作读或者写值了。否则,我们可能需要执行两此内存访问,因为对象可能被分放在两个8字节的内存块中。
以下是vs中的32位机器演示,默认对齐数为4字节
修改默认对齐数( 预处理指令)
如果你不想浪费空间,也可以修改默认对齐数,但效率可能会下降
#pragma pack(num) 修改默认对齐数为num
#pragma pack() 还原为默认对齐数
#include <stdio.h>
int main()
{
#pragma pack(1)//设置默认对齐数为1
struct C
{
char c1;
int i;
char c2;
}_s2;
#pragma pack()//还原为默认对其数
struct S
{
char c1;
int i;
char c2;
}_s1;
printf("%zd\n", sizeof(_s2));
printf("%zd\n", sizeof(_s1));
return 0;
}
建议修改默认对齐数尽量为2的倍数,为了效率的保障
结构体传参
#include <stdio.h>
//结构体传参
struct S
{
int data[1000];
int num;
}s = { {1,2,3,4},1000 };
//结构体传参
void print1(struct S s)
{
printf("%zd\n", s.num);
}
void print2(struct S* ps)
{
printf("%zd\n", ps->num);
}
int main()
{
print1(s);
print2(&s);
return 0;
}
上面这个代码,那个函数效率更好,肯定是print2。
从效率看:print1使用结构体传参,会给形参开辟一块S类型结构大小的内存空间,在拷贝赋值,而print2函数,则是开辟一块4/8字节的内存,用一个结构体指针接受变量s地址,直接访问结构体变量s。
从功能看:传结构体能做的事,指针肯定能做,但指针能做的事,传结构体未必能做,传指针可以改边实参,但传一个结构体就做不到。
总结,结构体在传参的时候可以优先考虑结构体指针传参
结构体实现位段
位段:是 C 语言中一种特殊的结构体成员,用于将一个整型变量按位划分为多个独立的段,每个段可以单独存储数据。位段的定义方式是通过在结构体中使用 :位数
来指定每个成员占用的二进制位数。所谓位段,位就是指计算中最小的单位bit,段就是指一个成员变量。
位段的内存分配不同于不同的结构体内存对齐,而是根据你的需求开辟指定的位(bit),能省下很多空间,但不代表完全按照你的需求开辟空间,内存分配从左向右还是从右向左开始,一次不够会共用内存,还是另外开辟一块内存存储这些都是不确定的,c语言并没有明确规定,所以位段是不支持跨平台的。
位段的内存分配
如图演示
位段使用注意点
上面的介绍说明了,位段的成员可以只占用几个bit位,这超过计算机中内存中的最小的分配单元字节(byte),所以是不能对位段的成员使用取地址的,因为一个字节才有一个地址,几个bit位是没有地址概念的。
上面的这种用法是错误的,它错误的使用了对位段成员取地址,而因该直接赋值
如下图,是正确的给位段成员赋值
位段的应用
位段的使用其实非常小众,运用非常少,比较经典的运用就是网络协议中IP数据报的格式
如下图(是我引用别人的博客图)
如何节省结构体的内存(当内存比较珍贵时)
1.修改默认默认对齐数(上面介绍过)
2.使用位段(使用比较少)
3.尽量把内存小的成员变量集中在一起
s1和s2的内存一样吗,还是谁更大一些
s2更大一些(如果对这里还不太懂,说明结构体的内存对齐你还没有搞明白,还需要再看一下)