struct tag
{
member-list;
}variable-list;例如要描述一个学生:
struct stu
{
char name[20];
int age;
char number[20];
};这里的分号不能丢struct stu
{
char name[20];//姓名
int age;//年龄
char number[20];//学号
};
int main()
{
struct stu s1={"zhangsan",20,"20203029292"};//按顺序初始化
printf("%s\n", s1.name);
printf("%d\n", s1.age);
printf("%s\n", s1.number);
printf("\n");
//不按顺序初始化
struct stu s2 = { .age = 18,.name = "lisi",.number = "32040320304" };
printf("%s\n", s2.number);
printf("%d\n", s2.age);
printf("%s\n", s2.name);
return 0;struct
{
char name[20];//姓名
int age;//年龄
char number[20];//学号
}s1,s2;这个结构体在声明的时候省略了结构体标签(tag),这种写法只能像上面这种写法,其余写法均为错误,这种匿名结构体类型只能使用一次,后期不能使用这个类型在创建变量,只能再创建结构体的同时创建变量,以后不能再用了。
struct Node
{
int data;
struct Node* next;
};我们前面学习了typedef重命名,是不是也可以将这个结构体重新命名一下?当然是可以了。
typedef struct Node
{
int data;
struct Node* next;
}Node;通过上面的代码,我们就可以将这个结构体重新命名为Node,后面我们就可以直接使用这个新名字进行结构体的初始化,将struct Node重新命名为Node。
typedef struct stu
{
char name[20];//姓名
int age;//年龄
char number[20];//学号
}stu;
int main()
{
stu s1={"zhangsan",20,"20203029292"};//按顺序初始化
printf("%s\n", s1.name);
printf("%d\n", s1.age);
printf("%s\n", s1.number);
printf("\n");
//不按顺序初始化
stu s2 = { .age = 18,.name = "lisi",.number = "32040320304" };
printf("%s\n", s2.number);
printf("%d\n", s2.age);
printf("%s\n", s2.name);
return 0;
}通过上面的学习,我们基本上掌握了结构体的基本使用。接下来让我们深入讨论一个问题:计算结构体的大小。
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处(第一个成员总是放在偏移量为0的地址上)
2.从第二个成员变量开始,都要对齐到某个对齐数的整数倍的地址处 对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。(vs中默认的值为8,Linux中gcc没有默认对齐数,对齐数就是成员自身的大小)
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中的最大的)的整数倍
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
struct s3
{
double d;
char c;
int i;
};
struct s4
{
char c1;
struct s3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
printf("%d\n", sizeof(struct s3));
printf("%d\n", sizeof(struct s4));
return 0;
}接下来,让我们来看看上面的结果分别是什么?
根据上面的规则,sizeof(struct s1)==12,sizeof(struct s2)==8,sizeof(struct s3)==16,sizeof(struct s4)==32
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间: 让占用空间小的成员尽量集中在一起
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
#include <stdio.h>
#pragma pack(1)//设置的一般为2的次方数
//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//
//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么
printf("%d\n", sizeof(struct S));
return 0;
}通过上面代码中的#pragma pack( 1 )就可以改变编译器的默认对齐数,将默认对齐数8改为1,#pragma pack()是取消设置的对齐数,还原为默认。如果需要计算结构体的大小,可以根据规则计算出大小。
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;
}在上面两种结构体传参的方式中,推荐传址调用,这样可以减少空间的使用
位段的声明和结构体是类似的,有两个不同:
比如:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};A就是一个位段类型,冒号后面的数字分别表示a占2个比特位,b占5个比特位,c占10个比特位,d占30个比特位。
前面我们学习了如何计算结构体的大小,那位段A所占内存的大小是多少?
printf("%d\n",sizeof(struct A));位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟。
在开辟空间的时候,会出现下面的几个问题:
1.一个字节(整型)的内存中,到底是从左向右使用,还是从右向左使用不确定
2.剩余的空间不能满足下一个成员的时候,是否浪费不确定。(vs编译器认为空间需要被浪费)
我们先假设从右向左使用,通过编译器我们可以得出sizeof(struct A)==8,这是什么原因呢?

我们知道a占2个比特位,b占5个比特位,c占10个比特位,d占30个比特位,加起来一共是47个比特位,6个字节是48个比特位,很明显是可以存下47个比特位的,但编译器给我们的结果却是8,这就说明存在空间浪费,剩余的空间不能满足下一个成员的时候,vs编译器认为这些空间需要被浪费,那这样的话,a,b,c一共是17个比特位就需要申请4个字节,d占30个比特位需要再申请4个字节的空间,这样就达到了8个字节的空间。
struct s
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct s s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}10的二进制是00001010;12的二进制是00001100;3的二进制是00000011;4的二进制是00000100。


我们分别将上面以16进制的形式打印地址:从右向左0x62 03 04 ,从左向右0x58 18 40

