内置类型并不能满足在复杂条件下的使用,比如对人的描述:年龄,身高,电话等信息,C语言提供了自定义类型:结构体、联合体、枚举
一、结构体
1.1、结构体基本定义
结构是一个或多个变量的集合,这些变量称为成员变量,这些变量可以是不同的类型。
而数组是一组相同类型元素的集合,结构的每个成员可以是不同类型的变量,这点要注意区分下。
1.2、结构体声明
struct 是关键字;
tag是结构标记,是我们想要的名字,一般表示某种信息,比如描述一个人,people;
{}中的member - list是成员列表,可以有一个或多个成员,类型可以不同;
variable - list是变量列表。
// struct tag
// {
// member - list;
// }variable - list;
// struct 是关键字,tag是结构体标签,
// {}中的member - list是成员列表,
// variable - list是变量列表。
(1) 这里只是创造了一个类型struct people,类似于int、double等是类型。
struct people
{
//成员列表可以放描述某人的属性信息,可以放多个成员
char name[15];
int age;
}; //注意这里有分号
//这里只是创造了一个类型 struct people
//比如int double 是类型
(2) 这里的代码在创建struct people类型的同时,创建两个结构体变量p1和p2,并且是全局变量。
struct people
{
char name[15];
int age;
}p1, p2; //创建类型的同时,创建两个变量p1和p2
//这里的p1和p2是利用创建的类型struct people,创建了两个结构体变量
//p1和p2是全局变量
(3)结构体创建的全局变量和局部变量。
struct people
{
char name[15];
int age;
}p1, p2; //创建类型的同时,创建两个结构体变量p1和p2
//p1和p2是全局变量
int main()
{
struct book
{
char name[20];
float price;
int number;
}b1, b2; //b1和b2是局部变量
struct people p3; //有了类型之后,使用struct people类型创建结构体变量p3,p3是局部变量
return 0;
}
1.3、结构体特殊声明
(1)创建结构体类型省略结构体标记时,称为匿名结构体类型,此时结构体变量无法创建。因为结构体标记表示声明一种新的结构体类型,如果省略,结构体类型变量无法创建。
//匿名结构体类型
struct
{
char name[15];
int age;
}s1,s2;
//创建匿名结构体类型时创建两个变量s1和s2,
// 只能使用s1和s2
//后面不能再通过结构体类型创建变量,比如:struct s3,创建s3这是不可以的。
(2)两个匿名结构体成员一致,但是编译器会将两个声明当成完全不同的两个类型。
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
//匿名结构体定义一个数组a,数组a中可以放20个数据,
//定义了p,p是结构体指针。 *和匿名结构体类型结合起来称为结构体指针
int main()
{
//两个匿名结构体放置的成员一样,但是编译器会把上面的两个声明当成完全不同的两个类型。
p = &x; //是非法的。
return 0;
}
1.4、结构体自引用
(1)在结构体内部包含自己是错误的,会造成死循环。而通过在结构体成员中包含一个同类型的结构体指针,是允许的,可以达到自引用的效果。
struct Node
{
int data;
struct Node next; //结构体中包含自己类型是错误的
};
int main()
{
int sz = sizeof(struct Node); //err
return 0;
}
//通过在结构体成员里包含一个同类型的结构体指针,自引用
struct Node
{
int data;
struct Node* next; //结构体中指针能够找到自己同类型的节点,自引用。
};
(2) 结构体类型太长,可以用typedef对其进行重命名,注意要先有结构体类型才能重命名。
//err,要先有结构体类型,才能重命名
typedef struct
{
int data;
Node* next;
}Node;
typedef struct Node
{
int data;
struct Node* next;
}Node;
//创建结构体类型struct Node的同时,对struct Node重命名为Node,
//以后: struct Node n1; 和 Node n1; 这两种写法是一样的
(3)同样,可以定义结构体指针。
①可以在定义结构体类型的同时,创建一个结构体指针并重命名;
② 也可以先创建一个结构体,再创建结构体指针,重命名。
//1、定义了一个结构体指针的同时,重命名为linklist
typedef struct Node
{
int data;
struct Node* next;
}*linklist;
//2、定义一个结构体类型
struct Node
{
int data;
struct Node* next;
};
//重命名结构体指针为linklist
typedef struct Node* linklist;
1.5、结构体变量的定义和初始化
(1)结构体初始化
① 可以在创建结构体类型的同时,创建一个结构体类型的变量,并对其初始化。
② 也可以利用结构体类型创建一个变量,在对其初始化。
struct point
{
int x;
int y;
}p1 = {2,3},p2; //创建struct point类型的同时,创建一个struct point类型的变量p1和p2,
//创建p1变量的同时对p1初始化。
int main()
{
struct point p3 = { 3,4 }; //对p3初始化
return 0;
}
(2)结构体嵌套初始化。
struct score
{
int k;
char ch;
};
//结构体嵌套
struct student
{
char name[20];
int age;
struct score s;
};
int main()
{
struct student s1 = { "lee",22,{99,'*'} }; //创建变量并初始化
printf("%s\n", s1.name);
printf("%c\n", s1.s.ch);
printf("%s %d %d %c", s1.name, s1.age, s1.s.k, s1.s.ch);
return 0;
}
1.6、结构体内存对齐
在能够对结构体使用时,还有一个问题:结构体的大小是多少?
在计算结构体大小之前,需要了解结构体内存对齐的规则:
下面通过例子介绍对齐规则。
1.6.1、计算结构体大小-1
①对于struct S1,第一个成员 “c1” 在偏移量为0的地址处,c1的大小为1byte;
②第二个成员 “i” 的大小为4byte,默认对齐数是8。对齐数是默认对齐数与成员大小的最小值,所以 “i” 对齐数是4,所以地址要对齐到偏移量是4的整数倍处,偏移量为1、2、3的位置不是4的整数倍,所以不能存储在这些偏移量处,偏移量为4的位置是对齐数4的整数倍,可以放置,“i” 大小为4byte,所以 “i” 可以存放在偏移量为4处。
③c2的大小为1byte,默认对齐数是8,所以c2的对齐数是1,所以c2的地址要对齐到偏移量是1的整数倍处,偏移量为8的位置是1的整数倍(任意整数都是1的整数倍),所以可以存储在此处,c2的大小为1byte,占用一个字节
④结构体的总大小为最大对齐数的整数倍,这里的最大对齐数是每个成员的对齐数进行比较,选择其中对齐数最大的一个。
c1对齐数为1,i 对齐数为4,c2对齐数为1,所以结构体的最大对齐数为4,总大小为4的整数倍,可以是4、8、12、16…
⑤所以最终结构体大小为12byte,其中黑色部分是未使用的内存,即浪费掉的内存。
另外可以通过offsetof计算偏移量,来验证上述分析中的偏移量是否正确。
offsetof的第一个参数是结构体类型,第二个参数是要确定偏移量的结构体成员。
struct S1
{
char c1; //1byte
int i; //4byte
char c2; //1byte
};
int main()
{
struct S1 s1; //为s1开辟空间,空间大小?
printf("%d\n", sizeof(struct S1));
//offsetof
printf("%d\n", offsetof(struct S1, c1)); //0
printf("%d\n", offsetof(struct S1, i)); //4
printf("%d\n", offsetof(struct S1, c2)); //8
return 0;
}
1.6.2、计算结构体大小-2
第一个成员 c1 偏移量为0,大小为1byte,对齐数为1;
第二个成员 c2 大小为1,默认对齐数为8,所以c2的对齐数为1,偏移量为1的整数倍,任意整数都是1的整数倍。
第三个成员 i 的大小为4,默认对齐数是8,所以 i 的对齐数为4,偏移量为4的整数倍,可以是4、8、12、16...。
结构体的总大小为最大对齐数的整数倍,这里的最大对齐数是每个成员进行比较,选择其中对齐数最大的一个,比如 c1 对齐数为1,c2 对齐数为1,i 对齐数为4,所以结构体的最大对齐数为4,总大小为4的整数倍,可以是4、8、12、16…
最终s2的大小为8byte。
1.6.2、计算结构体大小-3-存在结构体嵌套的情况
先分析S3的情况
d偏移量为0,d的大小为8byte,默认对齐数是8,所以d的对齐数为8;
c的大小为1,默认对齐数为8,所以c的对齐数为1,偏移量为1的整数倍;
i的大小为4byte,默认对齐数是8,所以i的对齐数是4,偏移量为4的整数倍,
结构体的总大小为最大对齐数的整数倍,也就是8的整数倍。
所以最终S3的大小为16byte。
对于S4来说,嵌套了结构体。
如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数)的整数倍。
拿s3举例,也就是说嵌套的结构体的对齐数为自己成员的最大对齐数,s3里面成员的最大对齐数是8,所以s3的对齐数为8 。
对于S4来说:
c1偏移量为0,c1的大小为1byte;
S3的大小为16byte,s3的最大对齐数为8,默认对齐数是8,所以s3的对齐数是8,偏移量是8的倍数;
d的大小为8byte,默认对齐数是8,所以d的对齐数是8,偏移量为8的整数倍,
结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数)的整数倍。
S4中,最大对齐数为8,所以结构体大小是8的整数倍。
最终,S4的大小为32byte。
1.6.4、结构体内存对齐的原因
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
因此在设计结构体的时候,既要满足对齐,又要节省空间,所以在允许的情况下,让占用空间小的成员尽量集中在一起,在一定程度上节省空间。
比如1.6.1中和1.6.2中两个例子,让占用空间小的c1和c2集中在一起。
1.7、修改默认对齐数
默认对齐数是可以修改的,使用#pragma 这个预处理指令,可以改变默认对齐数。
注意:将默认对齐数修改、使用之后,将默认对齐数在改为vs的默认对齐数。
//将默认对齐数改为4
#pragma pack(4)
struct S
{
int i;
double d;
};
//vs默认对齐数是8,在()中不填数字,默认vs当前对齐数
#pragma pack()
//默认对齐数改为1,意味着不对齐,因为任何数都是1的整数倍
#pragma pack(1)
struct S1
{
char c;
int i;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S));
printf("%d\n", sizeof(struct S1));
return 0;
}
1.8、结构体传参
结构体传参有两种形式,一种是传结构体(传值),另一种传结构体地址(传址);相应的,在接收结构体也有两种形式:结构体和指针。
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象(传值调用时)的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。
因此结构体传参的时候,要传结构体的地址(传址调用)。
struct S
{
int data[1000];
int num;
};
void print1(struct S ss)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ss.data[i]);
}
printf("%d ", ss.num);
}
void print2(const struct S* ps)
{
int i = 0;
for(i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d ", ps->num);
}
int main()
{
struct S s1 = { {1,2,3},100 };
print1(s1); //传值调用,形参是对实参的一份临时拷贝,会创建一份独立空间存放s1
//
print2(&s1); //传址调用,传递地址,地址是4byte(32位)或者8byte(64位)
//节省空间,提高效率
//为了防止传址调用,值被修改,加const
return 0;
}