背景: 最近在重学C语言,目的是为了能看懂操作系统的底层代码,也为后续使用C语言开发一个类似redis数据库的中间件做准备,于是又重新踏上了学习C语言的道路,早在上学期间就学习过C语言,但是很久都不用了,语法和技巧都忘记了。计算机的知识真是浩如烟海,用进废退。
本次学习struct,C语言没有对象object也没有类class,只有struct提供了对象object和类class的相似功能。 struct用于 封装一个复杂物体的各个变量,例如: 学生,有姓名、学号、班级、年龄、性别等等参数,他们非常相关; 另外一个用途是 某些函数需要传入多个参数,我们把这些多个参数封装到一个结构体中,再进行传入这个结构体或结构体的指针,这样方便快捷。 结构体所表示的一条数据,类似数据库学生表的一行记录。
1. 定义一个结构体:
struct student {
char* name;
int num;
int grade;
int age;
char sex;
};
// 声明变量的同时,对变量进行赋值
struct student {
char* name;
int num;
int grade;
int age;
char sex;
} s1 = {"xiaolong",1000,1,6,'m'},s2 = {"xiaopeng",1010,1,6,'m'};
2. 定义结构体变量 并对结构体进行设置:
// 声明自定义类型的变量时,类型名前面,不要忘记加上struct关键字
struct student xiaoming;
xiaoming.name = "xiaoming";
xiaoming.num = 1001;
xiaoming.grade = 1;
xiaoming.age = 6;
xiaoming.sex = 'm';
// 这种方式需要注意 大括号里面 值的顺序,必须与struct声明时属性的顺序保持一致
struct student xiaohong = {"xiaohong",1002,1,6,'f'};
// 否则需要为每个值指定属性名
struct student lihua = { .num=1003, .name="lihua", .sex='m', .age=7, .grade=2};
// 可以修改
lihua.age = 8;
// 也可以使用typedef命令
typedef struct student {
char* name;
int num;
int grade;
int age;
char sex;
} stu;
// stu就是 struct student的别名
stu xiaohua = {"xiaohua",1020,1,6,'m'};
还有就是 多个结构体嵌套,嵌套的初始化和设置与上面一致,这里不再赘述。
3. 结构体的复制
赋值运算符( = )可将 struct 结构每个属性的值,一模一样的复制一份,拷贝给另外一个struct变量,与数组不同(使用赋值运算符复制数组,不会复制数据,只会共享地址)
如果 结构体里面 有指针类型的变量,那么复制的是 指针的值,也就是说 两个结构体 指向的是同一个内容,修改一个会影响另外一个的值。
如果 结构体里面 没有指针类型的变量,那么 复制的是 数据的值,是两份毫无关系的数据,各自修改不影响另外一个的值。
char* name ; 这种字符指针所指向的字符串是不能修改的,只能重新指向一个新的字符串。
struct student { char name[30]; short age; } a, b;
strcpy(a.name, "xiaohu");
a.age = 18;
b = a;
b.name[0] = ' ';
b.name[1] = 'l';
printf("%s\n", a.name); // xiaohu
printf("%s\n", b.name); // laohu
struct student2 { char name[30]; short age; } a2, b2;
a2.name = "xiaohu"
a2.age = 18;
b2 = a2;
b2.name = " laohu" // 内存新建了" laohu"字符串,b2的指针这次指向" laohu"
// 也就是说 a2.name 和 b2.name 不再指向同一块地址空间
printf("%s\n", a2.name); // xiaohu
printf("%s\n", b2.name); // laohu 这是为什么
struct student3 { char* name; short age; } a3, b3;
a3.name = "xiaohu";
a3.age = 18;
b3 = a3;
// 这是为了 修改字符串所做的测试, 结论是 无法修改,虽然能通过编译,但是运行失败,显示段错误。
*(b3.name+0) = ' ';
*(b3.name+1) = 'l';
printf("%s\n", a3.name);
printf("%s\n", b3.name);
4. 结构体数组 和 结构体指针
如果将 struct 变量作为参数 传入函数,则进行了 结构体的复制,一是会占用内存,二是函数内修改只是修改副本,而不会影响函数外部的原始数据,所以 一般情况下,我们 使用 struct 指针 作为参数 传入函数中。
void happy(struct student* s){
(*s).age = (*s).age + 1; // . 优先级比 * 高,所以需要使用()
}
happy(&xiaoming);
void happy2(struct student* s){
s->age = s->age + 1 ; // 或 s->age++; 自增
}
void happy2(struct student* s){
s->age++; // 自增 , -> 优先级比 ++ 高
}
对于 struct 的变量 使用 . 获取属性,对于 struct的变量指针 使用 -> 获取属性。
xiaoming.age == (*ptr).age == ptr->age
5. 通过 malloc来构造链表
struct node {
int data;
struct node* next;
};
struct node* head;
// 生成一个三个节点的列表 (11)->(22)->(33)
head = malloc(sizeof(struct node));
head->data = 11;
head->next = malloc(sizeof(struct node));
head->next->data = 22;
head->next->next = malloc(sizeof(struct node));
head->next->next->data = 33;
head->next->next->next = NULL;
// 遍历这个列表
for (struct node *cur = head; cur != NULL; cur = cur->next) {
printf("%d\n", cur->data);
}
6. 空间对齐
struct所占用的空间不是各个属性存储空间的总和,而是最大内存占用属性的存储空间倍数,其他属性会添加空位阈值对齐。
#include<stdio.h>
int main()
{
struct student {
char* name; // 8 字节
int num; // 4 字节
int grade; // 4 字节
int age; // 4 字节
char sex; // 1 字节
};
printf("student %d\n",sizeof(struct student));
struct student_other1 {
char sex; // 1 字节
int num; // 4 字节
int grade; // 4 字节
int age; // 4 字节
char* name; // 8 字节
};
printf("student_other1 %d\n",sizeof(struct student_other1));
struct student_other2 {
char sex; // 1 字节
char* name; // 8 字节
int num; // 4 字节
int grade; // 4 字节
int age; // 4 字节
};
printf("student_other2 %d\n",sizeof(struct student_other2));
return 0;
}
64位计算机
student: 8+ 4 + 4 + 4 + 4 = 24 ,最后一个 char sex 本身占用1个字节,但是为了补齐,和前面的int age 组合成为了一个 8 字节。
student_other1 : 4 +4+4+4+8 =24 , 第一个char sex 本身占用1个字节,但是为了补齐,和后面的int num 组合成为了一个 8 字节
student_other2: 8 + 8 + 8 + 8 =32 , 第一个 char sex本身占用1个字节,但是为了补齐7个字节,这次单独占用8个字节; 最后一个 int age 4字节无法和其他人凑成 8字节的倍数了,只能被空位填充,补齐4个字节,也就变成了8。
字节对齐是为了 提高读写效率,是存储空间的倍数, 示例中 最后一个是 把 占空间最大的指针放在了 属性顺序的中部,导致前面的属性和后面的属性都需要补空位才能实现对齐,而不是第一个和第二个可与其他属性拼凑来实现字节对齐,所以 第三种是32个字节。
结论是: 不要把 本身所占空间大的属性 写在 中间,而是按照各属性大小,从小到大进行书写,这样最大的写在尾部,即 存储空间递增的顺序来书写定义每个属性,这样能节省空间。
7.位字段
位字段其实就是 二进制的位,比如: 在TCP/IP协议中,有一些标志位如下图所示:
URG/ACK/RST/PSH/SYN/FIN 等标志位
下面代码仅做示例,不是真实的Linux tcp的代码:
struct flag {
unsigned int urg:1;
unsigned int ack:1;
unsigned int rst:1;
unsigned int psh:1;
unsigned int syn:1;
unsigned int fin:1;
unsigned int :6;
unsigned int head:4;
} flag;
flag.urg = 0;
flag.ack = 1;
printf("flag %d\n",sizeof(struct flag));
每个属性后面的:1 表示指定这些属性只占用 一个二进制位, :6代表保留的6位,:4代表是头部4位。
8. 弹性数组成员
如果在struct中声明数组成员,但我一开始不知道有多少,如果给定一个很大的数会浪费空间。
struct vstring {
int len;
char chars[];
};
struct vstring* str = malloc(sizeof(struct vstring) + n * sizeof(char));
str->len = n;
弹性成员的数组,必须是 struct 结构的最后一个属性。另外,除了弹性数组成员,struct 结构必须至少还有一个其他属性。