1.结构体变量的创建和初始化
1.1 结构体的概念
结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员可以是不同类型的变量
struct tag
{
member-list;
}variable-list;
例如:描述一个学生
struct student
{
char name[20]; //姓名
int age; //年龄
char sex[5]; // 性别
char ID[20]; // 学号
};
1.2 结构体变量的创建和初始化
例1
struct point
{
int x;
int y;
}p1; // 声明类型 同时定义变量p1
struct point p2; //定义结构体变量p2
struct point p3 = { 1, 2 }; // 初始化:定义变量的同时赋初值
例2
struct student
{
char name[20];
int age;
};
struct student s = { "llisi", 20 }; //初始化
例3
#include <stdlib.h>
struct point
{
int x;
int y;
}p1;
struct point p2;
struct node
{
int data;
struct point p;
struct node* next;
}n1 = { 20, {5, 6}, NULL }; //结构体嵌套初始化
struct node n2 = { 10,{4, 5}, NULL }; //结构体嵌套初始化
例4 :不按照成员顺序的初始化(C99)
struct student
{
char name[20];
int age;
};
struct student n1 = { .age = 20, .name = "zhangfang" };
// 或者 :
struct student
{
char name[20];
int age;
}n1 = { .age = 20, .name = "zhangfang" };
1.3 完全声明与不完全声明
不完全声明 :声明时,省略结构体名称
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
// 此时 x , a[20], *p, 是全局变量
注意:
编译器会把这两个声明当成完全不同的两个类型(哪怕结构类型的成员一样),所以是非法的
匿名的结构体类型,如果没有对结构体类型重命名,只能使用一次
1.4 结构体的自引用 (结构体中包含该结构本身的成员)
比如,定义一个链表的节点:
这样写对吗 ??😜
struct node
{
int data;
struct node next;
};
直接写成 struct node next 会造成无限套娃 ,此时结构体变量的大小就是无穷大,不合理
正确写法:
struct node
{
int data;
struct node* next;
};
特例: typedef对匿名结构体类型的重命名
typedef struct
{
int data;
node* next;
}node;
这样写 对吗??
错误。node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用 node 来创建成员变量,这样是不可行的
解决方案:定义结构体不使用匿名结构体
typedef struct node
{
int data;
struct node* next;
}node;
2.结构体成员访问操作符:" . "和 " -> "
形式如下:
结构体变量.成员变量名
结构体指针.成员变量名
例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
struct stu
{
char name[20]; //姓名
int age;
};
void print(struct stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct stu* p)
{
strcpy(p->name, "lisi");
p->age = 19;
}
int main()
{
struct stu s = { "zhangsan", 20 };
print(s);
set_stu(&s);
print(s);
return 0;
}
3. 结构体传参
#include <stdio.h>
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(const struct s* p)
{
printf("%d\n", p->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
两种方式,首选 print2 函数 == >> 结构体传参的时候,要传结构体的地址
因为: 函数传参的时候,参数需要压栈,造成时间和空间上的系统开销。
如果传递结构体对象的时候,结构体过大,参数压栈的系统开销比较大,导致性能下降
4. 结构体内存对齐
4.1 对齐规则:
(1)结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处
(2) 其他成员变量要对齐到对齐数的整数倍的地址处
对齐数:编译器默认的对齐数 与 该成员变量大小的较小值
VS 默认的值是8; Linux没有对齐数,对齐数就是成员自身大小
(3) 结构体总大小是最大对齐数的整数倍
最大对齐数:结构体中每个成员变量都有一个对齐数,所有对齐数中最大的
(4) 如果嵌套结构体,嵌套的结构体成员对齐到自己成员的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍
例:
#include <stdio.h>
int main()
{
// 1
struct s1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct s1)); // 12
// 2
struct s2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct s2)); // 8
// 3
struct s3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct s3)); // 16
// 4
struct s4
{
char c1;
struct s3 s3;
double d;
};
printf("%d\n", sizeof(struct s4)); // 32
}
内存存储示意图:
>> 可以使用 offset宏 (头文件 < stddef.h> ) 计算结构体成员相较于起始位置的偏移量
printf("%d", offset(struct s4, c1);
4.2 为什么存在内存对齐 ?
most 参考资料:
(1) 平台原因 (移植原因) :
不是所有的硬件平台都能访问任意地址的任意数据;某些硬件平台只能在某些地址处去某些特定类型的数据,否则造成硬件异常
(2) 性能原因:
数据结构(尤其是栈)应该尽可能在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问只需要一次。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果将 所有的double类型的数据的地址全部对齐成8的倍数,那么就可以用一个内存块来读或者写值了;否则,可能进行两次内存访问,因为对象可能被分别放在两个8字节内存块中。
总体而言,结构体的内存对齐是拿空间换取时间的做法
倘若既要满足对齐,又要节省空间,做法:让占位空间小的成员尽量集中在一起
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
s1 和 s2 类型的成员一样,但是s1 所占空间小于 s2所占空间
4.3 修改默认对齐数
#pragma 这个预处理命令,可以改变编译器的默认对齐数
#include <stdio.h>
#pragma pack(1) //设置默认对齐数 1
struct s
{
char c1;
int i;
char c2;
};
#pragma pack() // 取消默认对齐数,还原为默认
int main()
{
printf("%d", sizeof(struct s)); // 6
return 0;
}
5. 结构体实现位段
5.1 什么是位段
位段与结构体相似,只有两处不同:
(1) 位段的成员必须是 int ,unsigned int 或者 signed int ,在C99中,位段成员的类型也可以是其他成员
(2) 位段的成员名后面有一个冒号和一个数字
例:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
其中,冒号后面的数字代表所占多少bit位,(注意:数字 不能超过数据类型所占字节的限制)
位段A 所占内存:
printf("%d", sizeof(struct A)); // 8
5.2 位段的内存分配
(1) 位段的成员可以是 int , unsigned int, signed int , 或者是char 等类型(C99之后)
(2) 位段的空间按照4个空间( int )或者1给空间( char ) 的方式开辟
(3) 位段涉及很多不确定性的因素,位段是不跨平台的,注重可移植性的程序要避免使用位段
例:
struct s
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
struct s m = { 0 };
m.a = 10;
m.b = 12;
m.c = 3;
m.d = 4;
// 空间是如何开辟的呢?
5.3 位段的跨平台问题
(1) int 位段作为有符号数还是无符号数是不确定的
(2) 位段中最大位的数目不能确定(如:16位机器最大16,32位机器最大32,如果写成27,在16位机器上会出问题)
(3) 位段中的成员在内存中从左向右分配,还是从右向左分配 标准尚未定义
(4) 当一个结构中包含两个位段 且 第二个位段成员比较大,无法容纳第一个位段剩余的位时,时是舍弃剩余的位还是利用,这是不确定的
总而言之,与结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是存在跨平台的风险
5.4 位段的应用 ----- IP数据报
5.5 位段使用的注意事项
位段的多个成员共用一个字节,所以有些成员的起始位置并不是某个字节的起始位置,那么这些位置是没有地址的。内存为每个字节分配一个地址,但是一个字节内部的bit位是没有地址的
所以不能对位段成员使& 操作符,所以也不能使用scanf直接给位段的成员输入值,只能先输入放入一个变量中,然后赋值给位段的成员
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct A sa = { 0 };
// scanf("%d",&sa.b); 这是错误的!!!错误的!!!错误的!!!
// 正确操作:
int b = 2;
scanf("%d", &b);
sa.b = b;
return 0;
}