当前位置:   article > 正文

C语言 自定义类型_c语言自定义数据类型

c语言自定义数据类型

在C语言当中,除了我们常用的几个基本的数据类型之外,还有一种类型叫自定义类型。比如:我们要描述一个学生。这个学生有姓名,性别,年龄,身高等。单独用基本的数据类型是不能完全描述的。这个时候就要使用我们自定义的类型来进行描述。自定义的类型有结构体,枚举和联合体

一、结构体

结构体时一些值得集合,这些值被称为成员变量,每一个成员变量可以有不同的类型。

1,声明

struct tag//这里的tag表示标签,不是变量名

{

        member-list;//成员变量

}variable-list;//变量列表,用来定义变量。注意这里的分号

比如,描述一个人:

  1. struct person
  2. {
  3. char name[20];
  4. char sex[5];
  5. int age;
  6. char nation[20];
  7. };

这里的变量列表可以没有。在变量列表里面定义的变量是全局变量。

2,定义且初始化

要定义,可以直接在变量列表里面,也可以在main函数里面定义。

  1. struct person
  2. {
  3. char name[20];
  4. char sex[5];
  5. int age;
  6. char nation[20];
  7. }person1;//结构体变量person1
  8. struct person person2;//结构体变量person2
  9. int main(void)
  10. {
  11. struct person person3;//结构体变量person3
  12. struct person person4[2];//结构体数组变量person4,有两个元素,都是结构体类型
  13. return 0;
  14. }

要初始化也很简单。

  1. #include<stdio.h>
  2. struct person
  3. {
  4. char name[20];
  5. int age;
  6. }person1 = {"zhangsan", 18};//结构体变量person1
  7. struct person person2 = {"lisi", 18};//结构体变量person2
  8. int main(void)
  9. {
  10. struct person person3 = {"wangwu", 18};//结构体变量person3
  11. struct person person4[2] =
  12. {"abc", 18, {"def", 18}};//加不加{}都可以
  13. return 0;
  14. }

有的时候,结构体还会出现嵌套的情况:

  1. struct person
  2. {
  3. char name[20];
  4. int age;
  5. }person1 = {"zhangsan", 18};//结构体变量person1
  6. struct people
  7. {
  8. struct person person;
  9. char nation[20];
  10. };

 对其进行初始化:

struct people people = { {"zhangsan",18}, "XXX" };//嵌套结构体的初始化

我们在声明结构体的时候,也可以不完全声明,这个时候省略掉标签。

  1. struct
  2. {
  3. int a;
  4. char b;
  5. float c;
  6. }a;

有一种情况是不可以省略标签的,即使用typedef关键字的时候。typedef关键字能够为一种数据类型定义一个新名字。如果省略,就会报错。

  1. typedef struct S
  2. {
  3. int data[1000];
  4. int num;
  5. }s;
  6. s s1 = { {1,2,3,4}, 1000 };

3,结构体成员的访问

结构体成员的访问需要通过点(.)操作符来进行访问的。下面我们来打印这几个初始化的变量。

  1. #include<stdio.h>
  2. struct person
  3. {
  4. char name[20];
  5. int age;
  6. }person1 = {"zhangsan", 18};//结构体变量person1
  7. struct people
  8. {
  9. struct person person;
  10. char nation[20];
  11. };
  12. struct person person2 = {"lisi", 18};//结构体变量person2
  13. int main(void)
  14. {
  15. struct person person3 = { "wangwu", 18 };//结构体变量person3
  16. struct person person4[2] =
  17. {"abc", 18, {"def", 18}};//加不加{}都可以
  18. struct people people = { {"zhangsan",18}, "XXX" };//嵌套结构体的初始化
  19. printf("%s\n", person1.name);
  20. printf("%d\n", person1.age);
  21. printf("--------------\n");
  22. printf("%s\n", person2.name);
  23. printf("%d\n", person2.age);
  24. printf("--------------\n");
  25. printf("%s\n", person3.name);
  26. printf("%d\n", person3.age);
  27. printf("--------------\n");
  28. printf("%s\n", person3.name);
  29. printf("%d\n", person3.age);
  30. printf("--------------\n");
  31. printf("%s\n", person4[1].name);
  32. printf("%d\n", person4[1].age);
  33. printf("--------------\n");
  34. printf("%s\n", people.person.name);
  35. printf("%s\n", people.nation);
  36. return 0;
  37. }

 现在,我们来看看结构体指针访问指向的变量的成员。有以下代码:

  1. struct Stu
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. void print(struct Stu* ps)
  7. {
  8. printf("name = %s age = %d\n", (*ps).name, (*ps).age);
  9. //使用结构体指针访问指向对象的成员,为了简化,使用->操作符来替代(*).操作。
  10. printf("name = %s age = %d\n", ps->name, ps->age);
  11. }
  12. int main()
  13. {
  14. struct Stu s = { "zhangsan", 20 };
  15. print(&s);//结构体地址传参
  16. return 0;
  17. }

  1. #include<stdio.h>
  2. struct person
  3. {
  4. char name[20];
  5. int age;
  6. };
  7. struct people
  8. {
  9. struct person* person;
  10. char nation[20];
  11. };
  12. int main(void)
  13. {
  14. struct person person = { "zhangsan", 18 };
  15. struct people people = { &person ,"XXX"};
  16. printf("%s\n", people.person->name);
  17. printf("%d\n", people.person->age);
  18. printf("%s\n", people.nation);
  19. return 0;
  20. }

 4,结构体传参

  1. struct S
  2. {
  3. int data[1000];
  4. int num;
  5. };
  6. struct S s = { {1,2,3,4}, 1000 };
  7. //结构体传参
  8. void print1(struct S s)
  9. {
  10. printf("%d\n", s.num);
  11. }
  12. //结构体地址传参
  13. void print2(struct S* ps)
  14. {
  15. printf("%d\n", ps->num);
  16. }
  17. int main()
  18. {
  19. print1(s); //传结构体
  20. print2(&s); //传地址
  21. return 0;
  22. }

