前言
一.结构体
1.结构体的定义与声明
2.结构体的自引用
3.结构体的初始化
4.结构体的大小及内存对齐
5.结构体的传参
前言
由于数组只能保存同种类型的数据,为了帮助我们保存不同类型的数据,将他们整合在一起,用来描述一个对象,就有了结构体这样的概念。
一、结构体
1.结构体的声明
结构体是不同类型的数据的集合,用来描述同一个对象。这些类型的数据叫做结构体的成员变量。他们声明示例如下:
struct Student
{
char name[20];
int age;
char sex[5];
float score;
} s1, s2, s3;//s1, s2, s3 是三个结构体变量 - 全局变量
int main()
{
struct Student s4, s5, s6;//s4, s5, s6 是三个结构体变量 - 局部变量
return 0;
}
如图所示,其中的s1,s2,s3是全局变量,s4,s5,s6是局部变量。
我们发现创建Student变量时,定义的代码过长 每次都要输入struct XXX 这样的代码,于是我们又发明了typedef关键字 避免代码的定义过于繁琐,实例如下:
struct Student
{
char name[20];
int age;
char sex[5];
float score;
} s1, s2, s3;//s1, s2, s3 是三个结构体变量 - 全局变量
typedef struct Student
{
char name[20];
int age;
char sex[5];
float score;
} Stu;
int main()
{
struct Student s4, s5, s6;//s4, s5, s6 是三个结构体变量 - 局部变量
Stu s7, s8;
return 0;
}
假如我们写代码的时候,不小心漏掉了Student结构体名称,结果发现仍然编译通过,创建的变量也可以正常使用,但是后来当我们再想在main函数里创建局部变量时发现无法创建,说明只能使用当时创建的全局变量,这也符合C语言的语法规则。实例如下:
struct
{
char name[20];
char author[12];
float price;
}b1, b2;
2.结构体的自引用
结构体中嵌套另一个结构体肯定可以,如果包含自己会发生什么情况?
struct Node
{
int data;//数据
struct Node n;//下一个节点
};
int main()
{
printf("%zd\n", sizeof(struct Node));
}
结果我们发现系统报错
其实不只是未定义的问题,如果我们仔细思考,一个结构体嵌套自己,那他的大小到底是多少?
这个问题没有答案,所以这样肯定是不行的,正确的方式应该是这样:
typedef struct Node
{
int data;//存放数据-数据域
struct Node* n;//存放下一个节点的地址-指针域
}Node;
int main()
{
printf("%zd\n", sizeof(Node));
return 0;
}
这也是顺序存储结构之一——链表的声明。
3.结构体变量的定义和初始化
结构体定义方式有两种,可分别定义为全局变量和局部变量,而初始化也可分为局部初始化和全部初始化,实例如下:
struct Point
{
int x;
int y;
}p1 = {1,2};//定义时按照固定顺序直接赋值
struct Stu
{
char name[15];//名字
int age;
};
struct Node
{
int data;
struct Point p;
struct Node* next;
};
int main()
{
int a = 10;
int b = 20;
struct Point p2 = {a, b};
struct Stu s = { "zhangsan", 20 };//按照固定顺序全部初始化
struct Stu s2 = { .age=18, .name="如花"};//乱序全部初始化
struct Stu s3 = {"abc" };
s3.age = 30;//定义时初始化一部分,后续乱序赋值
printf("%s %d\n", s.name, s.age);
printf("%s %d\n", s2.name, s2.age);
printf("%s %d\n", s3.name, s3.age);
struct Node n = { 100, {20, 21}, NULL };//按照固定顺序全部初始化
printf("%d x=%d y=%d\n", n.data, n.p.x, n.p.y);
return 0;
}
运行结果如下:
4.结构体的大小及内存对齐
C语言中采用offsetof 自定义宏获取结构体成员变量偏移量的大小,先看以下代码:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
printf("%d\n", sizeof(struct S1));
printf("%d\n", offsetof(struct S2, c1));
printf("%d\n", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
printf("%d\n", sizeof(struct S2));
return 0;
}
上图中两个结构体中成员变量看似相同,只是顺序不同,但他们得到的结构体大小和偏移量却是不同的,这也反映了结构体中有着特殊的内存开辟规则,也叫对齐规则。
对齐规则如下:
那我们知道了 这两个看似相同的结构体是这样分配内存的:
那为什么要有这种对齐方式呢?这样不是会浪费一部分的空间吗?
官方给出的解释是这样的:
那我们就明白了,为了写出来的代码大部分平台都能够适用,并且提高代码的访问速度(虽然提高的不多),所以C语言采用了这样的结构对齐方式。
我们声明时尽量让小一点的单元放在一起,尽可能减少开辟的空间~~
下面再举两个小例子~~
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
答案分别是16 32
由于适应不同场景下的需要,编译器也提供了我们自己修改默认对齐数的方式~~采用的是#pragma pack()
#pragma pack(1)
struct S2
{
char c1;//1 1 1
int i;//4 1 1
char c2;//1 1 1
};
#pragma pack()
int main()
{
//printf("%d\n", sizeof(struct S1));//8
printf("%d\n", sizeof(struct S2));//12
//printf("%d\n", sizeof(struct S3));//
//printf("%d\n", sizeof(struct S4));
return 0;
}
修改后,s2的各个成员变量的默认对齐数就成了1 S2的最大对齐数也成了1 所以S2 结构体大小就成了6~
注意!!!更改完默认最大对齐数的值后记得恢复!!!
5.结构体的传参
结构体的传参一共有两种方式:传值调用和传值调用 请看代码:
struct S
{
int data[1000];
int num;
};
void print1(struct S t)
{
printf("%d %d %d %d\n", t.data[0], t.data[1], t.data[2], t.num);
}
void print2(const struct S * ps)
{
printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
}
int main()
{
struct S s = { {1,2,3}, 100 };
print1(s);//传值调用
print2(&s);//传址调用
return 0;
}
结果如下:虽然两种方法都能够成功调用 并满足我们的需求,但是还是推荐传地址调用的方式~
二、位段
1.位段的定义
由于结构体的内存对齐的方式浪费了大量空间,所以人们又发明了用结构体实现的位段 位段的出现就是用来节省空间的。
而位段的成员变量只能是int或char类型,并采用int/char XXX: n(n指的是n个比特位)这样的方式来声明位段!这也方便了内存空间开辟和访问~
2.位段的内存对齐
代码如下:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(struct S));
return 0;
}
我们不得不去猜测 内存到底是如何存放的?又是如何减少开辟的空间的?
在VS环境中,位段是这样存放的~
由此可见,VS编译器是从右向左存放数据,一旦遇到较大的不够的内存就会舍弃,并且由于4个二进制位一个16进制位以及小端存储的规则,就产生了上图的内存存放~
但是我们想想,其他编译器是如何存放位段的?从右向左还是从左向右,小端还是大端?会不会舍弃不够用的空间?我们使用前并不知道,这就要说到位段的缺点~
3.位段的缺陷
所以如果代码要跨平台使用,不要使用位段,为了节省空间还是可以使用的~
4.位段的应用
三、枚举类型
1.枚举定义
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day ,enum Sex, enum Color 都是枚举类型。
也就是枚举常量,这些{}中的内容是枚举类型的可能取值,这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。例如:
enum Sex
{
//枚举的可能取值
MALE=3,//枚举常量
FEMALE,
SECRET
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
enum Sex sex = SECRET;
printf("%zd\n", sizeof(sex));
return 0;
}
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;//不可以这样直接赋值!!
2.枚举常量的优点
所以如果项目比较大,常量之间关系复杂,不妨试试使用枚举常量~
四、联合体类型
1.联合体类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体),示例如下:
由此可见,联合体的各个成员变量都是公用同一块空间的,所以联合体也在很大程度上省掉了空间,弥补了结构体的不足~
由于使用的是同一块空间,所以联合体的成员变量不会同时使用~
2.联合体的应用
判断当前计算机的大小端存储,代码如下:
int check_sys()
{
union
{
char c;
int i;
}u;
u.i = 1;
return u.c;//返回1表示小端,返回0表示大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
在内存中是这样分布的~
3.联合体的大小计算、
联合的大小至少是最大成员的大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。例如:
所以联合体还是比较严谨的~
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了C语言常见自定义类型的使用,如果问题欢迎评论区讨论留言~~