自定义类型:结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学生,描述⼀本书,这时单⼀的内置类型是不行的。 描述一个学生需要名字、年龄、学号、身高、体重等; 描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
1.结构的声明
struct tag
{
member-list;
}variable-list;
描述一个学生:
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
1.1 结构体变量的定义和初始化
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化
struct Point p3 = {10, 20};
//代码2
struct Stu
{
char name[20];
int age;
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age = 20, .name = "lisa"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4, 5}, NULL};//结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
1.2 结构体访问操作符
1.2.1 结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1, 2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
使用方式:结构体变量.成员名
1.2.2 结构体成员的间接访问
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x: %d y: %d\n", ptr->x, ptr->y);
return 0;
}
使用方法:结构体指针->成员名
综合举例:
#inlcude <stdio.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "lisa");
ps->age = 20;
}
int main()
{
struct Stu s = { "zahngsan", 20};
print_stu(s);
set_ stu(&s);
print_stu(s);
return 0;
}
1.3结构的特殊声明
在声明结构时,可以不完全声明。
//匿名结构体类型
struct
{
int a;
int b;
float c;
}x;
//匿名结构体指针类型
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么在上面代码的基础上,下面的代码合法吗?
p = &x;
警告:
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
2.结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
如,定义一个链表的节点:
struct Node
{
int data;
struct Node next;
};
上面的代码正确吗?如果正确,那sizeof(struct Node)是多少?
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
正确的自引用方式:
struct Node
{
int data;//数据域
struct Node* next;//指针域
};
在结构体自引用使用的过程中,夹杂了typedef对匿名结构体类型重命名,也很容易引入问题,如:
typedef struct
{
int data;
struct Node* next;
}Node;
因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决方案如下:
typedef struct Node
{
int data;
struct Node* next;
}Node;
定义结构体尽量不要使用匿名结构体了。
3.结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点:结构体内存对齐
3.1对齐规则
首先得掌握结构体的对齐规则:
//练习1
struct S1
{
char c1;
int i;
char c2;
};
由上图可知,到c2总共占用了9个字节,再根据第4条规则可知,最大对齐数为4,所以该结构体的大小是12个字节。
//练习2
struct S2
{
char c1;
char c2;
int i;
};
由上图可知,到i总共占用了8个字节,再根据规则4,最大对齐数为4,所以该结构体的大小为8个字节。
//练习3
struct S3
{
double d;
char c;
int i;
};
由图可知,到i总共占用16个字节,再根据规则4,最大对齐数为8,所以该结构体的大小为16个字节。
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
由图可知到d总共占用了32个字节,再根据规则4,最大对齐数为8,所以改结构体的大小为32个字节。
3.2为什么存在内存对齐?
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
总的来说:结构体的内存对齐是拿空间来换取时间的做法。
所以在设计结构体的时候,我们既要满足对齐,又要节省空间,要让占用空间小的成员尽量集中在一起。
例如:
struct S1
{
char c1;
int i;
char c2;
};
//变成
struct S2
{
char c1;
char c2;
int i;
};
3.3修改默认对齐数
#pragma这个预处理指令,可以改变编译器的默认对齐数。
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
4.结构体传参
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(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s);//传结构体
print2(&s);//传地址
return 0;
}
上面的print1和print2函数哪个更好?
答案是:print2
原因: