一.结构体概念
结构体和数组类似,是一种聚合类型。不同的是结构体成员变量类型可以不同。
二.结构体的声明
struct tag//tag需要做到见名知意,可以省略,但不建议省略。起到了标签的作用
{
member-list;//成员列表,不可省略,C中不可以定义孔结构体
}variable-list;//变量列表,建议省略,结构体变量按需定义
例如:描述一个学生信息
struct stu //声明结构体类型,这个类型包括以下信息
{
char school[20];//学校
char name[20]; //姓名
char id[20]; //学号
int age; //年龄
char sex[4]; //性别
//这些称为结构体成员变量
}; //分号不能丢
struct stu s; //定义结构体变量
结构体成员变量可以是任何类型,甚至是其他结构体类型。
特殊的声明:在定义结构体的时候可以不完全的声明
struct //匿名结构体类型,省略了标签tag,没有错误但是一般情况下不建议这样做,容易混淆
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20],*p;
试试下面这样可以吗
p = &x;
结构体变量之间是不可以互相赋值的,因为不同的结构体类型在声明的时候开辟的内存空间大小可能不同,编译器会将两个 声明当成不同的类型。所以是非法的。
三.结构体成员的访问
结构体变量的成员变量是通过(.)操作符进行访问的,操作符左边是结构体变量右边是结构体成员变量
例如:
struct Stu
{
char name[20];
int age;
};
struct Stu s;
s.age = 20; //通过 . 操作符访问结构体成员变量并赋值
strcpy(s.name, "zhangsan"); //通过 . 操作符访问结构体成员变量并赋值
假如得到的是一个结构体指针,就可以用" . "或"->"操作符来访问结构体成员:
struct Stu
{
char name[20];
int age;
};
struct Stu s;
void print(struct stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
printf("name = %s age = %d\n", ps->name, ps->age);
}
四.结构体的自引用
先看两段代码
//代码1
struct Node
{
int data;
struct Node next;
};
//问:sizeof(struct Node)结果是多少?
//代码2
struct Node
{
int data;
struct Node* next;
};
//问:sizeof(struct Node)结果是多少
所以,在结构体自引用的时候一定要是引用自身结构体的指针。否则会不断在栈上递归式地开辟空间,耗时耗空间相当大。
五.结构体的不完整声明
struct B;//结构体不能再声明之前就使用
struct A
{
int a;
int b;
int arr1[4];
int arr2[2][2];
struct B* p;
}
struct B
{
int a;
int b;
char c;
struct A* q;
};
六.结构体变量的定义和初始化
开头前面提到结构体和数组一样是聚合类型,所以结构体的初始化和数组可以说是一模一样
每个元素初始化的值用逗号隔开,每个元素的初始化用花括号括起来,大不了就是花括号套花括号。
但是结构体禁止整体赋值!
struct A
{
int a;
int b;
int arr1[4];
int arr2[2][2];
}x = { 10, 20, { 1, 2, 3, 4 }, { { 1, 2 }, { 3, 4 } }}; //结构体嵌套初始化
struct A y = { 1, 2, { 11, 22, 33, 43 }, { { 10, 20 }, { 30, 40 } } }; //结构体嵌套初始化
七.结构体内存对齐
(1)什么是内存对齐?
看下面两个结构体大小相同吗?
struct A
{
char a;
int b;
char c;
};
struct B
{
char a;
char c;
int b;
};
int main()
{
printf("%d\n", sizeof(struct A));
printf("%d\n", sizeof(struct B));
system("pause");
return 0;
}
很明显不相同,但是两个结构体成员完全一样只不过声明顺序变了怎么就能导致两个结构体类型大小不同?
没错!就是因外内存对齐!
(2)为什么要有结构体对齐?
由于硬件规定,CPU在访问内存的时候只能从4的整数倍处访问。本来访问未对齐的内存需要两次内存访问才能完成,但访问对齐之后的内存只需要一次就能完成。所谓内存对齐其实就是用用空间换取时间的方法,牺牲内存上部分空间,大大提高程序运行效率。
另一方面就是如果内存不对齐可能会影响代码在平台之间的移植。
(3)怎么对齐?偏移量整除自身对齐数就是对齐了
偏移量:数据刚放入内存的位置偏移量为0,后面存入的输出与初始位置的”距离“就是叫做偏移量。
对齐数:编译环境有默认的对齐数(VS默认是8,Linux默认是4),一个结构体成员自身大小与编译环境默认大小之间的较小值。
(4)对齐规则:记住四句话:
1.第一个元素有对齐数。默认对齐,偏移量为0.
2.每一个元素的起始偏移量必须为自身对齐数的整数倍,如果不是就往后移。
3.结构体总大小必须为成员最大对齐数(结构体每个成员都有对齐数,其中的最大者)的整数倍。
4.如果嵌套了结构体,嵌套的结构体对齐到自身最大对齐数的整数倍处,结构体的整体大小为所有最大对齐数的整数倍。
举个栗子,有两个结构体A和B,A嵌套在B中,A就要对齐到自身最大对齐数(本结构体成员的最大对齐数)的整数倍处,而B结
构体的整体大小最所有成员最大对齐数的整数倍。其实就是第三句话。
(5)其实简单点可以这样理解,存放一个数据的时候求出其偏移量,再用偏移量除以大小,如果能整除就将数据存到这个位
置,若果不行就往后移。最后要保证总大小是最大对齐数的整数倍。
(6)关于#pragma pack()的使用,这是用来自定义对齐数的,只不过括号里面能填的只有1,2,4,8或者16
无参#pragma pack()放在有参#pragma pack()之后可以恢复默认对齐数。
八.结构体传参
记住传参传指针,因为在传参的时候参数是需要压栈的。如果结构体很大,要是传参不传地址的话那压栈的开销就是很大的。会直接影响性能。
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);
printf("%d\n", sizeof(ps->data));
for (int i = 0; i < 4; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
int main()
{
print1(s);
print2(&s);
system("pause");
return 0;
}
九.位段:表示的是结构体实现位段的能力
位段的特点:成员之间本着能挤就挤的原则占用比特位
struct A
{
char a:2;
int b:5;
int c:7;
int d:1;
}
问sizeof(struct A)的结果是多少?结果是8!
因为位段在声明的时候先开辟一个类型(只能是int,unsigned ,signed或char)的空间,然后每个成员按其后面的数字一次
占用比特位,直到比特位不够用的时候再开辟新的空间。每次开辟空间只能是4个字节或1个字节
上面的例子先开辟一个char型一个字节大小的空间,前两个比特位存放a,存放下一个成员的时候要开辟4个字节的空间,
但同时考虑到内存对齐,所以其实偏移量要后移3个字节,然后再开辟一个整形的空间,前五个比特位存放b,紧挨着存c和d。
不需要再为c,d开辟新的空间。
使用位段最大的缺点就是跨平台问题,因为一个字节中比特位的分配是从左往右还是从右往左在不同机器上是不确定的。
第二点,第一个在存放完第一个成员还是剩余比特位但不够第二个成员时,第二个成员是紧挨着上个成员存放还是开辟新的字节
在不同机器上也是不确定的。
十.联合
联合的特征是其成员共用同一块内存空间,所以一个联合变量的大小至少是其最大成员的大小。
在计算联合体大小的时候也要考虑内存对齐。
union Un
{
char c;
int i;
};
//联合类型变量的定义
union Un un;
//打印变量大小
printf("%d\n",sizeof(un));
十一.枚举
枚举类型成员都代表常数,默认第一个是0,往后依次加1,。但如果其中一个被赋值之后后面的成员就按照这个被赋值的成员依次 加1,
enum Day
{
Mon; //0
Tues;//1
Wed;//2
Thur;//3
Fri;//4
Sat;//5
Sun;//6
};
enum Color
{
RED;//0
FORGIVE=2;
BLUE;//3
}