目录
结构体
结构体的声明
结构体自引用
结构体变量的定义、初始化以及访问
结构体内存对齐
结构体传参
位段
枚举
枚举是什么?
枚举的声明
枚举的优点
枚举类型的大小
联合体
联合体类型的定义
联合的初始化
判断当前计算机的大小端存储
联合大小的计算
练习—通讯录
静态存储
动态开辟内存版本
练习
结构体
结构体的声明
struct 结构体名字{结构体成员};//注意大括号后面的分号;不能少,这是一条完整的语句。//例如描述一个学生:struct Stu{char name[20];//名字int age; //年龄char sex[5];//性别char id[20];//学号};//分号不能丢
特殊声明:匿名结构体
//匿名结构体:结构体没有名字。//匿名结构体只有在声明的时候才能创建变量。并且之后无法再次创建,因为其没有名字,没有办法创建。struct{char c;int i;char ch;}s;struct{char c;int i;char ch;}*ps;int main(){//即使两个结构体内成员相同,也是不同的结构体。并且编译器也会将这两个结构体当作不同的类型。ps = &s;//编译报错。return 0;}
结构体自引用
在结构体中包含一个类型为该结构体本身的成员是否可以呢?
//这种方式不可行,如果创建一个该结构体类型变量。其中会一直包含一个Node类型的变量。//则这个结构体变量的大小也是无限的。完全不可行struct Node{int data;struct Node next;};
正确的自引用方式
//其中可以包含一个本结构体类型的指针。//这就是数据结构链表的连接方式,链表的每一个元素都分为两块:数据域与指针域,指针域指针指向同类型的下一个元素。struct Node{int data;struct Node* next;};
注意
//将匿名结构体类型重命名为Node,然后在其中使用Node*类型指针可以吗?//不行,要重命名Node类型,必须先有这个匿名结构体类型,才能定义其类型指针typedef struct{int data;Node* next;}Node;//解决方法:必须现有该类型,然后才能重命名类型。//typedef struct Node{}sNode,将struct Node类型重命名为sNode类型typedef struct Node{int data;struct Node* next;}sNode;
结构体变量的定义、初始化以及访问
参考结构体初阶。
结构体内存对齐
内存对齐规则
/** 1、结构体的第一个成员,被放结构体变量在内存中存储位置的偏移量为0的地址处。* 2、从第二个成员往后的所有成员,都要放在偏移量为其对齐数的整数倍的地址处。* - 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。对齐数用于确定成员从哪个地址开始存放,存放的空间取决于该成员的大小。* - vs中默认对齐数为8。* - Linux系统下没有默认对齐数的概念,成员与自己的大小对齐。* 3、结构体的总大小是结构体所有成员的对齐数中最大的那个对齐数的整数倍。* 4、如果该结构体内嵌套了其他的结构体,则嵌套的结构体的对齐数=(该结构体中的所有成员的最大对齐数),嵌套的结构体的大小就是其大小。* 该结构体的大小,是所有对齐数(包括其嵌套的结构体内的对齐数)中最大对齐数的整数倍。*/
练习
#include <stdio.h>struct s1{char c1; //1 0偏移处int i;//4 4~7偏移处char c2; //1 8偏移处//8+1=9,最大对齐数为:4,比9大的最小的 4的倍数为,所以该结构体大小为:12。};struct s2{char c1; //10偏移处char c2; //11偏移处int i;//44~7偏移处//7+1=8,是最大对齐数4的倍数,所以该结构体大小为:8};struct s3{double d; //80~7偏移处char c;//18偏移处int i;//412~15偏移处//15+1=16,是最大对齐数8的倍数,所以该结构体大小为:16};//结构体嵌套struct s4{char c1; //10偏移处struct s3 s3; //大小16,对齐数:8 8~23偏移处double d; //824~31偏移处//31+1=32,最大对齐数8,所以该结构体大小为:32};int main(){printf("%d\n",sizeof(struct s1));//12printf("%d\n",sizeof(struct s2));//8printf("%d\n",sizeof(struct s3));//16printf("%d\n",sizeof(struct s4));//32return 0;}
为什么存在内存对齐?
/** 1、平台原因(移植原因)* 不是所有的硬件平台都能访问任意地址上的任意数据。某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出异常。* 2、性能原因:* 数据结构(尤其是栈),应该尽可能的在自然边界上对齐。* 问题在于:为了访问未对齐的内存,处理器需要做两次内存访问。而对齐的内存仅需要一次访问就可以获取数据。* 如:struct S{char c; int i;};* - 如果是内存对齐:c在0偏移处,i在4~7偏移处,总共占用8个字节。*假设CPU读取时一次读取四个字节,第一次读前四个字节得到c,第二次读接着的4个字节,得到了i* - 如果不是内存对齐:c在0偏移处,i在2~4偏移处,总共占5个字节。*假设cpu读取时一次读取四个字节,第一次读取前四个字节得到c;第二次读接着的四个字节,其中第一个字节是i的最后一个字节。*为了得到i,算是读取了两次,第一次读取到i的三个字节,第二次读取到i的一个字节** 总结来说:结构体的内存对齐,其实是拿空间来换取了时间。*/
在设计结构体的时候,我们既要满足对齐,又要节省空间。应该:让占用空间小的成员尽量集中在一起。
#include <stdio.h>struct s1{char c1;int i;char c2;};struct s2{char c1;char c2;int i;};int main(){printf("%d\n",sizeof(struct s1));//12printf("%d\n",sizeof(struct s2));//8return 0;}
修改默认对齐数
/** - #praga是个预处理指令,我们可以使用这个指令来修改默认对齐数。* - 在结构体的对齐方式不合适的时候,我们可以自己更改默认对齐数。* - 一般不修改对齐数,修改的话,一般不设置为奇数,而是为2的倍数或次方。*/#include <stdio.h>#pragma pack(8) //设置默认对齐数为8struct S1{char c1;int i;char c2;};#pragma pack() //取消设置的默认对齐数,还原为默认#pragma pack(2) //设置默认对齐数为2struct S2{char c1;int i;char c2;};int main(){printf("%d\n",sizeof(struct S1));//12printf("%d\n",sizeof(struct S2));//8return 0;}
写一个宏,计算结构体中某变量相对于首地址的偏移并给出说明。
考察:offseof宏的实现,我们还没有学习宏,我们现在先使用。等学完宏之后再实现
#include <stdio.h>#include <stddef.h>struct S1{char c1;int i;char c2;};int main(){printf("%d\n",offsetof(struct S1,c1));//0printf("%d\n",offsetof(struct S1,i));//4printf("%d\n",offsetof(struct S1,c2));//8return 0;}
结构体传参
/** - 函数在传参的时候,参数需要压栈,会有时间和空间上的系统开销。* 如果传传递的结构体很大时,参数压栈占用的资源较大,性能就会降低。* - 所以结构体传参的时候,要传结构体地址。选print2()函数比print1()更好*/#include <stdio.h>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;}
位段
什么是位段?
/** 位段的声明与结构体类似,有两个不同:* - 位段的成员必须是int、unsigned int 或signed int 。或者是char类型* - 位段的成员名后有一个冒号和一个数字。*/struct A{int _a:2;int _b:5;int _c:10;int _d:30;};
位段的内存分配
/** 位段的内存分配* - 位段的成员可以是int、unsigned int、signed int 或是char类型(char属于整型家族)* - 位段的空间上是按照需要以4个字节或以1个字节的方式开辟的。* 如果一个位段中都是int类型,则以4个字节开辟。* 如果一个位段中都是char类型,则以1个字节开辟。* - 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。*/#include <stdio.h>struct A{//位段首先开辟四个字节空间,也就是32bit位int _a:2;//_a占2个比特位,还剩30bit位int _b:5;//_b占5个比特位,还剩25比特位int _c:10;//_c占10个比特位,还剩15bit位//此时指剩下15个比特位,而_d需要30个比特位,所以再次开辟4个字节。int _d:30;//_d占30个比特位//这里的_d使用30个比特位,是先使用前面剩下的15个比特位,再使用新开辟的4个字节中的15个比特位呢;还是直接使用新开辟的四个字节中的30个比特位?//注意:这里并不能确定是怎么使用的,C语言并没有明确规定。所以这是一个不确定因素,不同环境下的实现可能不同,所以位段是不跨平台的。//如果需要跨平台使用,则最好不要使用位段。或者研究不同平台下位段使用的不同,最后针对不同的平台写出不同的代码。//一共开辟了两个4字节空间,所以该结构体大小应该是8。//一个整型占用四个字节,所以其最大占用的比特位是32位,超过32位报错。//int _e:40;/** _a只有两个比特位,两个比特位能做什么呢?* - 在描述性别的时候,有三个可能:* 00 男* 01 女* 10 保密* 11 (空闲不使用)* - 这样用两个比特位就表示了原来需要四个字节才能表示的数据,节省了空间。*/};int main(){printf("%d\n",sizeof(struct A));//A结构体大小是8return 0;}
内存分配详解
#include <stdio.h>struct S{//分配一个字节,也就是8个比特位char a:3;//a占用3个比特位,还剩5个比特位。char b:4;//b占用4个比特位,还剩1个比特位。char c:5;//c占用5个比特位,再开辟一个字节空间,那是先使用前面剩下的一个比特位,再使用新开辟的空间呢?// 还是丢弃这个比特位,使用新的这个字节的比特位?假设这5个比特位都是使用新开辟的这个字节空间的比特位。还剩下3个比特位。char d:4;//d占用4个比特位,再开辟一个字节空间。};int main(){/** 将s初始化为0。* - 一个字节有8个比特位,是先使用低位呢?还是先使用高位?我们假设先使用低位* s.a = 10; 10转换为二进制是1010,因为a只能存储3位,所以发生截断,取其后三位存储进s,第一个字节就成为:00000 010* s.b = 12; 12转换为二进制是1100,刚好b能存储4位,第一个字节成为:0 1100 010* - 此时剩下1个比特位了,而c占用5个比特位,再开辟一个字节空间。我们假设前一个比特位丢失,则c存储到第二个字节的后5个比特位中。* s.c = 3; 3转换为二进制是11,第二个字节就成为:000 00011* - 此时剩下3个比特位了,而d占用4个比特位,再次开辟一个字节空间。同样假设前三个比特位丢失,d存储到第三个字节的后4个比特位中。* s.d = 4; 4转换位二进制是100,第三个字节就成为:0000 0100* - 因为是小端村存储方式,也就是低位低地址,所以s的三个字节就是:0110 0010 0000 0011 0000 0100* 内存中存储为十六进制,每四位二进制就可以转化为一位十六进制: 6 20 30 4* 所以s在内存中就存储为:62 03 04 。我们加断点,Debug查看s在内存中存储。发现确实是这样存储的。** (CLion以及VS中是如此)* 结论:* - char使用每个字节空间的时候,是从低位向高位使用。* 如果是int的四个字节,也是从低位到高位使用。小端存储(低位低地址)模式下,则先使用四个字节中最左侧的那个字节* - 如果一块空间里(char类型位一个字节/int类型为四个字节)剩余的比特位不够下一个成员使用的时候,则剩余的比特位会被丢弃掉,不被使用* 会开辟一块新的空间,使用新的空间中的比特位。*/struct S s ={0};s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%d\n",sizeof(struct S));//3return 0;}
位段的跨平台问题
/** 1、int位段被当成有符号数还是无符号数是不确定的。* C语言标准没有规定位段中的int被当成有符号或是无符号处理。可能不同的平台实现就不同。* 2、位段中最大位的数目不能确定。(16位机器最大16、32位机器最大32。写成27,在16位机器上会出问题)* 16位机器下,int是两个字节,也就是16bit* 32位机器下,int是四个Byte,也就是32个比特位。* 3、位段中的成员是在内存中从左向右分配,还是从右向左分配尚未定义。* 4、当一个结构体包含两个位段,第一个位段剩余的比特位不够下一个位段成员使用时,开辟空间后,是舍弃剩余的比特位还是利用,也无法确定。** 总结:跟结构体向比,位段可以达到同样的效果,虽然可以很好的节省空间,但是存在跨平台的问题。*/
位段的应用——网络层面传输数据时的占用空间划分
枚举
枚举是什么?
/** - 枚举是什么?* 顾名思义,枚举的意思就是一一列举。* - 我们生活中,总是有一些可以用有限值描述的东西。* 如:一周有7天,周一到周日 ; 性别:男、女、保密 ; 一年有12个月等等,可以一一列举的,就可以用枚举来表示。*/
枚举的声明
//枚举类型的声明:/*枚举关键字enum 枚举类型名{//枚举类型的可能取值,枚举值都是常量。枚举值,枚举值,...枚举值};*/#include <stdio.h>//RED、GREEN、BLUE的值是0,1,2。他们是整型吗?//他们是枚举类型,枚举值与整型仅仅是值相同,但类型却不同。枚举成员被叫做枚举常量。可以说是枚举常量值是int类型值。enum Color{//这三个值默认是0,1,2。默认值从0开始,每个递增1。RED, //0GREEN, //1BLUE //2//如果是RED=3, 则三个值是3,4,5。被叫做赋初值。// RED=3,// GREEN, //4 递增1// BLUE //5 递增1//默认情况下,枚举常量值是递增的。如果为每个枚举常量都设置了值,那么其值不再递增,就是我们所设置的值。// RED=5, //5// GREEN=8, //8// BLUE=10 //10//默认从0开始,如果为中间一个设置了值,那么下一个枚举常量的值是该量的值+1。// RED, //0// GREEN=8, //8// BLUE //9//枚举常量不是常量么,怎么还可以修改呢?//注意:这里不是修改,这里是定义常量时,赋的初始值};int main(){//因为C语言没有这种检查,所以这种写法也可以,但是不推荐使用。如果是在c++中则会报错。//enum Color c = 2;enum Color c = BLUE;//常量一点定义好,其值不可以再次修改。只能是在定义的时候为其赋值。//BLUE = 8;printf("%d\n",RED);printf("%d\n",GREEN);printf("%d\n",BLUE);return 0;}
枚举的优点
//为什么使用枚举?我们也可以使用#define定义常量,为什么要使用枚举呢?//#define RED 5;//#define GREEN 8;//#define RED 10;//enum Color//{// RED=5,// GREEN=8,// BLUE=10//};/** 枚举的优点* 1. 增加代码的可读性和维护性。* 2. 和#defint定义的标识符比较,有类型检查,更加严谨。* 3. 防止了命名污染。(封装)* #define定义的常量是全局的,都可以访问。而枚举定义的值是枚举类型自己的可能取值。* 什么是明明污染?定义一个变量,哪里都可以访问,到处都可以使用。* 4. 便于调试* - test.c如何成为test.exe可执行程序呢?*先通过编译,再链接,最后才生成可执行程序。编译有三个过程:预编译 ——> 编译 ——> 汇编* - 什么时候调试呢?生成可执行程序exe了才可以调试* - 如定义一个RED常量:#define RED 5; 我们的程序语句int a = RED;*在预编译阶段RED就被替换为5,成为int a = 5; 在我们调试时,在内存中,看不到RED这样的符号,而是看到的5* - 而枚举不是替换的,在运行(调试)的时候,其实是二进制,这个二进制对应的就是枚举写的代码。* - 枚举常量定义方便,使用时也方便。*/#include <stdio.h>void menu(){printf("---------------------------------");printf("---------1. Add 2. Sub---------");printf("---------3. Mul 4. Div---------");printf("------------0. exit--------------");}enum Option{EXIT, //0ADD, //1SUB, //2MUL, //3DIV//4};int main(){int input = 0;do{menu();printf("请选择:");scanf("%d",&input);//原来是数字,可读性差// switch (input) {// case 1://break;// case 2://break;// case 3://break;// case 4://break;// case 0://break;// default://break;// }//使用枚举,可读性更高。switch (input) {case ADD://Add(); 调用加法方法break;case SUB://Sub(); 调用减法方法break;case MUL://Mul(); 调用乘法方法break;case DIV://Div(); 调用除法....break;case EXIT:break;default:break;}} while (input);}
枚举类型的大小
#include <stdio.h>enum Color{RED, //0GREEN, //1BLUE //2};int main(){//枚举类型值是int类型,所以其大小应该就是4个字节。printf("%d\n",sizeof(enum Color));//4printf("%d\n",sizeof(RED));//4return 0;}
联合体
联合体类型的定义
/** 联合类型的定义:* - 联合类型也是一种特殊的自定义类型。* - 其类型中也包含一系列成员,特征是这些成员共用同一块内存空间(所以联合也叫共同体)** 总结 - 联合体的特点:* - 联合的成员是共用一块空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少要保存占用空间最大的成员)*/#include <stdio.h>union Un{char c;//1int i;//4};int main(){union Un u;//Un联合体中,有一个char类型变量,还有一个int类型变量。我们猜测最小也要占用5个字节。//可是实际上Un却只占4个字节大小,这是为什么呢?//char变量c与int变量i共用了联合体中第一个字节的空间。所以是4个字节大小。printf("%d\n",sizeof(u));//4//我们查看u、u.c、u.i的地址,发现他们的地址相同。说明c与i都是从联合体所占空间的起始地址值处开始存储的,他们共用了第一个字节的空间。printf("%p\n",&u);//0000005cf65ff81cprintf("%p\n",&(u.c));//0000005cf65ff81cprintf("%p\n",&(u.i));//0000005cf65ff81creturn 0;}
联合的初始化
#include <stdio.h>union Un{char c;int i;};int main(){//因为联合是成员共用一块空间,所以我们初始化的时候只能初始化一个值。union Un u = {10};//这个值是放到了联合所占的4字节空间内了。00000000 00000000 00000000 00001010//c和i共用第一个字节(也就是低位的那个字节),所以他们都是10。printf("%d\n",u.c);//10printf("%d\n",u.i);//10//因为联合成员共用同一块空间,所以在改动c的同时,i也会被修改。再改动i的同时,c也会被修改。//所以联合体的另一个特点:在同一时间,只能使用联合成员中的一个成员进行操作。u.c = 100;printf("%d\n",u.c);//100printf("%d\n",u.i);//100u.i = 1000;printf("%d\n",u.c);//-24printf("%d\n",u.i);//1000//那联合体有什么用呢?//如果多个成员,你想要他们共同使用一块空间,并且空间的共用不会影响整体的使用,这个时候就可以选择联合体。return 0;}
判断当前计算机的大小端存储
之前使用的方式
/** 如:1(00 00 00 01)在内存中的存储就是:* 低地址——————————————————————————————————>高地址* 小端:高字节高地址01 00 00 00* 大端:高字节低地址00 00 00 01*/#include <stdio.h>int check_sys(){int a = 1;//使用char类型指针变量,这样就可以只取a变量第一个字节。char * p = (char*)&a;//取出来然后返回,如果是1,则*p=1,就返回1。如果取出来0,则*p=0,返回0.return *p; //返回1表示小端;返回0表示大端。}int main(){int ret = check_sys();if(ret == 1){printf("小端");}else{printf("大端");}return 0;}
使用联合
/** 如:1(00 00 00 01)在内存中的存储就是:* 低地址——————————————————————————————————>高地址* 小端:高字节高地址01 00 00 00* 大端:高字节低地址00 00 00 01*/#include <stdio.h>int check_sys(){//联合Un中char类型变量c与int变量i共用第一个字节的内容。union Un{char c;//1int i;//4}u;//将i初始化为1u.i = 1;//因为是联合体,所以c与i共用第一个字节。也就是说,c如果存储的1,就说明采用小端存储;如果c存储0,说明是大端存储。return u.c;}int main(){int ret = check_sys();if(ret == 1){printf("小端");}else{printf("大端");}return 0;}
联合大小的计算
#include <stdio.h>/** - 联合的大小至少是最大成员的大小* - 当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。*/union Un1{char c[5];//大小:5 因为是char类型,所以对齐数是:1int i;//大小:4 对齐数:4。//联合大小最小为:5,比5大的最大对齐数4的倍数是8,所以大小:8};union Un2{short c[7];//大小:14 因为是short类型,所以对齐数是:2int i;//大小:4 对齐数:4。//联合体最小为:14,比14大的最大对齐数4的倍数:16,素以大小:16};int main(){printf("%d\n",sizeof(union Un1));//8printf("%d\n",sizeof(union Un2));16return 0;}
练习—通讯录
静态存储
/** 遗留问题:* - 删除/修改都是以姓名判断的,而如果出现同名的情况,则碰到第一个就会返回。* - 排序没有做*/
contact.h
//类型定义、函数声明。#ifndef FIRST_CONTACT_H#define FIRST_CONTACT_H//头文件引入#include <stdio.h>//常量的定义#define MAX_NAME 20#define MAX_SEX 10#define MAX_TELE 12#define MAX_ADDR 30#define NUM_PEOPLE 1000//类型定义typedef struct PeoInfo{//将其定义为常量,修改时可以在定义的位置修改。char name[MAX_NAME];char sex[MAX_SEX];int age;char tele[MAX_TELE];char addr[MAX_ADDR];}PeoInfo; //重命名该结构体类型为PeoInFo//因为我们为通讯录添加人的时候,需要知道往哪里添加,所以这里我们将其再次封装,增加一个int类型变量用于记录通讯录中有效信息的条数typedef struct Contact{PeoInfo data[NUM_PEOPLE]; //存放添加了的通讯录信息int sz;//当前通讯录中有多少人}Contact;//重命名该结构体类型为Contact//————————函数声明//初始化通讯录void InitContact(Contact* pc);//添加联系人void AddContact(Contact* pc);//打印通讯录void PrintContact(const Contact* pc);//删除联系人void DelContact(Contact* pc);//查找void SearchContact(Contact* pc);//修改void ModContact(Contact* pc);#endif //FIRST_CONTACT_H
contact.c
//函数实现#include <string.h>#include "contact.h"//初始化通讯录函数的实现void InitContact(Contact* pc){pc->sz = 0;//使用memset()函数将pc->data中的所有字节都设置为0//pc->data找到的是保存通讯录信息的那个数组名,而sizef(数组名)计算的就是这个数组整个的大小memset(pc->data,0,sizeof(pc->data));}//添加联系人void AddContact(Contact* pc){//判断通讯录是否满了if(pc->sz == NUM_PEOPLE){printf("通讯录已满,无法添加");return;}//添加一个人的信息//当前sz是几,表示有几条信息。我们为data数组下表为sz的元素添加信息。//因为name、sex、tele、addr都是数组,而数组名就是地址,所以不用再取地址。//而age是int类型,所以要整体括起来取其地址。printf("请输入名字:");scanf("%s",pc->data[pc->sz].name);printf("请输入性别:");scanf("%s",pc->data[pc->sz].sex);printf("请输入年龄:");scanf("%d",&(pc->data[pc->sz].age));printf("请输入电话:");scanf("%s",pc->data[pc->sz].tele);printf("请输入地址:");scanf("%s",pc->data[pc->sz].addr);//添加之后,sz++,此时通讯录中就添加了一条信息pc->sz++;printf("== 添加成功 == \n");}//打印:肯定不会修改,所以加const修饰void PrintContact(const Contact* pc){int i;//printf(" 姓名 — 性别 — 年龄 — 电话 — 地址\n");//%10s表示要打印的字符串如果不满10个字符,不满的部分则会在其前面以空白补充。可以说是“右对齐”这种方式//%-10s表示要打印的字符串如果不满10个字符,不满的部分则会在其后面以空白补充。可以说是“左对齐”这种方式printf(" %-10s %-4s %-4s %-12s %-20s\n","姓名","性别","年龄","电话","地址");for(i=0; i<pc->sz ; i++){printf("%d. %-10s %-3s %-3d %-12s %-20s\n",i+1,pc->data[i].name,pc->data[i].sex,pc->data[i].age,pc->data[i].tele,pc->data[i].addr);}}//通过名字查找,并返回其在数组中的下标。加static表示这个函数只能在本文件中使用。static int FindByName(Contact* pc,char name[]){int i = 0;for(i=0 ; i<pc->sz ; i++){if(strcmp(pc->data[i].name,name) == 0){return i;}}//找不到返回-1return -1;}//删除联系人void DelContact(Contact* pc){if(pc->sz == 0){printf("通讯录为空\n");return;}char name[MAX_NAME] = {0};printf("请输入要删除的联系人姓名:");scanf("%s",name);//查找要删除的人//没有则结束方法int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");return;}//删除这个人,假设sz是1000,则pos=999时,999+1=1000,下标为1000,数组发生了越界//所以我们让sz-1,这样数组就不会越界了。但是最后一个元素删除不了了int i = 0;for(i=pos ; i<pc->sz-1 ; i++){//用下一个元素将这个元素覆盖。pc->data[i] = pc->data[i+1];}//我们如果要删除最后一个元素,则需要另外删除。//以上只能删除下标为:0~sz-1(不包括sz-1)的任意一条数据,但是下标为sz-1处的数据没办法删除。所以我们手动删除。if(pos == pc->sz-1)//当pos时最后一条数据的时候{//pc->data是数组首元素的地址,+pc->sz-1就是指向最后一个元素的指针//将其后的一个结构体大小的数据置为0。memset((pc->data+pc->sz-1),0,sizeof(PeoInfo));}//每次删除后,有效数据数sz-1。所以也可以不做对最后一个元素的删除。//就算不删除最后一个元素,每次执行后sz都减去1,这样即使最后一个元素没有删除,但是sz-1了,这个元素不显示了,也跟删除一样。//等到下次要添加数据的时候,会直接添加sz的位置,就把最后一个元素覆盖掉了。pc->sz--;printf("== 删除成功 ==\n");}//查找指定联系人void SearchContact(Contact* pc){char name[MAX_NAME] = {0};printf("请输入要查找人的姓名:");scanf("%s",name);//查找int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");}else{printf(" %-10s %-4s %-4s %-12s %-20s\n","姓名","性别","年龄","电话","地址");printf("%d. %-10s %-3s %-3d %-12s %-20s\n",pos+1,pc->data[pos].name,pc->data[pos].sex,pc->data[pos].age,pc->data[pos].tele,pc->data[pos].addr);}}//修改void ModContact(Contact* pc){char name[MAX_NAME] = {0};printf("请输入要修改的联系人的姓名:");scanf("%s",name);//查找int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");}else{//因为是按照名字修改,所以我们就不改名字了// printf("请输入名字:");// scanf("%s",pc->data[pc->sz].name);printf("修改性别为:");scanf("%s",pc->data[pos].sex);printf("修改年龄为:");scanf("%d",&(pc->data[pos].age));printf("修改电话为:");scanf("%s",pc->data[pos].tele);printf("修改地址为:");scanf("%s",pc->data[pos].addr);printf("== 修改成功 ==");}}
test.c
//测试通讯录的模块/** 通讯录:* 1. 通讯录中能够存放1000个人的信息。* 每个人的信息:名字、年龄、性别、电话、地址* 2. 增加、删除、修改、查找指定人的信息。* 3. 排序通讯录信息*/#include "contact.h"void menu(){printf("--------------------------------------------------\n");printf("----------1. add2. del 3. mod-------------\n");printf("----------4. search 5. sort 6.print------------\n");printf("------------------ 0. exit -------------------\n");printf("--------------------------------------------------\n");printf("请选择:");}//使用枚举enum Option{//从0开始递增1。与菜单上的选项相对应。EXIT,ADD,DEL,MOD,SEARCH,SORT,PRINT};int main(){int input = 0;//创建通讯录Contact con;//调用函数对通讯录进行初始化。InitContact(&con);do{menu();scanf("%d",&input);switch(input){case ADD://为通讯录中添加联系人。因为是要为已经创建好的通讯录添加,所以肯定是传址调用。AddContact(&con);break;case DEL:DelContact(&con);break;case MOD:ModContact(&con);break;case SEARCH:SearchContact(&con);break;case SORT://排序自己实现。//因为没有一个比较适合排序的选项,所以不再实现break;case PRINT:PrintContact(&con);break;case EXIT:printf("退出程序");break;default:printf("输入错误请重新输入。");break;}}while(input);}
动态开辟内存版本
contact.h
//类型定义、函数声明。#ifndef FIRST_CONTACT_H#define FIRST_CONTACT_H//头文件引入#include <stdio.h>#include <stdlib.h>#include <string.h>//常量的定义#define MAX_NAME 20#define MAX_SEX 10#define MAX_TELE 12#define MAX_ADDR 30#define DEF_SZ 3#define INC_SZ 2//类型定义typedef struct PeoInfo{//将其定义为常量,修改时可以在定义的位置修改。char name[MAX_NAME];char sex[MAX_SEX];int age;char tele[MAX_TELE];char addr[MAX_ADDR];}PeoInfo; //重命名该结构体类型为PeoInFo//因为我们为通讯录添加人的时候,需要知道往哪里添加,所以这里我们将其再次封装。typedef struct Contact{PeoInfo* data; //指向动态申请的空间,用来存放练习人的信息//PeoInfo* data = malloc(3*size0f(PeoInfo));int sz;//当前通讯录中有多少人int capacity; //记录当前通讯录的最大容量是几人}Contact;//重命名该结构体类型为Contact//————————函数声明//初始化通讯录void InitContact(Contact* pc);//添加联系人void AddContact(Contact* pc);//打印通讯录void PrintContact(const Contact* pc);//删除联系人void DelContact(Contact* pc);//查找void SearchContact(Contact* pc);//修改void ModContact(Contact* pc);//销毁通讯录DestoryContact(Contact* pc);#endif //FIRST_CONTACT_H
contact.c
//函数实现#include "contact.h"//初始化通讯录函数的实现void InitContact(Contact* pc){pc->data = (PeoInfo*)malloc(DEF_SZ* sizeof(PeoInfo));if(pc->data == NULL){perror("InitContact");return;}pc->sz = 0;pc->capacity = DEF_SZ;}//添加联系人void AddContact(Contact* pc){//判断通讯录是否满了,满了就扩容if(pc->sz == pc->capacity){//扩容后的大小:(当前最大容纳几人+2)*每人所占大小PeoInfo* ptr = (PeoInfo*)realloc(pc->data,(pc->capacity+INC_SZ)* sizeof(PeoInfo));if(ptr != NULL){pc->data = ptr;pc->capacity += INC_SZ;printf("== 通讯录容量不够,已进行扩容 ==\n");}else{perror("AddContact");printf("通讯录扩容失败\n");return;}}//添加一个人的信息//当前sz是几,表示有几条信息。我们为data数组下表为sz的元素添加信息。//因为name、sex、tele、addr都是数组,而数组名就是地址,所以不用再取地址。//而age是int类型,所以要整体括起来取其地址。printf("请输入名字:");scanf("%s",pc->data[pc->sz].name);printf("请输入性别:");scanf("%s",pc->data[pc->sz].sex);printf("请输入年龄:");scanf("%d",&(pc->data[pc->sz].age));printf("请输入电话:");scanf("%s",pc->data[pc->sz].tele);printf("请输入地址:");scanf("%s",pc->data[pc->sz].addr);//添加之后,sz++,此时通讯录中就添加了一条信息pc->sz++;printf("== 添加成功 == \n");}//打印:肯定不会修改,所以加const修饰void PrintContact(const Contact* pc){int i;//printf(" 姓名 — 性别 — 年龄 — 电话 — 地址\n");//%10s表示要打印的字符串如果不满10个字符,不满的部分则会在其前面以空白补充。可以说是“右对齐”这种方式//%-10s表示要打印的字符串如果不满10个字符,不满的部分则会在其后面以空白补充。可以说是“左对齐”这种方式printf(" %-10s %-4s %-4s %-12s %-20s\n","姓名","性别","年龄","电话","地址");for(i=0; i<pc->sz ; i++){printf("%d. %-10s %-3s %-3d %-12s %-20s\n",i+1,pc->data[i].name,pc->data[i].sex,pc->data[i].age,pc->data[i].tele,pc->data[i].addr);}}//通过名字查找,并返回其在数组中的下标。加static表示这个函数只能在本文件中使用。static int FindByName(Contact* pc,char name[]){int i = 0;for(i=0 ; i<pc->sz ; i++){if(strcmp(pc->data[i].name,name) == 0){return i;}}//找不到返回-1return -1;}//删除联系人void DelContact(Contact* pc){if(pc->sz == 0){printf("通讯录为空\n");return;}char name[MAX_NAME] = {0};printf("请输入要删除的联系人姓名:");scanf("%s",name);//查找要删除的人//没有则结束方法int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");return;}//删除这个人,假设sz是1000,则pos=999时,999+1=1000,下标为1000,数组发生了越界//所以我们让sz-1,这样数组就不会越界了。但是最后一个元素删除不了了int i = 0;for(i=pos ; i<pc->sz-1 ; i++){//用下一个元素将这个元素覆盖。pc->data[i] = pc->data[i+1];}//我们如果要删除最后一个元素,则需要另外删除。//以上只能删除下标为:0~sz-1(不包括sz-1)的任意一条数据,但是下标为sz-1处的数据没办法删除。所以我们手动删除。if(pos == pc->sz-1)//当pos时最后一条数据的时候{//pc->data是数组首元素的地址,+pc->sz-1就是指向最后一个元素的指针//将其后的一个结构体大小的数据置为0。memset((pc->data+pc->sz-1),0,sizeof(PeoInfo));}//每次删除后,有效数据数sz-1。所以也可以不做对最后一个元素的删除。//就算不删除最后一个元素,每次执行后sz都减去1,这样即使最后一个元素没有删除,但是sz-1了,这个元素不显示了,也跟删除一样。//等到下次要添加数据的时候,会直接添加sz的位置,就把最后一个元素覆盖掉了。pc->sz--;printf("== 删除成功 ==\n");}//查找指定联系人void SearchContact(Contact* pc){char name[MAX_NAME] = {0};printf("请输入要查找人的姓名:");scanf("%s",name);//查找int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");}else{printf(" %-10s %-4s %-4s %-12s %-20s\n","姓名","性别","年龄","电话","地址");printf("%d. %-10s %-3s %-3d %-12s %-20s\n",pos+1,pc->data[pos].name,pc->data[pos].sex,pc->data[pos].age,pc->data[pos].tele,pc->data[pos].addr);}}//修改void ModContact(Contact* pc){char name[MAX_NAME] = {0};printf("请输入要修改的联系人的姓名:");scanf("%s",name);//查找int pos = FindByName(pc,name);if(pos == -1){printf("通讯录中不存在此联系人\n");}else{//因为是按照名字修改,所以我们就不改名字了// printf("请输入名字:");// scanf("%s",pc->data[pc->sz].name);printf("修改性别为:");scanf("%s",pc->data[pos].sex);printf("修改年龄为:");scanf("%d",&(pc->data[pos].age));printf("修改电话为:");scanf("%s",pc->data[pos].tele);printf("修改地址为:");scanf("%s",pc->data[pos].addr);printf("== 修改成功 ==");}}//销毁通讯录DestoryContact(Contact* pc){free(pc->data);pc->data = NULL;pc->sz = 0;pc->capacity = 0;}
test.c
//测试通讯录的模块/** 动态增长的通讯录:* 1. 通讯录初始化后,能存放三个人的信息* 当空间存满之后,扩容两个人的空间。3+2+2+2+2....* 每个人的信息:名字、年龄、性别、电话、地址* 2. 增加、删除、修改、查找指定人的信息。* 3. 排序通讯录信息*/#include "contact.h"void menu(){printf("--------------------------------------------------\n");printf("----------1. add2. del 3. mod-------------\n");printf("----------4. search 5. sort 6.print------------\n");printf("------------------ 0. exit -------------------\n");printf("--------------------------------------------------\n");printf("请选择:");}//使用枚举enum Option{//从0开始递增1。与菜单上的选项相对应。EXIT,ADD,DEL,MOD,SEARCH,SORT,PRINT};int main(){int input = 0;//创建通讯录Contact con;//调用函数对通讯录进行初始化。//为data在堆上申请一块连续的空间//sz=0//将capacity初始化为当前data指向的空间快的最大容量。InitContact(&con);do{menu();scanf("%d",&input);switch(input){case ADD://为通讯录中添加联系人。因为是要为已经创建好的通讯录添加,所以肯定是传址调用。AddContact(&con);break;case DEL:DelContact(&con);break;case MOD:ModContact(&con);break;case SEARCH:SearchContact(&con);break;case SORT://排序自己实现。//因为没有一个比较适合排序的选项,所以不再实现break;case PRINT:PrintContact(&con);break;case EXIT://销毁通讯录 —— 释放动态开辟的内存DestoryContact(&con);printf("程序退出,通讯录已销毁");break;default:printf("输入错误请重新输入。\n");break;}}while(input);return 0;}
练习
结构体大小的计算
//32位系统环境,编译选项为4字节对齐,那么A与B结构体的大小是:struct A{int a; //对齐数:4 占用:0~3short b;//对齐数:2 占用:4~5。浪费:6~7int c; //对齐数:4 占用:8~11char d; //对齐数:1 占用:12//12+1=13,最大对齐数是4,大于13的最小的4的倍数是16,所以结构体大小是:16};struct B{int a; //对齐数:4 占用:0~3short b;//对齐数:2 占用:4~5char c; //对齐数:1 占用:6。7被浪费int d; //对齐数:4 占用:8~11//11+1=12,最大对齐数是4,所以结构体大小是:12};//编译选项为:四字节对齐//long占4个字节struct tagTest1{short a; //对齐数:2 占用:0~1char d;//对齐数:1 占用:2long b;//对齐数:4 占用:4~7long c;//对齐数:4 占用:8~11//11+1=12 ,最大对齐数是4,所以结构体大小是12};struct tagTest2{long b;//对齐数:4 占用:0~3short c; //对齐数:2 占用:4~5char d;//对齐数:1 占用:6long a;//对齐数:4 占用:8~11//11+1=12 ,最大对齐数是4,所以结构体大小是12};struct tagTest3{short c; //对齐数:2 占用:0~1long b;//对齐数:4 占用:4~7char d;//对齐数:1 占用:8long a;//对齐数:4 占用:12~15//15+1=16 ,最大对齐数是4,所以结构体大小是16};
如有以下宏定义和结构定义 ,当A=2,B=3时,pointer指向的空间大小
#include <stdlib.h>//当A=2,B=3时,就成为//#define MAX_SIZE A+B#define MAX_SIZE 2+3//char类型位段struct _Record_Struct{//先开辟一个字节空间//占用4个bit,还剩4个bitunsigned char Env_Alarm_ID :4;//占用2个bit,还剩2个bitunsigned char Paral :2;//要占用1个字节,也就是8个bit,而剩下的2个不够用,浪费掉。//再开辟一个字节空间,全部占用。unsigned char state;//再开辟一个字节空间,占用4个bitunsigned char avail :4;//此时_Record_Struct位段结构体就占用3个字节空间}*Env_Alarm_Record;int main(){//3*2+3 = 9//struct _Record_Struct* pointer = (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct)*MAX_SIZE);printf("%d\n",sizeof(struct _Record_Struct)*MAX_SIZE);return 0;}
位段练习
int main(){unsigned char puc[4];struct tagPIM{//开辟一个字节空间,占用一个字节空间unsigned char ucPim1;//再开辟一个字节空间,占用1个bit,还剩7个unsigned char ucData0 : 1;//占用2个bit,还剩5个unsigned char ucData1 : 2;//占用3个bit,还剩两个unsigned char ucData2 : 3;}*pstPimData;//将指向数组首元素的指针,转换为tagPIM指针类型,赋给pstPimData//也就是说pstPimDate此时指向puc数组的开头。pstPimData = (struct tagPIM*)puc;//将puc数组的4个字节设置为0。memset(puc,0,4);//pstPimData虽然操作的是指向数组的空间,但是会按照结构体对变量的分配,进行赋值。//是对数组的空间进行改变的。//将指向的第一个字节,赋值为2。就成为:0000 0010pstPimData->ucPim1 = 2;//将指向的第二个字节的第一个bit设置为:3。3转换为二进制是11,因为只能存储1位,所以发生截断,取后1位存储进去。//第二个字节就成为:0000000 1pstPimData->ucData0 = 3;//将指向的第二个字节的第2~3位置的bit设置为:4。4转换为二进制是100,因为只能存储2位,所以发生截断,取后2位存储进去。//第二个字节就成为:00000 001pstPimData->ucData1 = 4;//将指向的第二个字节的第4~6位置的bit设置为:5。5转换为二进制是101,可以存储3位,存储进去。//第二个字节就成为:00 101001 也就是:0010 1001pstPimData->ucData2 = 5;//此时数组的四个字节空间就称为:00000010 00101001 00000000 00000000//转换为16进制就是:02 29 00 00//打印://%x表示以十六进制输出,02表示每个字节以两位十六进制数输出。位数不够的补0printf("%02x %02x %02x %02x\n",puc[0],puc[1],puc[2],puc[3]);return 0;}
联合体大小
//联合的大小至少是最大成员的大小//当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。#include <stdio.h>union Un{short s[7]; //对齐数:2 大小:14int n;//对齐数:4 大小:4//该联合体大小最小是14,最大对齐数是4,所以该联合体大小:16};int main(){printf("%d\n",sizeof(union Un));//16return 0;}
联合体+大小端
#include <stdio.h>int main(){union{short k; //对齐数:2 大小:2char i[2];//对齐数:1 大小:2//该联合体的大小是两个字节。i数组与k共用这两个字节。}*s,a;s = &a;//i中第一个字节就是:十六进制的39,第二个字节是:十六进制的38s->i[0] = 0x39;s->i[1] = 0x38;//因为是联合体,所以k中的第一个字节也是:39;第二个字节:38。//因为采用小端存储,也就是低位低地址,所以39是低位的那个字节。所以k取出来应该是:0x3839//而存进去的时候就是:39 38,与以上相同。printf("%x\n",a.k);return 0;}
枚举常量的值
enum ENUM_A{//默认从0开始。递增1X1,//0Y1,//1Z1=255,//Z1设置了值,下一个枚举常量的值就=Z1+1,以此类推。A1,//256B1//257};int main(){enum ENUM_A enumA = Y1;//1enum ENUM_A enumB = B1;//257printf("%d %d\n",enumA,enumB); //1 257return 0;}
重命名之后的匿名结构体大小计算
typedef struct{int a;//0~3char b;//4short c;//6~7short d;//8~9//9+1=10,最大对齐数:4,所以结构体大小12}AA_t;int main(){//这个虽然是匿名结构体,但是被重命名为AA_t,所以这里计算的还是这个结构体的大小printf("%d\n",sizeof(AA_t));return 0;}