通过vs编译器,我们很明显看出vs是从从右向左的形式使用的。
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
通过上面的学习,我们知道位段的几个成员共用一个字节,内存中每个字节会分配一个地址,但是一个字节内部的比特位是没有地址的。共用一个字节的位段成员并不是每个成员都有地址。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct A s = { 0 };
//scanf("%d\n", &s.a);这是错误的
//正确写法
int n = 0;
scanf("%d\n", &n);
s.a = n;
return 0;
}像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同类型。联合体的关键字是union。但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用一块内存空间。所以联合体也叫:共用体。
给联合体其中一个成员赋值,其他成员的值也会跟着变化。
//联合体的声明 union tag { //成员 }:
#include<stdio.h>
//联合体的声明
union s
{
char c;
int i;
};
int main()
{
//联合变量的定义
union s un = { 0 };
//计算联合体变量的大小
printf("%d\n", sizeof(un));
return 0;
}联合体的成员是共用一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)
//代码1
union s
{
char c;
int i;
};
int main()
{
union s un = { 0 };
printf("%p\n", &un);
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
return 0;
}//代码2
union s
{
char c;
int i;
};
int main()
{
union s un = { 0 };
un.i = 0x11223344;
un.c = 0x55;
return 0;
}代码1输出的结果是001AF85C,001AF85C,001AF85C ;代码2输出的结果是11223344。我们发现代码1输出的三个结果是一模一样的,代码2的输出中,我们发现将i的第4个字节的内容由44改成了55


通过仔细分析,我们画出un的内存分布图

通过上面的学习,我们可以得出联合体的使用情况:用 i 的时候不用 c,用c 的时候不用 i ,换句话来说就是 i 和 c 不能同时使用(联合体的成员不同时使用)
1.联合的大小至少是最大成员的大小
2.当最大成员大小不是最大对齐数的整数倍数的时候,就要对齐到最大对齐数的整数倍。
union s
{
int i;
char c[5];
};
int main()
{
printf("%d\n", sizeof(union s));
return 0;
}在计算数组的对齐数的时候,要按一个元素的大小来算对齐数,我们知道这个联合体的最大成员的大小是5,但5不是最大对齐数4的整数倍,所以我们需要对齐到8,由此可以得出这个联合体的大小是8。
由于联合体只为最大成员分配足够的内存,所以使用联合体是可以节省空间的,举例:
比如,我们要搞⼀个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、页数 杯⼦:设计 衬衫:设计、可选颜色、可选尺寸
库存量、价格和商品类型是共同拥有的,书名、作者、页数是图书单独拥有的,设计是杯子单独拥有的;设计、可选颜色,可选尺寸是衬衫单独拥有的。当我们需要图书的时候,就不需要杯子和衬衫所独有的信息;当我们需要杯子的时候,就不需要图书和衬衫所独有的信息;当我们需要衬衫的时候,就不需要杯子和图书所独有的信息。这不就是一个联合体的使用情况嘛!
话不多说,看代码:
struct gift_list
{
int stock_number;
double price;
int type;
union
{
struct a
{
char name[20];
char author[20];
int num_of_page;
}book;
struct b
{
char design[20];
}mug;
struct c
{
char design[20];
char colors;
int size;
}skirt;
}item;
}list;判断大小端字节序
int check()
{
union un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小段\n");
}
else
printf("大端\n");
return 0;
}上面代码中的 u.c 是拿出第一个字节,1在内存中的地址是以16进制形式打印,1的地址是00 00 00 01,1如果以小端字节序存储的话,那就是01 00 00 00;大端字节序存储就是00 00 00 01,然后我们进行判断是否等于1,如果等于1(16进制的01也是1),那就是小端字节序存储,如果等于0,那就是大端字节序存储。
枚举顾名思义就是一一列举,把所有可能的取值一一列举出来。枚举的关键词是enum
enum Days//星期
{
Mon,
Tus,
Wed,
Thur,
Fir,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};以上定义的enum Days,enum Sex都是枚举类型。{ }中的内容是枚举类型可能的取值,也叫作枚举常量。另外这些取值都是有值的,因为他们叫做常量,默认是从0开始,依次递增1,这时就有小伙伴提出疑问了,难道这些值都是不能改的吗?当然不是,我们可以在声明枚举类型的时候赋一个初始值,如果没有赋值,会依次递增1。比如:
enum Sex//性别
{
MALE=2,
FEMALE=5,
SECRET=7
};那我们该如何使用枚举类型创建变量呢?接下来,让我们使用枚举类型来创建变量:
枚举类型 变量=初值(初值是大括号内的可能内容),比如: enum Sex a=MALE;
#include<stdio.h>
enum sex
{
MALE,
FEMALE,
SACRET
};
int main()
{
//使用枚举类型来创建变量
enum sex s = MALE;//使用枚举创建变量的时候,
//初值是enum sex大括号内的可能内容
if (s == MALE)
printf("男\n");
else if (s == FEMALE)
printf("女\n");
else
printf("保密\n");
return 0;
}void menu()
{
printf("********************\n");
printf("****1.add 2.sub ****\n");
printf("****3.mul 4.div ****\n");
printf("**** 0.exit ****\n");
}
enum option
{
exit,
add,
sub,
mul,
div
};
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, y = 0;
do
{
menu();
printf("请选择>>");
scanf("%d", &input);
printf("请输入两个数>>");
scanf("%d %d", &x, &y);
switch (input)
{
case add:
printf("%d\n", Add(x, y));
break;
case sub:
printf("%d\n", Sub(x, y));
break;
case mul:
printf("%d\n", Mul(x, y));
break;
case div:
printf("%d\n", Div(x, y));
break;
case exit:
break;
default:
break;
}
} while (input);
return 0;
}