文章目录

  • 一.结构体
    • (一)结构体类型的声明
    • 1.结构体的基础知识
    • 2.结构的声明
    • 3.特殊的声明
    • 4.结构的自引用
    • (二)结构体变量的定义和初始化
    • 1.定义结构体成员变量
    • 2.结构体成员的初始化
    • (三)结构体内存对齐
    • 1.结构体的内存对齐的规则
    • 2.为什么存在内存对齐?
    • 3.修改默认对齐数
    • (四)结构体传参
    • (五)结构体实现位段
    • 1.什么是位段
    • 2.位段的内存分配
    • 3.位段的跨平台问题
    • 4.总结
  • 二.枚举
    • 1.枚举是什么?
    • 2.枚举类型的定义
    • 3.枚举的优点
    • 4.枚举的应用
  • 三、联合(共用体)
    • 1.联合类型的定义
    • 2.联合的特点
    • 3.联合的应用
    • 4.联合体大小的计算
  • 未完待续!!!!!


一.结构体


(一)结构体类型的声明


1.结构体的基础知识


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


2.结构的声明


我们以描述一个学生为例,

一个学生变量,用性别,年龄,姓名描述。

struct Stu{char sex[10];int age;char name[20];};int main(){struct Stu s={"female",20,"zhaoxiangqian"};printf("%s  %d  %s\n",s.sex,s.age,s.name);return 0;}

打印效果

3.特殊的声明

在声明结构的时候,可以不完全的声明。

匿名结构体类型

struct{int a;char b;float c;}x;struct{int a;char b;float c; } *p;

  在上述代码中,并未给结构体加上标签,所以我们在使用时无法直接使用其变量,在;前创建变量,且只能用一次。

那么问题来了?

在上述代码的基础上,下面的代码是否合法?

    p=&x;

  该等式并不成立,在c语言中,虽然他们的成员变量是相同的,但是他们的结构体类型不相同,所以编译过程会报错,不同类型的成员互不兼容。

4.结构的自引用

在这里,我们引入数据结构的部分内容来理解。

//代码一struct Node{int date;struct Node next;};

这个代码能否帮助我们从一个数字串联到下一个数字?

从表面上看,确实从一个结构体中能找到下一个结构体中的数据,但是

如果可以,那sizeof(struct Node)是多少?

将会是一个无穷大的量,无法计算,所以不可行。

正确的自引用方式:

//代码2struct Node{ int data; struct Node* next;};

这里我们如何理解呢?

这样就可以串联链表中的每一个数字,结构体引用结构体,(类似于递归),这就是结构体的自引用。

(二)结构体变量的定义和初始化

有了结构体类型,那么如何定义结构体成员变量呢?

1.定义结构体成员变量

struct Point{int x;int y;}p1;         //声明类型的同时定义变量p1struct Point p2; //定义结构体变量p2

2.结构体成员的初始化

初始化:定义变量的同时赋初值

struct Point p3 = { 4, 5 };

struct Stu          //结构体类型说明{char name[20];  //名字int age;        //年龄};struct Stu s = { "zhangsan", 20 };  //初始化

结构体嵌套初始化

struct Node{int date;struct Point p;struct Node *next;}n1 = { 10, {4,5},NULL};//结构体嵌套初始化

(三)结构体内存对齐

在掌握结构体的基础知识后,我们想要计算一下结构体的大小,那么是如何计算的呢?

结构体在计算大小时会出现一个问题,那就是结构体的内存对齐

1.结构体的内存对齐的规则

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

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

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

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

对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

VS中默认的值为8

Linux中的默认值为4

对规则的解读:

什么叫做偏移量?


让我们通过练习来熟悉内存对齐的使用

练习一

//练习一struct s1{char c1;  // 1int i;    // 4char c2;  // 1};int main(){printf("%d\n", sizeof(struct s1));return 0;}

请问该结构体的大小是多少?

可能是6,也可能是其他,让我们运行一下程序。


结果为 12,那么如何得出结构体的大小为 12 呢?

char c1 占1个字节 ,从结构体的起始位置开始存储。

int i 占4个字节,vs环境下他的对齐数为4,所以他在地址中要从4的倍数开始储存。

char c2 占1个字节, 对齐数为1,所以它在int 后又占1个字节。