在传参的时候,参数需压栈。传递一个结构体对象的时候,由于结构体过大,参数在压栈的时候的系统开销比较大,导致性能的下降。另外,在计算结构体的大小的时候,如果是传的结构体的值,会导致结构体的大小无限的增大。所以,结构体在传参要传地址。

5,结构体内存对齐

现在,我们来讨论结构体的内存大小。

5.1,来看下面这段代码,计算它的大小(*):

  1. struct S1
  2. {
  3. char c1;
  4. int i;
  5. char c2;
  6. };
  7. int main(void)
  8. {
  9. printf("%d\n", sizeof(struct S1));
  10. return 0;
  11. }

 我们发现,这个结构体的大小并不是我们认为的6个字节,而是12个字节。

结构体内存的计算如下:

1,第一个成员在与结构体变量偏移量为0的地址处。

2,其他的成员变量要对齐到对齐树的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8,有的默认为4。

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

简单来说,就是:在内存里面,第一个变量放在开始的0地址处,其他的要放在什么地址处,要看对齐数。对齐数是默认对齐数和成员大小的最小值。按对齐数放了之后,这个结构体的大小是最大的对齐数的整数倍。

计算下面嵌套结构体的大小: 

  1. struct S1
  2. {
  3. char c1;//1
  4. int i;//4
  5. };//大小为8
  6. struct S2
  7. {
  8. char c1;//1
  9. struct S1 s1;//8
  10. int d;//4
  11. };
  12. int main(void)
  13. {
  14. printf("%d\n", sizeof(struct S2));
  15. return 0;
  16. }

 在计算的时候,不可以直接把成员变量的大小直接加起来,在取对齐数的最大值倍数处。比如第一个计算(*)处:加起来是6,在取对齐数(4)的整数倍,结果是8,这显然和计算的结果不符合。

在来看最后一道:

  1. struct S
  2. {
  3. short s1;//2
  4. char c1;//1
  5. int s2;//4
  6. };
  7. int main(void)
  8. {
  9. printf("%d\n", sizeof(struct S));
  10. return 0;
  11. }

 如果做的熟练了,也不必要画出图来,直接进行计算就可以了。有的时候会遇到short和char在一起的情况,这个时候,可以直接看成4个大小。

5.2,存在内存对齐的原因:

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。这种做法是很常见的。

我们在设计结构体的时候,既要满足对齐,又要节省空间,那么就让占用空间小的成员尽量集中在一起。如果我们将(*)处的顺序变一下,比如两个char型放在一起,这个结构体的大小就会发生变化。

5.3,修改默认对齐数

  1. #pragma pack(1)//设置默认对齐数为1
  2. struct S2
  3. {
  4. char c3;
  5. int i2;
  6. char c4;
  7. };
  8. int main()
  9. {
  10. printf("%d\n", sizeof(struct S2));//6
  11. return 0;
  12. }

还原为默认:

#pragma pack()//取消设置的默认对齐数,还原为默认 

二、位段

1,定义和声明

结构体可以实现位段。位段的声明和结构体类似。位段是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。采用位段结构既能够节省空间,又方便于操作。 比如位段A:

  1. struct A
  2. {
  3. int _a:2;
  4. int _b:5;
  5. int _c:10;
  6. int _d:30;
  7. };

位段的成员名后边有一个冒号和一个数字。后面数字的单位是比特。位段的成员必须是整型家族。

2,位段的内存分配

位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。前面的位段A种,_a是int类型,是以4个字节开辟的。那么A的大小是多少(32位)?

  1. struct A
  2. {
  3. int _a : 2;
  4. int _b : 5;
  5. int _c : 10;
  6. int _d : 30;
  7. };
  8. int main()
  9. {
  10. //输出的结果是什么?
  11. printf("%d\n", sizeof(struct A));
  12. return 0;
  13. }

 这是如何计算的?

