前言
大家好,我是熊猫,今天我们来认识一下C语言中的自定义数据类型, C语言中的char,short,int,long,float,double这些类型我们大家肯定已经非常熟悉了, 这些都属于C语言自身所带的类型,但是在我们的日常生活中只具有单一属性的事物少之又少, 更多的是同时具有各种各样的不同属性, 比如作为在校大学生的我:“姓名”,“年龄”,“班级”,“学号”,“身高”等等等, 再比如一本书:“书名”,“作者”,“编号”,“价格”等等等 这些都不是用一个简单的数据类型就可以表示的,那么,我们就需要用到这些自定义类型 来定义我们需要的数据类型。下面就让我们从结构体开始认识吧!
一、结构体(struct)
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
(一)结构体的声明
1.结构的声明
struct tag{ member—list; }; member—list是成员列表
例如描述一本书籍:
struct book{
char title[20];
char writer[20];
float price;
};
这里的struct是结构体关键字,book是结构体标签,struct book才是一个完整的结构体名。 在创建结构体变量是必须写完全。 创建结构体有两种方法: 一种是直接在声明结构时直接创建,这种创建出来的是全局变量, 另一种是通过结构体类型创建。
例如:
struct book{
char title[20];
char writer[20];
float price;
}b1,b2;
struct book b3,b4;
2.特殊的声明(不完全声明)
我们在声明一个结构体时,可以不给它“起名字”,这个称为不完全声明
例如:
struct {
char ch;
char str[20];
int num;
}d1,d2;
struct {
char ch;
char str[20];
int num;
}d3,d4;
3.结构体的自引用
在结构中包含一个为结构体本身的类型的变量
例如:
struct str{
int data;
struct str* ps;
};
4.结构体的初始化与赋值
看代码:
struct str{
char ch;
int data;
}s1={'a',10};
struct str s2={'b',20};
struct str s3={.data=30,.ch='c'};
struct str s4;
scanf("%c %d",&s4.ch,&s4.data);
struct *p=s4;
scanf("%c %d",&(*p).ch,&(*p).data);
scanf("%c %d",&p->ch,&p->data);
5.结构体内存对齐
结构体的内存对齐是结构体的一个很重要的知识,这个与结构体在内存中的存储方式有关
下面我们先来计算一下下面这两个结构体的大小:
struct str1
{
char ch;
short sh;
int num;
};
struct str2
{
char ch;
int num;
short sh;
};
这里如果我们不知道结构体的内存对齐规则,那么肯定有很多朋友会认为这两个结构体的大小都是7, sizeof(str1)=1+2+4=7, sizeof(str2)=1+4+2=7; 那么既然我们专门讲了这个例子的话那就说明这是错误的结果了, 那么到底是第一个错了还是第二个错了还是两个大小都不对呢,
下面看实际运行结果:
这里为什么会出现这样的结果呢? 这里我们先来了解一下结构体内存对齐的规则。
内存对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
2.从第二个成员开始,偏移量必须是 对齐数(默认对齐数与它自身大小中的较小者) 的整数倍。 3.结构体总大小为最大对齐数的整数倍。 4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们通过画图进行详细了解:
我们也可以通过offsetof()函数来得到哥哥成员的偏移量来进行验证:
代码如下:
#include<stdio.h>
#include<stdlib.h>
struct str1
{
char ch;
short sh;
int num;
};
struct str2
{
char ch;
int num;
short sh;
};
int main()
{
printf("offsetof(struct str1, ch) = \t%d\n", offsetof(struct str1, ch));
printf("offsetof(struct str1, sh) = \t%d\n", offsetof(struct str1, sh));
printf("offsetof(struct str1, num) = \t%d\n", offsetof(struct str1, num));
printf("offsetof(struct str2, ch) = \t%d\n", offsetof(struct str2, ch));
printf("offsetof(struct str2, num) = \t%d\n", offsetof(struct str2, num));
printf("offsetof(struct str2, sh) = \t%d\n", offsetof(struct str2, sh));
return 0;
}
为什么存在内存对齐? 大部分的参考资料都是如是说的: 1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。 总体来说: 结构体的内存对齐是拿空间来换取时间的做法。 我们上面的两个例子中成员是完全相同的,但是一个大小为8,一个却为12; 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。
6.修改默认对齐数
修改对齐数需要用到预处理指令#pragma
代码实现:
#include<stdio.h>
#pragma pack(1)
struct str1
{
char ch;
short sh;
int num;
};
struct str2
{
char ch;
int num;
short sh;
};
int main()
{
printf("%d\n", sizeof(struct str2));
return 0;
}
还原默认对齐数:
#include<stdio.h>
#pragma pack(1)
#pragma pack()
struct str1
{
char ch;
short sh;
int num;
};
struct str2
{
char ch;
int num;
short sh;
};
int main()
{
printf("%d\n", sizeof(struct str2));
return 0;
}
7.结构体传参
我们在进行函数传参时既可以进行传值传参也可以进行传址传参 结构体也同样可以使用以上两种方法
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; }
如上面这种情况, 结构体非常大,如果我们进行传址传参的话形参是实参的一份临时拷贝, 编译器就会在内存中开辟一块和实参一样大的区域存放形参,这样做会浪费很大的空间, 而使用传址传参就只是传出一个指针,而一个指针大小无非是4/8个字节, 因此,我们在进行结构体传参时更建议使用传址传参。
(二)位段
结构体讲完就得讲讲结构体实现 位段 的能力。 我想,大多数同学都没有听说过位段这个概念吧,所以接下来我们就不卖关子, 直接通过下面的实例来了解它。
1.位段的声明
位段的声明和结构是类似的,有两个不同: 1.位段的成员必须是 int、unsigned int 、signed int 或 char 的整形家族。 2.位段的成员名后边有一个冒号和一个数字。
代码实例:
#include<stdio.h>
struct str
{
int a:4;
int b:10;
int c:20;
int d:8;
};
int main()
{
printf("%d\n",sizeof(struct str));
return 0;
}
运行结果:
2.位段的使用
这里关于位段的知识不进行过多赘述,我们知道有这个知识点就好, 当然他也有自己的使用场景:计算机网络里对数据的分段传输时需要加上描述信息,这时就可以使用位段, 可以对空间进行合理地使用。
二、枚举(enum)
枚举顾名思义就是–一一列举,把可能的情况全部都列举出来 一周有七天,可以一一列举, 一天有二十四个小时,可以一一列举, 英文字母有二十六个,也可以一一列举。
1.枚举类型的定义
enum Day
{
MON,
TUES,
WED,
THUR,
FRI,
SAT,
SUN
};
这里枚举类型默认从0开始,既: MON == 0 , TUES == 1 , WED == 2 …… 在初始化时可以更改他们的值,eg: MON = 3, 那么TUES就会变为4,往后依次增大1
2.枚举的优点
为什么使用枚举? 我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点: 1.增加代码的可读性和可维护性 2.和#define定义的标识符比较枚举有类型检查,更加严谨。 3.防止了命名污染(封装) 4.便于调试 5.使用方便,一次可以定义多个常量
3.枚举的使用
enum Day
{
MON,
TUES,
WED
THUR,
FRI,
SAT,
SUN
};
int main()
{
enum Day d;
scanf("%d",&d);
switch(d)
{
case MON:
printf("星期一\n");
break;
case TUES:
printf("星期二\n");
break;
case WED:
printf("星期三\n");
break;
case THUR:
printf("星期四\n");
break;
case FRI:
printf("星期五\n");
break;
case SAT:
printf("星期六\n");
break;
case SUN:
printf("星期日\n");
break;
}
return 0;
}
三、联合(union)
1.联合类型的定义
联合体我们也顾名思义一下那就是–站在一起,共同使用。 联合体也是一个特殊的自定义类型,可以包含不同的成员,而这些成员共同使用同一块内存空间。(所以也叫公用体)
union un
{
int num;
float fa;
char str[10];
};
2.联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)。
3.联合的使用
经典例题:判断该计算机是大端存储还是小端存储
int main()
{
int a=0x1;
char*p=(char*)&a;
printf("%d\n",*p);
return 0;
}
上面我们是使用了强制类型转换的方法取得了a的地址,但是根据今天我们讲的共用体,我们就可以设计一种更巧妙的方法进行判断
如下:
union un
{
int a;
char ch;
};
int main()
{
union un d = { 0 };
scanf("%d", &d.a);
printf("%d\n", d.ch);
return 0;
}
4.联合大小的计算
联合体也有对齐数, 联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union un1
{
char ch1;
char ch2;
int data;
};
union un2
{
char ch1;
char str[10];
int data;
};
int main()
{
printf("%zu\n",sizeof(union un1));
printf("%zu\n",sizeof(union un2));
return 0;
}
运行结果:
我是在VS下测试的,VS的默认对齐数为8
下面看图解:
总结
以上就是关于结构体、枚举、以及联合的知识总结,这里我再写几点熊猫自己的总结:
- 结构体和联合都需要内存对齐,设计时尽量将小变量放在一起,内存对齐有时会造成内存的浪费,但是却可以提高成员访问速度,
也就是我们常说的用内存换时间。 - 结构体位段的存在就是为了节省空间,所以位段不需要内存对齐,使用位段时要注意成员后面的’ : '以及分配的字节数。
- 枚举类型各个成员之间是通过’ , '连接的,也就是说枚举类型实际上只有一个变量,因此:sizeof(enum day)== 4。
- 在定义自定义类型时要注意大括号后面的’ ; ',这是一条语句结束的标志,如果有的编译器没有自动给出我们也不能忘记。
那么今天的内容就写到这里,感谢大家的支持,欢迎大家来评论区一起探讨,大家的鼓励是继续更新的巨大动力。
|