此时计算大小为9,总大小应该符合总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,最大对齐数为4,所以总大小应为12.

内存示意图:
练习二

struct s2{char c1;char c2;int i;};int main(){printf("%d\n", sizeof(struct s2));return 0;}

char c1占一个字节,从结构体的起始位置开始存储。

char c2占一个字节,对齐数为1.

int i 占4个字节,vs环境下他的对齐数为4,所以他在地址中要从4的倍数开始储存。

此时结构体的总大小是8个字节,为最大对齐数4的倍数。

示意图:

显示结果

练习三

struct s3{double d;  // 8char c;    // 1int i;     // 4};int main(){printf("%d\n", sizeof(struct s3));return 0;}

练习四

结构体的嵌套问题

struct s3{double d; // 8char c;  // 1int i;   // 4};struct s4{char c1;     // 1struct s3 s; // 16double d;    // 8};int main(){printf("%d\n", sizeof(struct s4));return 0;}


2.为什么存在内存对齐?

大部分的参考资料都是这样说的:

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
    处取某些特定类型的数据,否则抛出硬件异常。
  1. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
    需要作两次内存访问;而对齐的内存访问


  假如在 32 位 的机器上, 一次读取数据只能读取 4个字节,那么在不考虑并对齐的情况下, int类型的数据要读取两次才能完整计算, 而在考虑对齐的情况下,int 只需读取一次。

总的来说

结构体的内存对齐是拿空间来换取时间的做法

那在设计结构体的时候,我们既要满足对齐,又要节省空间,怎样才能做到?

让占用空间小的成员尽量集中在一起

//例如:struct S1{ char c1; int i; char c2;};struct  S2{  char c1;  char c2;  int  i;};

s1 和 s2类型的成员一模一样,但是 s1 和 s2 所占空间的大小有了一些区别。

3.修改默认对齐数

#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数

#include <stdio.h>#pragma pack(8)//设置默认对齐数为8struct S1{ char c1; int i; char c2;};#pragma pack()  //取消设置的默认对齐数,还原为默认int main(){printf("%d\n",sizeof(struct S1));return 0;}

这里我们将默认对齐数修改为8,所以打印的结果为 12

#pragma pack(1)//设置默认对齐数为8struct S2{ char c1; int i; char c2;};#pragma pack()//取消设置的默认对齐数,还原为默认int main(){printf("%d\n",sizeof(struct S2));return 0;

这里设置默认对齐数为1,就相当于连续存放,结构体的大小为 1+4+1=6

结论:

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

(四)结构体传参

  当我们有一个结构体类型,又创建了一个结构体变量,当我们使用这个变量时,没有直接使用,而是传给其他函数使用,我们可以选择传送这个结构体变量的值,也可以选择传送这个结构体变量的地址

struct S{int date[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 * s){printf("%d\n", s->num);}int main(){print1(s);print2(&s);return 0;}

我们向print函数分别传送了struct 结构体本身,以及结构体的地址,那如果比较的话,print1 和 print2 哪个更好?

答案是:首选print2函数。

原因

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

 举一个例子,传结构体本身的话,这个结构体内部有int i[10000]的数据,那我们就要传送10000个整形数据,但是如果我们传送的是结构体的地址的话,首先指针总共就只占用4个字节,我们可以通过这4个字节来找到并操作所指向的10000个整形数据。大大节省了空间和时间。

值传递:函数的形参值传递,是拷贝实参的内容到形参中并且开辟出同等大小的空间来存储的过程,在这个过程中回发生压栈,如果结构体成员过多,压栈的空间就越多,导致空间上的浪费。

址传递:直接把实参的地址传递过去,而使用的空间仅仅是4/8个字节,而且地址传递还能对实参进行修改,所以一般结构体传参都是传址。

结论

结构体传参的时候,要传结构体的地址。

(五)结构体实现位段


1.什么是位段

位段的声明和结构体是类似的,有两个不同:

1.位段的成员必须是 int ,unsigned int ,或 signed int

2.位段的成员名后面有一个冒号和一个数字。

struct A{int a : 2;int b : 5;int c : 10;};

A就是一个位段类型

那位段A的大小是多少?

printf("%d\n",sizeof(struct A));

我们可能会这样计算:2+5+10=17,但是位段的内存分配并不是简单的相加减等等。

