1.结构体的声明
struct tag
{member - list;
}variable-list;
我们描述一本书或者一个学生的时候可以这样写
//书 - 书名,作者,价格,序号struct book
{char _book_name[20];char _author[20];int price;char id[20];
};//学生 - 姓名,性别,年龄,学号
struct Stu
{char _name[20];char sex[10];int age;char id[20];
};
2.结构体变量的创建和初始化
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
};int main()
{// 第一种初始化方式:需要按结构体成员顺序初始化struct Stu s1 = { "欧阳",20,"男","20240319" };// 第二种初始化方式:可以自定义顺序struct Stu s2 = { .age = 21,.sex = "女",.name = "阳区欠",.id = "20240318" };//输出S1printf("name: %s\n", s1.name);printf("age : %d\n", s1.age);printf("sex : %s\n", s1.sex);printf("id : %s\n", s1.id);printf("\n");//输出S2printf("name: %s\n", s2.name);printf("age : %d\n", s2.age);printf("sex : %s\n", s2.sex);printf("id : %s\n", s2.id);return 0;
}
3.结构体的特殊声明
3.1匿名结构体类型
struct
{int a;char b;double c;
}x;struct
{int a;char b;double c;
}a[20], *p;
前面两个结构体都省略了结构体的tag(标签)
p = &x; 这行代码合理吗?
警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
3.2结构体的自引用
struct Stu
{int data;struct Stu* next;
};
3.3重命名匿名结构体 - typedef
typedef struct
{int data;Stu* nxet;
}Stu;
这样的重命名也会带来问题,就如上面的代码一样是不合法的,因为程序是由上向下运行的,所以在创建Stu* next 时Stu还未创建
解决方案:定义的结构体不使用匿名结构体
typedef struct Stu
{int data;struct Stu* next;
}Stu;
4.结构体对齐 - 计算结构体大小
4.1对齐规则
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。-VS 中默认的值为 8 -Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
- 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
下面以练习题来了解这四条规则吧
//练习1
int main()
{struct S1{ // 变量本身 vs 对齐数char c1;// 1 8 1int i; // 4 8 4char c2;// 1 8 1};printf("%zd\n", sizeof(struct S1));
}// 根据规则二我们可以算出每个类型的对齐数// 根据规则一我们开始画图,从0开始,c1为char类型对齐数为1,所以第一格就可以给他,
// 第二个成员类型为int,对齐数为4,根据规则二前面也需要保障为4的整数倍处,所以在4填涂,然后留出4个格子
// 第三个成员类型为char,对齐数为1,在后面一格填图即可,最后整个结构体的大小要为最多对齐数的整数倍,所以填补到12格
// 以下练习大家可以自己画图计算试试//练习2
int main()
{struct S2{char c1; // 1 8 1char c2; // 1 8 1int i; // 4 8 4};printf("%zd\n", sizeof(struct S2));//8//练习3struct S3{double d; //8 8 8char c; //1 8 1int i; //4 8 4};printf("%zd\n", sizeof(struct S3));//16//练习4-结构体嵌套问题struct S4{char c1; // 1 8 1struct S3 s3;// 8 8 8double d; // 8 8 8};printf("%zd\n", sizeof(struct S4)); //32return 0;
}
画图参考:
4.2为什么存在内存对齐
- 平台移植原因 :不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总结:用空间换时间
设计结构体时,又想满足对齐规则,又想节省空间,需要尽量将占用空间小的成员集中到一起
如:
int main()
{struct S1{char c1;int i;char c2;};struct S2{char c1;char c2;int i;};printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}// S1的大小占比为12,而S2的大小占比为8。
4.3 修改默认对齐
使用#pragma 预处理命令,可以改变编译的默认对齐数
#pragma pack(1)
struct S
{char c1;int i;char c2;
};
#pragma pack()int main()
{printf("%zd\n", sizeof(struct S)); // 6return 0;
}
5.结构体传参
struct book
{char _book_name[20];int price;
};
struct book s1 = { "狂人日记",18 };
//结构体传参
void print1(struct book s1)
{printf("%s\n", s1._book_name);
}//结构体地址传参
void print2(const struct book* ps)
{printf("%s\n", ps->_book_name);
}
int main()
{print1(s1);print2(&s1);return 0;
}
上⾯的 print1 和 print2 函数哪个好些?
答案是:⾸选print2函数。
print1 是传值调用,形参,是实参的一份拷贝,这意味着重新开辟一个空间复制一遍实参进行使用,这大大浪费了空间
print2 是传址调用,使用的数据空间还是原来的,只要根据地址找寻即可,但是为了防止在访问时更改我们需要加上const
6.结构体实现位段
6.1什么是位段
- 位段成员必须是整型,int/ unsigned int/ signed int,在C99中其他类型的位段成员也可以
- 位段成员名后面有一个冒号和一个数字
struct S
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};// S是一个位段类型,那么位段S所占内存大小位多少?
int main()
{printf("%zd\n", sizeof(struct S)); // 8return 0;
}
6.2 位段的内存分配
- 位段的成员可以是 int / unsigned int / signed int 或者是 char 等类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
//⼀个例⼦
struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
};
struct S s = { 0 };
int main()
{s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%zd\n", sizeof(struct S)); //3return 0;
}//空间是如何开辟的?// a:10 -> 01010 存储: 3 -> 010
// b:12 -> 01100 存储: 4 -> 1100
// c:3 -> 00011 存储: 5 -> 00011
// d:4 -> 00100 存储: 4 -> 0100
// 十进制 二进制// 图片没有存储的地方补0
// 最后就是 01100010 00000011 00000100
// 转换成16进制即可 0x62 0x03 0x04
6.3段位跨平台问题
- int位段被当成有符号数还是⽆符号数是不确定的。
- 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
6.4段位的使用注意事项
//段位是几个成员共用一个字节,所以无法通过scanf取地址输入struct S
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};int main()
{struct S s = { 0 };//scanf("%d", &s._a); //err//正确的方式int b = 0;scanf("%d", &b);s._a = b;return 0;
}