在这里,_a开辟了4个字节的大小,但是_a只需要两个比特就够了。在32位下,还有30个比特。_b需要5个比特,还剩下25比特,_c需要10个比特,这样就剩下15个比特,_d需要30个比特,剩下的15个比特不够,就在开辟4个字节大小的空间来存放_b。这里的图假设是从左开始放。

我们来看这一道题(在VS上是从右开始放):

  1. struct S
  2. {
  3. char a : 2;
  4. char b : 3;
  5. char c : 4;
  6. char d : 5;
  7. };
  8. int main(void)
  9. {
  10. struct S s = { 0 };
  11. s.a = 10;
  12. s.b = 12;
  13. s.c = 3;
  14. s.d = 4;//在内存中是什么样的
  15. return 0;
  16. }

3,位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

三、枚举

枚举,就是一一列举。将可能的值进行一一列举。

1,定义:

  1. enum Day//星期
  2. {
  3. Mon,
  4. Tues,
  5. Wed,
  6. Thur,
  7. Fri,
  8. Sat,
  9. Sun
  10. };
  1. enum Day//星期
  2. {
  3. Mon,
  4. Tues,
  5. Wed,
  6. Thur,
  7. Fri,
  8. Sat,
  9. Sun
  10. };
  11. int main(void)
  12. {
  13. printf("%d\n", Mon);
  14. printf("%d\n", Tues);
  15. printf("%d\n", Wed);
  16. printf("%d\n", Thur);
  17. printf("%d\n", Fri);
  18. printf("%d\n", Sat);
  19. printf("%d\n", Sun);
  20. return 0;
  21. }

 以上定义的 enum Day是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量 。这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

  1. enum Day//星期
  2. {
  3. Mon,
  4. Tues,
  5. Wed,
  6. Thur = 6,
  7. Fri,
  8. Sat,
  9. Sun
  10. };

2,优点

1. 增加代码的可读性和可维护性

2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

3. 防止了命名污染(封装)

4. 便于调试

5. 使用方便,一次可以定义多个常量

3,使用

在switc语句当中,如果我们的分支太多,case后面的标签又是数字,这个时候,要知道这个数字代表什么,就需要去找。但是如果我们使用enum,就可以把数字换成让人一眼便知的常量名,大大提高代码的可读性。

  1. enum Game
  2. {
  3. EXIT,
  4. GAME_BEGIN,
  5. GAME_DISCONTINUE
  6. };
  7. int main(void)
  8. {
  9. switch (1)
  10. {
  11. case GAME_BEGIN:
  12. printf("你开始了游戏!\n");
  13. case GAME_DISCONTINUE:
  14. printf("你中止游戏!\n");
  15. case EXIT:
  16. printf("你退出了游戏!\n");
  17. }
  18. return 0;
  19. }

 四、联合体(共用体)

1,定义:

联合体也是一种特殊的自定义类型,这种类型定义的变量包含一系列的成员。这些成员是公用一块空间,所以也叫共用体。联合体的定义和结构体类似。

  1. //联合类型的声明
  2. union Un
  3. {
  4. char c;
  5. int i;
  6. };
  7. //联合变量的定义
  8. union Un un;

2,特点:

联合体是共用一块空间的,这样,这个联合变量的大小,至少是最大成员的大小,才能存放最大的那个成员。

  1. #include<stdio.h>
  2. union Un
  3. {
  4. int i;
  5. char c;
  6. };
  7. union Un un;
  8. int main(void)
  9. {
  10. // 下面输出的结果是一样的吗?
  11. printf("%p\n", &(un.i));
  12. printf("%p\n", &(un.c));
  13. //下面输出的结果是什么?
  14. un.i = 0x44;
  15. un.c = 0x55;
  16. printf("%x\n", un.i);
  17. return 0;
  18. }

 

从这里可以看出联合体的特点:共用一块空间。正因为如此,所以在打印un.i的时候,会发现结果是0x55。这里的0x44被覆盖了。由联合体的特点,可以用来判断当前计算机的大小端存储。

  1. union Un
  2. {
  3. int i;
  4. char ch;
  5. };
  6. int main(void)
  7. {
  8. union Un un;
  9. un.i = 1;
  10. printf("%d\n", un.ch);//1,是小端
  11. return 0;
  12. }

3,联合体大小的计算

联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

  1. union Un1
  2. {
  3. char c[5];//共5个元素,每个占1个字节,总的大小为5
  4. int i;//4
  5. };
  6. int main(void)
  7. {
  8. printf("%d\n", sizeof(union Un1));//8,是最大对齐数4的倍数。注意不是5,这里的5是数组总的大小
  9. return 0;
  10. }
  1. union Un2
  2. {
  3. short c[7];//2*7
  4. int i;//4
  5. };
  6. int main(void)
  7. {
  8. printf("%d\n", sizeof(union Un2));//16
  9. return 0;
  10. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/518898
推荐阅读
相关标签
  

闽ICP备14008679号