  这里位段的位是 二进制位,为什么要引入位段的概念呢?我们要知道,如果在内存中要存储 0 、1、2、3这样的数据,所占的空间大小最多才为2个bit位,但是我们创建了一个int类型占4个字节,32个bit位来进行储存这个数据,导致了内存空间的浪费,所以位段可以帮我们在一定程度上节省空间。

2.位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

好,我们细细分析,首先位段成员必须是 整形家族类型的数据,这是位段的规定。
在计算空间大小时,我们应该这样计算:

以上面 struct A 为例,来分析该位段的大小如何计算。

int a:2 // a占2个bit位
int b : 5 // b占5个bit位
int c : 10 // c占10个bit位
int d:30 // d占30个bit位

位段一次开辟4个字节的空间,将a,b,c,d依次放入

  但是我们发现将a,b,c已经占了17个bit位了,d 占30个bit位无法存入这个空间中,我们就需要再开辟一个4个字节的空间大小。所以总共占据8个字节进行存储。


但是我们搞清楚了空间的大小,却不清楚数据如何进行存放。
d存放的可能方式

1.将第一个开辟的空间剩余的13个bit位占满后,在第二个空间中存放17个bit位。

2.因为无法整体存入第一空间中,所以从第二个空间进行存放,第一个空间剩余的内存被浪费掉。

如何进行存放,我们还不是特别清楚。

  所以这里的第三条内存分配的规则就讲述了位段跨平台的特点,其实在不同的编译器上存放的规则也不相同,但是上述2种存放的方法都对,不过在不同的平台存放的形式有所差异。

3.位段的跨平台问题

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

2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)

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

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

4.总结

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

二.枚举


1.枚举是什么?

枚举顾名思义就是一一列举。

把可能的取值一一列举。

比如我们现实生活中:


这里就可以使用枚举了。


2.枚举类型的定义

enum Day   //星期{Mob,Tues,Wed,Thur,Fri,Sat,Sun};

enum Sex   //性别{female,male,secret};

enum Color  // 颜色{RED,GREEN,BLUE};

  以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫 枚举常量 。

这些可能取值都是有值的,默认从0开始,一次递增1。如下图所示:

enum Color  // 颜色{RED,GREEN,BLUE};int main(){enum Color c;printf("%d %d %d\n", RED, GREEN, BLUE);return 0;}


当然定义的时候也可以赋初值,例如:

enum Day   //星期{Mon=20,Tues,Wed=1,Thur,Fri,Sat=6,Sun};int main(){enum Day d;printf("%d %d %d %d %d %d %d\n", Mon, Tues, Wed,Thur,Fri,Sat,Sun);return 0;}



3.枚举的优点

为什么使用枚举?

我们可以使用 #define 定义常量,为什么非要使用枚举?

枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量。


4.枚举的应用

#include <stdio.h>enum S{EXIT,ADD,SUB,MUL,DIV};void  menu(){printf("***************************\n");printf("*****  1.add   2.sub  *****\n");printf("*****  3.mul   4.div  *****\n");printf("*****  0.exit         *****\n");printf("***************************\n");}int Add(int x,int y){return x + y;}int Sub(int x, int y){return x - y;}int Mul(int x, int y){return x*y;}int Div(int x, int y){return x*y;}int main(){int input = 0;int x = 0;int y = 0;do{menu();printf("请选择:>");scanf_s("%d", &input);switch (input){case ADD:printf("请输入两个操作数:>");scanf_s("%d %d", &x, &y);printf("%d\n", Add(x, y));break;case SUB:printf("请输入两个操作数:>");scanf_s("%d %d", &x, &y);printf("%d\n", Sub(x, y));break;case MUL:printf("请输入两个操作数:>");scanf_s("%d %d", &x, &y);printf("%d\n", Mul(x, y));break;case DIV:printf("请输入两个操作数:>");scanf_s("%d %d", &x, &y);printf("%d\n", Div(x, y));break;case EXIT:printf("退出程序\n");default:printf("选择错误\n");break;}} while (input);return 0;}


三、联合(共用体)


1.联合类型的定义

