0,引言
我们知道,如果需要保存一个整数,如:学号,定义一个int类型的变量
如果需要保存一个小数,如:成绩,定义一个 double类型的变量
如果需要保存一个字符串,如:姓名,定义一个字符数组 char name[50];
但是实际问题中,有些数据比较复杂,如:怎么保存一个“学生”数据呢? 没办法用一个单一的数据去保存,因为学生有很多属性。 所以c语言中,允许程序员自定义“组合类型” -》 构造类型 用来保存一些复杂的数据 -》 结构体 共用体 枚举
1,结构体
结构体是需要程序员自己去构造的一种类型
用 struct 关键字来构造
1.1 怎么构造/声明一个结构体类型 struct 结构体名 { 成员1类型 成员1名字; 成员2类型 成员2名字; ..... 成员n类型 成员n名字; }; 成员个数 >= 1即可 上面的语法就是声明了一个新的类型,新类型的名字叫做 : struct 结构体名 一般写在函数外面。如果有.h文件,就写在 .h文件里面 如: struct test { int a; double b; }; 声明了一个结构体类型,叫做 struct test。 声明:就是告诉编译器现在多了一个新的数据类型,-》 struct test 意思是说:从此刻起,数据类型除了 int double char long ......这些基本类型之外,我们多了 一个数据类型 -》 struct test 1.2 定义结构体类型的变量 怎么定义?符合定义变量的基本语法: 数据类型 变量名; struct test t; 定义一个变量,名字叫做t,类型是 struct test ,变量t中包含了两个成员 a,b 1.3 如何访问结构体变量中的成员 使用 . 和 -> 这两个运算符 (1) 结构体变量名.成员变量名 t.a t.b t.a = 10; t.b = 4.7; printf("t.a=%d,t.b=%lf\n",t.a,t.b); 练习: 声明一个结构体类型 struct student ,再定义一个结构体变量,然后给各个成员赋值 (2) (*结构体指针).成员变量名 struct student * p = &t;//定义指针变量,指向结构体变量t (*p).a (*p).b
(3) 结构体指针->成员变量名 struct student * p = &t;//定义指针变量,指向结构体变量t p->a p->b 具体见代码 1.4 结构体变量初始化 ,用 {} (1) 按照声明结构体类型时成员的顺序进行初始化,用逗号隔开 struct test t = {15,1.8}; struct student s = {1,85.5,"zhangsan"}; (2) 不按顺序,指定初始化某个成员 struct test t = {.b=2.5}; struct student s = {.id=1,.name="zhangsan"}; (3) 初始化结构体数组 struct student s[3] = {{1,80,"zhangsan"},{2,85,"lisi"},{3,70,"wangwu"}};
结构体变量之间的赋值,可以直接用 = 符号进行整体赋值 struct student s1 ={1,80,"zhangsan"}; struct student s2 ={2,85,"lisi"}; s1 = s2;
练习题: 定义一个 struct student数组并初始化,保存5个学生的信息,然后按照成绩进行排序(从小到大)
1.5 结构体各个成员在内存中的布局 -》字节对齐 最基本原则:各个成员按顺序存储 strcut test { int a; double b; }; struct test t; printf("%lld\n",sizeof(t));//16 为什么??? 因为构造类型存在字节对齐的问题。 字节对齐是指在内存中存储各种类型数据时,为了优化内存存储和访问效率, 需要把特定的数据类型的地址对齐到某个固定的边界上,通常是该数据类型大小的整数倍。 具体规则(以64位机器为例): 1,基本对齐原则 数据类型的起始地址必须是该数据类型大小的整数倍。 例如: 一个int类型的数据,地址必须是4的整数倍 一个double类型的数据,地址必须是8的整数倍 .... 2,填充字节原则 为了满足基本对齐原则,在相邻的成员之间可能需要填充若干个字节,使得后续数据类型能够满足对齐规则 这些填充的数据没有意义 3,结构体整体对齐规则 对于结构体等构造类型,其对齐方式必须满足最基本类型成员对齐方式的要求。 并且该结构体的起始地址是最大基本类型成员大小的整数倍, 该结构体整体大小也是最大基本类型成员大小的整数倍 例如:如果一个结构体中有3个成员,分别是 int ,double ,char类型,那么该结构体 的起始地址肯定是 8的整数倍,并且结构体的整体大小也是8的整数倍 如果一个结构体中有3个成员,分别是 int ,double ,char [10]类型,那么该结构体 的起始地址肯定是 8的整数倍(char [10]不是基本类型,当作10个单独的char来看待), 并且结构体的整体大小也是8的整数倍
struct test1 { char a; short b; int c; }; sizeof(struct test1) -> 8 内存布局(*代表填充) -* -- ---- struct test2 { short b; int c; char a; }; sizeof(struct test2) -> 12 内存布局(*代表填充) --** ---- -*** struct test3 { short b; char d; int c; char a; char e; }; sizeof(struct test3) -> 12 内存布局(*代表填充) -- -* ---- - -** struct student { int id; double score; char name[20]; }; sizeof(struct student) -> 40 ---- **** -------- 20--- ****
struct student { char name[20]; int id; double score; }; sizeof(struct student) -> 32 20--- ---- --------
struct test4 { double data; char s[10]; struct student stu; }; sizeof(struct test4) ->
2,共用体(联合体)
也是构造类型, 用关键字 union 声明
声明语法:
union 共用体名字
{
成员1类型 成员1名字;
成员2类型 成员2名字;
....
成员n类型 成员n名字;
};
访问共用体成员的方法和结构体完全一样
和结构体的区别在于: 结构体各个成员都有独立的内存空间,互不影响 共用体各个成员公用一块内存,不管有几个成员,都是共用一块内存 公用一块内存怎么理解?各个成员的首地址相同 union test { char a; //1字节 int b; //4字节 double c;//8字节 }; 三个成员a,b,c首地址相同 union test t;//定义共用体变量 printf("%p\n%p\n%p\n%p\n",&t,&t.a,&t.b,&t.c);//打印的四个地址的值完全一样 共用体总共占几个字节,由最大的那个成员决定。 改变任何一个成员的值,其它的成员都会改变。 大小端模式 内存中是以字节为单位存储数据的,如果一个数据超过1字节,就会占用多个内存单元, 这多个内存单元是按照什么顺序来存储这个数据的呢? 不同的机器有不同的存储顺序,有两种:被称之为大端模式和小端模式 int b = 0x41424344;//占4字节 这四个字节的数据分别为 0x41 0x42 0x43 0x44 小端模式: 低地址 存 低字节数据 高地址 存 高字节数据 大端模式: 低地址 存 高字节数据 高地址 存 低字节数据 具体分析见图 练习: int a = 0x12345678; char * p = (char *)&a; printf("%d\n",*p); 请问在大端或小端模式下,输出结果是什么? 大端: 18 小端: 120 练习: 写一个程序,测试你的电脑是大端还是小端? int a = 0x12345678; char * p = (char *)&a; //printf("%d\n",*p); if(*p == 0x12) { 大端 } else { 小端 } 经过测试,我们的电脑是小端模式 练习:分析大小端模式下 以下代码的输出结果 union test { int a; short b; }; void test3() { union test t; t.a = 0; t.b = 0x1234; printf("t.a=0x%x,t.b=0x%x\n",t.a,t.b); } 大端模式: 输出 t.a=0x12340000 t.b=0x1234 小端模式:输出 t.a=0x1234 t.b=0x1234 void test4() { union test t; t.b = 0; t.a = 0x12345678; printf("t.a=0x%x,t.b=0x%x\n",t.a,t.b); } 大端模式: 输出 t.a=0x12345678 t.b=0x1234 小端模式:输出 t.a=0x12345678 t.b=0x5678
共用体适用于那种多个成员不同时使用的情况
3,枚举
把所有可能的值都列举出来
有些情况,一个变量的值是有一定的范围
int direction;//保存方向,约定 上 1 下 2 左 3 右 4 direction = 10;//万一给了一个其它的值怎么办 direction = 2;//就是是约定的值,也需要去找对应的注释才知道 2代表这哪个方向 enum dire //声明一个方向的枚举类型,并且把所有可能的值都列举出来 { UP, DOWN, LEFT, RIGTH }; 定义一个这样的变量 : enum dire d; d = UP; d = DOWN;//这样可以提高代码的可读性 枚举{}里面列举出来的值本质其实就是一个整数: 如果没有特殊说明,默认是从0开始,依次递增1 enum dire //声明一个方向的枚举类型,并且把所有可能的值都列举出来 { UP, //0 DOWN,//1 LEFT,//2 RIGTH//3 }; 如果有特殊说明,那就按照说明来处理 enum dire //声明一个方向的枚举类型,并且把所有可能的值都列举出来 { UP = 5, //5 DOWN,//6 LEFT,//7 RIGTH//8 }; enum dire //声明一个方向的枚举类型,并且把所有可能的值都列举出来 { UP, //0 DOWN,//1 LEFT = 5,//5 RIGTH//6 };
4, typedef
typedef用来给一个已有类型取一个新的名字
比如: typedef int xxx;//那么xxx就是int的另一个名字
xxx a = 10;//定义了一个xxx/int类型的变量
当然了,取一个新的名字要有意义,上面这样随便取没有必要 typedef unsigend int uint;//给unsigend int 取了一个新的名字 uint typedef unsigend char uchar;//给unsigend char 取了一个新的名字 uchar uint a = 100; uint b=10,c=20; uint *p,*q; uchar b = 65; ....
struct student//声明类型 { int id; double score; char name[20]; }; typedef struct student Student;//取另一个新的名字 struct student s; Student s1;//定义了一个结构体变量 typedef struct student { int id; double score; char name[20]; }Student; //声明类型并取一个名字
for(i=0;i<3;i++) strcpy(&x[i][0],ch); abc\0 abc\0 abc\0 for(i=0;i<3;i++) printf("%s",&x[i][i]); abcbcc int a[12] = {0},*p[3],**pp,i;//p 是 指针数组 pp是二级指针 for(i=0;i<3;i++) p[i] = &a[i*4]; p[0] = &a[0]; p[1] = &a[4]; p[2] = &a[8]; pp = p; *(*(p+3)+1) <-> *(*(&p[0]+3)+1) <-> *(p[3]+1) <-> *(*(p+2)+2) <-> .... *(p[2]+2) <-> *(&a[8] + 2) <-> *(&a[0]) <-> a[10] pp[0][1] <-> *(*(pp+0)+1) <-> *(p[0]+1) <-> *(&a[0]+1) <-> a[1] char b1[] = "abcdefg"; char pb = b1 + 3; while(--pb>=b1) strcpy(b2,pb);//abcdefg
int n = 2,*p = &n,*q = p; -> int n = 2; int * p = &n; int * q = p;
void (*func_t) (int ,float); 定义了一个函数指针变量,名字 func_t ,指向的函数无返回值,有两个参数,分别是 int和 float类型
typedef void (*func_t) (int ,float); 声明了一个类型 func_t ,这个类型是函数指针类型,指向的函数无返回值,有两个参数,分别是 int和 float类型 func_t p;