目录
1 结构体的声明和定义
2 结构体的自引用
3 结构体成员访问操作符
4 内存对齐
4 结构体传参
5 位段
什么是结构?结构也就是元素的集合,在C语言里面,结构体里面的可以有多个变量,类似于集合中的元素,结构体里面的元素被叫做成员变量,成员变量可以是不同类型的多个变量,那么创建好结构体之后,定义的就是结构体变量,那么结构体的创建用到的是关键字struct,创建如下:
struct teg
{
member list;//成员变量
}variable-list;//结构体变量
variable-list在创建的时候是不用写的,这种写法是定义结构体的同时创建好结构体变量,需要注意的是分号不能丢,tag标签一般都要写,不然就是匿名结构体了。
匿名结构体,匿名结构体是结构体的一种特殊声明,匿名声明,特点是这种结构体只能使用一次,如下代码:
struct
{
int age;
char name[20];
}x;//匿名结构体 只能使用一次
struct
{
int age;
char name[20];
}*p;
int main()
{
p = &x;//类型不兼容 只用一次 不常用
return 0;
}
当我们创建了两个成员变量是一样的匿名结构体变量的时候,使用结构体指针想要取地址就会发现系统报警告说类型不兼容,这是匿名结构体的特点。当然,用了一次之后不想丢弃这个结构体也是有办法的,使用重命名typedef:
typedef struct
{
int age;
char name[20];
}x;
这样结构体就重新拥有名字了,就和平常创建的结构体变量是一样的了,创建结构体变量的时候像
x St = ……; 就可以了,但是实际写代码的时候用的时候一般不用匿名结构体,毕竟它的特点就只有一个——只能用一次。
比如我们要创建一个结构体表示学生的一些信息,像这样:
struct Stu
{
char name[20];//名字
int age;//年龄
float score;//成绩
char id[20];//学号
};
创建好了之后我们使用的时候要进行初始化,初始化可以分为两种情况
一是按照成员变量的顺序进行初始化:
struct Stu s1 = { "zhangsan",18,99.9,"20240123" };
二是使用操作符按照自己的想法进行初始化:
struct Stu s2 = { .score = 98.9,.age = 18,.id = "20240124",.name = "lisi" };
当然,如果你嫌完整初始化太麻烦了的话,直接给0也是可以的:
struct Stu s3 = { 0 };
套娃知道吧?main里面有个main知道吧?会崩溃知道吧?结构体里面有个自己程序也是会崩溃的,像这样:
struct Stu
{
char name[20];//名字
int age;//年龄
double score;//成绩
char id[20];//学号
struct Stu next;
};
如果想不明白就想这个问题:sizeof(struct Stu)等于多少?
在数据结构里面,会涉及到链表的内容,我们也是通过结构体实现的,但是不是这种套娃的形式,我们确实可以通过结构体的自引用找到下一个结构体,但停不下来,所以我们把结构体分为数据域和指针域:
struct Stu
{
char name[20];//名字
int age;//年龄
double score;//成绩
char id[20];//学号
struct Stu* next;
};
是的,就是加个*的事儿,结构体存下一个结构体的地址,这样循环往复,我们就可以像链条一样,挨个挨个找到我们需要使用的数据。
在结构体自引用的过程中也包括了typedef的重命名过程,像这样:
typedef struct
{
int data;
Node* next;
}Node;
有错误吗?当然是有的,在重命名好之前,Node就已经是成员变量了,但是实际上此时的结构体还没有真正拥有名字,所以这段代码是错误的。
创建好结构体之后我们就需要访问,使用该结构体了,那么结构体成员访问操作符有哪些呢?
有两个,点操作符和箭头操作符:
struct Stu
{
int age;
}*p;
int main()
{
struct Stu stu = { 18 };
p = &stu;
printf("%d\n", stu.age);
printf("%d\n", p->age);
return 0;
}
综上:点操作符的右边是成员变量,左边是结构体变量,箭头操作符的右边是成员变量,左边是结构体变量指针。
上文提到的,内存对齐是一大考点,也是热门的话题,内存对齐是用来计算结构体的大小的。
那么内存对齐的规则有:
i) 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
ii) 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
iii) 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍
iv) 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
第一条规则的意思就是计算大小的时候,是从0开始计算的,偏移量从0开始,以1递增,那么什么是对齐数呢?
对齐数就是系统默认的对齐数和成员变量大小中的较小的值,在VS里面默认对齐数是8,但是在Linux环境下的gcc编译器是没有默认最大对齐数的,对齐数就是成员变量的大小。
代码1:
struct Stu
{
char a;
char b;
int c;
};
int main()
{
printf("%zd\n", sizeof(struct Stu));
return 0;
}
运行结果是8,偏移量从0开始,那么char 类型占一个字节,比8小,所以对齐1个字节,那么0对应的字节就被char a占了,同理,1对应的字节被 char b占去了,那么接下来的偏移量是2 3 4 5 6 ,int c占4个字节,比8小,所以对齐数是4,那么根据第二条规则,int c应该从4开始对齐,所以占去了 4 5 6 7四个字节,那么0 - 7一共是8个字节,所以sizeof(struct Stu)的结果就是8。
代码2:
struct Stu
{
char a;
int b;
char c;
};
int main()
{
printf("%zd\n", sizeof(struct Stu));
return 0;
}
运行结果是12,偏移量从0开始,那么char a占0对应的字节,1 2 3 都不是4的整数倍,所以int b占去了4 5 6 7 四个字节,char c就占去了8对应的字节,那么现在的字节数是0 - 8,一共9个字节,那答案是9吗?指定不是,根据第三条规则,最大对齐数是4,9不是4的整数倍,所以就继续浪费空间,直到12,因为12是4的整数倍,所以运行结果是12。
代码3:
struct S3
{
double a;
char c;
int i;
};
struct S4
{
char a1;
struct S3 s3;
int i;
};
int main()
{
printf("%zd\n", sizeof(struct S4));
return 0;
}
运行结果是32,根据123规则我们可以得出s3的大小是16,那么char a1占0对应的字节,根据第四条规则,s3对齐不是根据16对齐,是根据成员变量最大的大小来对齐,那么就是8,所以8 - 23是s3占用的字节,24 - 27是int对应的字节,总字节数是28,不是8的整数倍,一直浪费到32,ok了就。
为什么存在内存对齐?
第一个原因:
平台原因(硬件原因),不是所有的硬件都能访问所有地址的数据的,有些硬件只能访问特定地址的数据,所以如果没有对齐好,可能就会访问失败。
第二个原因:
为了提高访问效率,因为有些硬件只能访问特定地址的位置,那么如果数据的位置不是在特定地址,是在特定地址的左右两边,这样就会导致原本只需要访问一次就可以获取的数据需要进行多次访问,就会降低性能。
故可以将内存对齐的存在理解为是空间换取时间的作法。
实际写代码的时候,我们着重将同一类型的放一起,浪费的空间就没有那么多。
内存对齐中涉及到的是偏移量,比如我们想要直到某一个成员变量对于起始地址的偏移量是多少我们就可以使用offsetof函数,这个函数就是专门计算起始偏移量的:
struct S4
{
char a1;
struct S3 s3;
int i;
};
int main()
{
printf("%zd\n", offsetof(struct S4,a1));
printf("%zd\n", offsetof(struct S4,s3));
printf("%zd\n", offsetof(struct S4,i));
return 0;
}
具体细节可以去cplusplus网站查看。
我们知道默认对齐数有的编译器有的编译器没有,而默认对齐数是可以自行修改的。
#pragma pack(1)
struct S5
{
char i;
int a;
char j;
}s5;
int main()
{
printf("zd\n", sizeof(s5));
return 0;
}
运行结果就是6,那么取消就在后面加一个:
#pragma pack()
就行了。
struct S
{
int data[1000];
int num;
}s = { { 1,2,3,4,5 },1000 };
void print1(struct S s)
{
printf("%d\n", s.num);
}
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s);
print2(&s);
return 0;
}
打印都是没有问题的,探讨的是结构体传参是传值好还是传地址好?
我们说形参是实参的一份临时拷贝,也就是说调用该函数的时候会有两个结构体,可能你会觉得没有什么,可是如果结构体一大,浪费的空间不仅很大,内存还要为了该函数压栈,就很浪费时间,所以结构体传参,传地址优于传值。
struct A
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
位段的写法就是char a : 任意数字,当然这里的char 也是可以换成其他的,那么关于位段:
i) 使用位段只能用int unsigned int signed int char
ii) 位段不具有可移植性,因为不同平台的位段使用规则不一样,所以要避免跨平台使用
iii) 位段在内存中的开辟是以4个字节(int)或1个字节(char)开辟的
但是实际使用的时候有许多需要注意的点,例如:
1: int 是unsigned int 还是 signed int是未知的
2:位段中的最大位数和机器有关,32位机器最大32,16位机器最大16
3:当一个结构体包含两个位段的时候是,其中一个位段占据了较大的空间导致剩余的位不够下一个位段使用的时候,是继续使用剩余位数还是舍弃剩下的位数在新开辟一个空间这是不确定的。
4 :位段中的成员在内存中的分配是从右往左还是从左往右这是不确定的
5:位段中可能多个成员共用一个字节,但是取地址的时候都是从首地址开始的,所以不能对位段成员进行取地址,这是错误示范:
正确做法是先赋值给一个临时变量,然后赋值给位段中的成员:
struct A
{
int a : 2;
int b : 2;
int c : 10;
int d : 30;
};
int main()
{
struct A s = { 0 };
int b1 = 0;
scanf("%d", &b1);
s.b = b1;
printf("%d", s.b);
return 0;
}
Tips:位段在计算的时候也要考虑内存对齐
感谢阅读!