  联合也是一种特殊的自定义类型。这种类型定义的变量也包含了一系列的成员,特征是这些成员共用一块空间(所以联合也叫共用体)。

比如

//联合类型的声明union Un{char c;int i;};//联合变量的定义union Un un;//计算连个变量的大小printf("%d\n", sizeof(un));


2.联合的特点

  联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

那么联合体成员变量是如何存放入内存的?

我们看一下代码:

union Un{int i;char c;};int main(){union Un un;printf("%p\n", un.i);printf("%p\n", un.c);return 0;}

i 和 c 如何进行存储?

来看打印结果
我们会发现i 和 c 占用了同一块空间。


  i 和 c 会占用同一块空间,这就是联合体(共用体)的特点。联合体的成员会共用一块空间。

对联合体的特点我们清楚以后,那我们什么时候使用联合体呢?

  以上面的 char c 、 int i 举例,在我们只使用char c 或 int i 时我们可以使用联合体。如果有多个成员,使用c的时候,i的值也会发生相应变化,使用i的时候,c的值也会发生相应的变化,所以这两个变量不能同时使用。

举一个例子:

当联合体中存储两个不同的值时,内存中的结果是显示的。

union Un{int i;char c;};int main(){union Un un ;//printf("%p\n",&( un.i));//printf("%p\n",&( un.c));un.i = 0x11223344;un.c = 0x55;printf("%x\n", un.i);return 0;}

打印的结果是什么呢?


那么 为什么呢?


u=0x55 改变了原来 i 的首字节44的存储,所以共用一块空间的结构体成员在使用时会互相影响。


3.联合的应用

面试题

判断当前大小计算机的大小端存储

  在之前的博客中,博主已经详细的介绍了大小端的概念以及如何查看计算机内存是大端存储还是小端存储。不懂的同学请关注博主之前的文章——C语言进阶(一)数据的存储中的百度系统工程师面试题。

这是普通方法的代码实现:

int check_sys(){int a = 1;if (*(char*)&a = 1)return 1;elsereturn 0;}int main(){int ret = check_sys();if (ret == 1){printf("小端\n");}elseprintf("大端\n");}

  下面我们用联合体的形式来判断当前机器的大小端存储。

#include <stdio.h>int check_sys(){union MyUnion{char c;int i;}u;u.i = 1;return u.c;}int main(){int ret = check_sys();if (ret == 1){printf("小端\n");}elseprintf("大端\n");}


4.联合体大小的计算

1.联合的大小至少是最大成员的大小。

2.当最大成员不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

比如:

union Un1 {  char c[5];  int i; }; union Un2 {  short c[7];  int i; }; //下面输出的结果是什么?int main(){printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2));return 0;}


  第一个打印结果, char 类型的数组占5个字节,该联合体最大对齐数是4,所以要浪费3个字节的空间,占8个字节。

  第二个打印结果, short类型的数组占14个字节,在联合体最大对齐数是4,所以要浪费2个字节的空间,占16个字节。


好的,关于自定义类型的详解就说到这里,谢谢大家的欣赏!!




未完待续!!!!!




C语言进阶(七)——动态内存管理已更新

©著作权归作者所有:来自51CTO博客作者Rich_?ིྀ的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 不一样的享元模式(设计模式四)
  2. 【算法与数据结构专场】BitMap算法基本操作代码实现
  3. python实战课程--控制结构
  4. 01.C语言学习(控制结构)
  5. 【体系结构】有关Oracle SCN知识点的整理--补充内容
  6. 优盘提示位置不可用无法访问,磁盘结构损坏且无法读取咋办???
  7. 第125天:Flask 项目结构
  8. flex 容器的 flex-flow, justify-content, align-items, align-c
  9. grid对齐方式与基础实战

随机推荐

  1. Linux下的find 命令使用实例
  2. UBUNTU 12.04下傻瓜式简单安装arm-linux-
  3. 在Ubuntu 9.10中有奇怪的pynotify行为
  4. Linux系统安装并使用Tomcat
  5. 通过指令“ps -l”查看进程信息
  6. 用虚拟机装的linux与宿主机的IP能互ping
  7. 计算所有字符,包括linux中的空格
  8. Linux服务列表(CentOS)
  9. 【Linux】CentOS7上解压zip需要安装uzip
  10. libpcap丢包原理分析及Fedora 9 内核2.6.