结构体
结构体是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量
结构体的声明
下面的代码是结构体的基本框架
struct tag
{
member-list;
}variable-list;
假设描述一个学生,学生的信息:姓名,年龄,性别,学号
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
结构体变量的定义
就以上面的学生为例,struct Stu属于结构体类型,下面定义的,s1,s2,s3,s4就是结构体变量 创建结构体类型的时候,并不开辟内存,只有定义完变量的时候才会开辟空间。
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}s1,s2;
int main()
{
struct Stu s3;
struct Stu s4;
return 0;
}
结构体的特殊声明
结构体的特殊声明就是在创建结构体类型的时候,省略了结构体标签名,也是不完全声明,这也称为匿名结构体类型。
struct
{
char name[20];
int age;
char sex[5];
char id[20];
};
而我们在给匿名结构体类型定义变量的时候只能在结构体类型上,不能在函数内定义结构体变量
struct
{
char name[20];
int age;
char sex[5];
char id[20];
}s1,s2;
int main()
{
struct Stu s3;
struct Stu s4;
return 0;
}
定义s3,s4变量就会报下面的错误,匿名结构体只能用一次,之后再想定义新的变量是不行的。 接下来下面有一个匿名结构体类型问题,来看一下
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
p=&x;
结论:因为是匿名结构体类型,所以编译器会把他们认定为两个不同的结构体类型, 所以p=&x,是错误的
结构体的自引用
结构体的自引用就是,在结构中包含一个为该结构体本身的成员
struct Node
{
int data;
struct Node next;
};
上面的这段代码是错误的,因为这样就类似于套娃,也就不能知道结构体类型的大小 应该改为下面这段代码
struct Node
{
int data;
struct Node* next;
};
如果嫌每次输入这个结构体类型太长,也可以选用typdef 下面的是正确写法
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
Node s1;
return 0;
}
下面是错误示范
typedef struct Node
{
int data;
Node* next;
}Node;
int main()
{
Node s1;
return 0;
}
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
Node s1;
return 0;
}
结构体的初始化
整体初始化
下面s1,s2,s3这两种初始化的方法是都可以的,而在定义s3时,可以顺序颠倒用这种方式
struct Stu
{
char name[15];
int age;
}s1={ "zhangsan",15 };
int main()
{
struct Stu s2 = {"lisi",20};
struct Stu s3={.age=25,.name="wangwu"};
printf("%s %d",s1.name,s1.age);
printf("%s %d",s2.name,s2.age);
printf("%s %d",s3.name,s3.age);
return 0;
}
单个初始化
字符串并不能直接初始化,需要借用到strcpy
struct Stu
{
char name[15];
int age;
char a;
}s1;
int main()
{
struct Stu s4;
strcpy(s4.name, "lili");
s4.age = 46;
s4.a = 'a';
printf("%s %d %c", s4.name, s4.age,s4.a);
return 0;
}
结构体内存对齐
在开始之前我们先来了解一个宏offsetof
它是用来计算结构体成员相对于起始位置的偏移量
#include <stddef.h>
size_t offsetof(type, member);
#include<stddef.h>
struct S1
{
char a;
int b;
char c;
}s1;
int main()
{
printf("%zd\n",offsetof(struct S1,a));
printf("%zd\n", offsetof(struct S1, b));
printf("%zd\n", offsetof(struct S1, c));
return 0;
}
我们先来看一段代码,来看看这个结构体的内存大小 可能有人会猜结果是:6,但是输出的结果为:12
struct S1
{
char a;
int b;
char c;
}s1;
int main()
{
printf("%d",sizeof(struct S1));
return 0;
}
结构体内存对齐的规则 1.结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处 2.从第二个成员开始,要对齐到某个【对齐数】的整数倍的偏移处 对齐数:结构体成员自身大小和默认对齐数的较小值 VS编译器的默认对齐数:8 Linux环境默认不设对齐数(对齐数是结构体成员的自身大小) 3.结构体的总大小,必须是最大对齐数的整数倍 每个结构体成员都有体格对齐数,其中最大对齐数就是【最大对齐数】 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们来看一段代码
struct S2
{
double d;
char c;
int i;
};
struct S1
{
char c1;
struct S2 s2;
double d;
};
int main()
{
printf("%d",sizeof(struct S1));
return 0;
}
修改默认对齐数
#pragma pack(1)
#pragma pack()
struct s1
{
char a;
int b;
char c;
}s;
int main()
{
printf("%d",sizeof(struct s1));
return 0;
}
为什么要内存对齐
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问 总结:结构体的内存对齐是拿空间来换取时间的做法。
结构体传参
可以将结构体变量直接传过去,也可以将结构体地址传过去
struct s
{
int a;
int b;
};
void print(struct s s1)
{
printf("%d",s1.a);
}
void print1(struct s* p)
{
printf("%d",(*p).b);
printf("%d",p->b);
}
int main()
{
struct s s1 = {1,2};
print(s1);
print1(&s1);
}
传值和传址哪个好 传址好! 原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。
位段
位段的声明和结构是类似的,有两个不同: 1.位段的成员必须是 int、unsigned int 或signed int 。 2.位段的成员名后边有一个冒号和一个数字(数字的单位是比特位)
下面这段代码就属于位段
struct s
{
int a : 2;
int b : 10;
int c : 20;
};
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
接下来我们来看下面的代码,具体的看一下位段的内存是怎么分配的
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
在这里我们也可以看出位段的内存是怎么分配的了,在VS当中每个字节的八个比特位是从右往左用的
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。(不确定是否存在符号位)
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。 总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
可以参考一下下面的代码,同样都是存入1,2,3,4,而用的字节却不一样,这也就是位段的优势。
struct S
{
char a : 1;
char b : 2;
char c : 3;
char d : 4;
};
struct S1
{
int _a;
int _b;
int _c;
int _d;
};
int main()
{
struct S s = { 0 };
struct S1 s1;
s.a = 1;
s.b = 2;
s.c = 3;
s.d = 4;
s1._a = 1;
s1._b = 2;
s1._c = 3;
s1._d = 4;
printf("%d\n",sizeof(struct S));
printf("%d\n",sizeof(struct S1));
return 0;
}
位段的应用
合理的使用空间
枚举
枚举的意思就是一一列举
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Color color1 = RED;
enum Color color2 = 5;
printf("%d",RED);
printf("%d", GREEN);
printf("%d", BLUE);
return 0;
}
也可以对枚举类型里的成员进行定义 在后面还进行了对枚举类型内存大小进行了打印,结果是4,因为枚举类型在定义变量的时候,一次只能定义一个。
enum Color
{
RED = 2,
GREEN,
BLUE = 5
};
int main()
{
enum Color color1 = RED;
printf("%d", RED);
printf("%d", GREEN);
printf("%d", BLUE);
printf("%d",sizeof(enum Color));
return 0;
}
通过这些代码我们也可以看出枚举和宏定义有一些类似 但是宏定义的内容并没有类型,就只是在预处理阶段全部替换了,并且如果数据变大之后,宏定义会很麻烦
#define RED 2
#define GREEN 3
#define BLUE 4
枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
联合(共用体)
联合体和结构体也有一些类似,结构体是只能存在一个成员,而结构体是要求每个成员必须都存在 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)。
union Un
{
char a;
int b;
double c;
};
int main()
{
union Un un;
printf("%d",sizeof(union Un));
printf("%d",sizeof(un));
return 0;
}
union Un
{
char a;
int b;
double c;
};
int main()
{
union Un un;
printf("%p\n",&un);
printf("%p\n", &un.a);
printf("%p\n", &un.b);
printf("%p\n", &un.c);
return 0;
}
在这里可以用联合体来判断计算机的大小端
int ret()
{
union Un
{
char a;
int b;
}un;
un.b = 1;
return un.a;
}
联合体的大小计算
联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
最后:文章有什么不对的地方或者有什么更好的写法欢迎大家在评论区指出
|