①结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
②结构是用来定义复杂对象的
struct tag
{
member - list;
}varisble - list;
struct:关键字
tag:结构体标签名
struct tag:结构体类型名
{ }里面放的是结构体的成员:即成员变量的列表,每个成员可以是不同类型的变量。
varisble - list:变量列表
struct Book
{
char name[20];
int price;
char id[15];
}b4,b5,b6;
int main()
{
struct Book b1;
struct Book b2;
struct Book b3;
//b1 b2 b3 b4 b5 b6都是结构体变量,都是我们创建出来的书的对象,但不同的是,b1 b2 b3 是局部变量,b4 b5 b6是全局变量,因为它是在主函数外部创建的
return 0;
}
在声明结构的时候,可以不完全地声明
如:匿名结构体类型;省略结构体标签tag
struct
{
char m;
int n;
char id;
}s1;
如:匿名结构体指针类型;省略结构体标签tag
struct
{
int a;
char b;
float c;
}a[20], *p;
注意:匿名结构体类型只能用一次,因为它没有名字
在结构体里面,结构体自引用的实现 不是包含同类型的结构体变量,而是包含同类型的结构体的指针
错误的自引用方式例1
struct Node
{
int data;
struct Node next;//error:在结构中包含一个类型为该结构本身的成员是不可取的
};
正确的自引用方式
struct Node
{
int data;
struct Node* next;//right
};
错误的自引用方式例2
typedef struct
{
int data;
Node* next;
}Node;
正确的自引用方式:
typedef struct Node
{
int data;
struct Node* next;
}Node;
有了结构体类型,如何定义变量呢?
结构体变量的基本创建举例:声明类型的同时直接创建变量
struct S
{
char ch;
int i;
}s1,s2;//创建全局变量
int main()
{
struct S s3,s4;//创建局部变量
return 0;
}
struct S
{
char ch;
int i;
}s1, s2;//创建全局变量
int main()
{
struct S s3 = {'x',20};//结构体初始化
return 0;
}
结构体里面包含结构体该如何初始化,创建及访问呢?
struct S
{
char ch;
int i;
}s1, s2;//创建全局变量
struct B
{
double d;
struct S s;
char ch;
};
int main()
{
//struct S s3 = { 'x', 20 };//结构体初始化
struct B sb = { 3.14, { 'w', 100 }, 'q' };
printf("%lf %c %d %c\n", sb.d, sb.s.ch, sb.s.i, sb.ch);
return 0;
}
#include
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()
{
struct S1 s1 = { 0 };
printf("%d\n", sizeof(s1));
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2));
struct S3 s3 = { 0 };
printf("%d\n", sizeof(s3));
struct S4 s4 = { 0 };
printf("%d\n", sizeof(s4));
return 0;
}
代码画图解析S3、S4的结果:
1.结构体的第一个成员放在结构体变量在内存中存储位置的0偏移处开始
2.从第二个成员往后的所有成员,都放在一个对齐数(成员的大小和默认对齐数的较小值)的整数的整数倍的地址处
3.结构体的总大小是结构体的所有成员的对齐数中最大的那个对齐数的整数倍。注:VS的默认对齐数是8
Linux中的默认值为4
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍。
1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
处取某些特定类型的数据,否则抛出硬件异常。2.性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,这该如何做到呢?
解决方法1:让占用空间小的成员尽量集中在一起。
例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
分析:S1和S2类型的成员一模一样,但是S1所占空间大于S2所占空间。
解决方法2:修改默认对齐数
使用#pragma这个预处理指令,可以改变默认对齐数
设置默认对齐数的方法如下:
#include
#pragma pack(2)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(6)//设置默认对齐数为
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//打印输出结果
printf("%d\n", sizeof(struct S1));//8
printf("%d\n", sizeof(struct S2));//12
return 0;
}
为什么要设置默认对齐数?
答:结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。
考察:offsetof的实现,它是C语言本身具有的宏,它需要引头文件
使用它可以计算结构体中某个成员相较于结构体变量起始位置的偏移量(即计算每个成员的偏移量从哪开始)
#include
struct S
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S, c1));
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S,c2));
return 0;
}
#include
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;
}
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2.如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。
什么是位段?
位段的声明和结构是类似的,但有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
比如:A就是一个位段类型
#include
struct A {
//4个字节=32bit
int _a : 2;//2表示_a成员占2个比特位
int _b : 5;//5表示_b成员占5个比特位
int _c : 10;//10表示_c成员占10个比特位
//32个bit被分走了17个,还剩15个,不够分需要再开辟4个字节=32bit
int _d : 30;//30表示_d成员占30个比特位
//注;每个成员最大占32个比特位
};
int main()
{
printf("%d\n", sizeof(struct A));//位段A的大小为:8
return 0;
}
位段会根据实际需求为你分配你需要的内存块的大小
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
下面通过一个例子来探讨一下位段里面的内存数据是如何被使用的?空间是如何开辟的?
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;
}
画图所示:了解位段的基本空间开辟方式及基本使用方式
在vs编译器下,一个字节内部的数据在使用的时候是先使用低比特位的地址再使用高比特位的地址,从右向左使用的。
当一块空间里面剩余的空间的内容不够下一个成员使用的时候,那块空间会被浪费掉。
位段的跨平台问题
不确定因素决定位段能否跨平台;
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位
还是利用,这是不确定的。- 总结:跟结构体相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在。
可以理解为把一个int分成几个成员,每个成员占不同的位段的位,使空间得到很好的优化和节省
枚举顾名思义就是一一列举,即把可能的取值一一列举出来。
比如现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
颜色也可以一一列举。
枚举类型就是一种类型,就像int是整型类型一样,枚举的可能取值的值是整型。
如:定义一个三原色颜色的类型
//enum :枚举关键字;
//Color:枚举的类型
//大括号里面放的是枚举类型的可能取值,也叫枚举常量。它里面的成员是常量,中间用逗号隔开
#include
//声明枚举类型
enum Color
{
red,
green,
blue
};
int main()
{
//创建一个颜色变量为c,它的未来可能取值就是三种颜色当中的一种
//enum Color c = green;//将绿色赋值给c变量
printf("%d\n", red);//0
printf("%d\n", green);//1
printf("%d\n", blue);//2
return 0;
}
这些可能取值都是有数值的,默认从0开始,依次递增1,当然在定义的时候也可以赋初值。
//使用#define 定义常量
#define red 5
#define green 8
#define blue 9
//使用枚举
enum Sex
{
male=5,//赋初值
female=8,//赋初值
secret
};
int main()
{
printf("%d\n", male);//5
printf("%d\n", female);//8
printf("%d\n", secret);//9
printf("%d\n", sizeof(c));//枚举类型的大小就是一个整型大小,4
return 0;
}
我们可以使用 #define 定义常量,为什么非要使用枚举呢?
增加代码的可读性和可维护性。
和#define定义的标识符比较,#define定义的标识符是没有类型的,枚举有类型检查,更加严谨。
防止了命名污染(封装)
比如说:枚举里面定义的符号是属于枚举类型的,是枚举自己的可能取值,不会和其他定义产生冲突;而define的定义符号的是全局的,任意地方都能看到容易产生冲突。便于调试。
define定义的符号它是替换的,在什么阶段替换呢?
使用方便,一次可以定义多个常量。(枚举在定义常量的时候,一下子可以定义多个,使用起来更加方便
要使用define定义的时候,一下次定义多个的时候,要写多句define代码,比较麻烦。)
举例说明为什么枚举会增加代码的可读性
如:用普通方法打印一个计算器菜单
void menu()
{
printf("*************************\n");
printf("**** 1.Add **********\n");
printf("**** 2.Sub **********\n");
printf("**** 3.Mul **********\n");
printf("*****4.Div **********\n");
printf("*****0.exit **********\n");
printf("*************************\n");
}
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;
}
} while (input);
return 0;
}
这个代码的写法要想知道1, 2, 3, 4代表什么还要去跟菜单去对,可读性较差。
如:使用枚举,将数字替换成名字,增加代码的可读性
void menu()
{
printf("*************************\n");
printf("**** 1.Add **********\n");
printf("**** 2.Sub **********\n");
printf("**** 3.Mul **********\n");
printf("*****4.Div **********\n");
printf("*****0.exit **********\n");
printf("*************************\n");
}
enum Option //选项
{
Exit, //0
Add, //1
Sub, //2
Mul, //3
Div //4
};
int main()
{
int input = 0;
do
{
menu();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case Add: //将数字定义成选项,直接就能看出它实现什么功能
break;
case Sub:
break;
case Mul:
break;
case Div:
break;
case Exit:
break;
default:
break;
}
} while (input);
return 0;
}
联合(也叫共用体)
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)
联合体这种类型在定义变量的时候有一个关键字union,用这个关键字可以创建一个联合体类型,跟我们学过的结构体有一个关键字叫struct,枚举有一个关键字叫enum一样。
union Un //这是类型的定义,即创建一个联合体类型 Un
{
//大括号里面放的是联合体的成员,如放两个成员
char c;
int i;
};
union Un un;//创建一个联合体变量un
printf("%d\n", sizeof(un));//计算变量的大小
# include
union Un
{
char c;//成员c是char类型,占1个字节
int i;//成员i是int类型,占4个字节
};
int main()
{
union Un un;//创建一个联合体类型的变量un
printf("%d\n", sizeof(un));//计算变量的大小,打印结果:4
//变量un被创建之后,就会在内存中开辟空间,只要开辟空间,就可以打印它的地址
printf("%p\n", &un);//打印un的地址
printf("%p\n", &(un.c));//un.c表示找到成员c, &(un.c):打印c的地址
printf("%p\n", &(un.i));
//打印un 、c 、i的地址,他们的地址都为008FFBF0
return 0;
}
分析上述代码:
==疑问:==用联合体所创建的变量un它占多大一块内存空间呢?
代码示例:计算变量un的大小
printf("%d\n", sizeof(un));
//根据打印结果可知,它占4个字节的空间
疑问:大括号里面放的是联合体成员,分别是1个字节和4个字节,那为什么联合体变量un的大小为4个字节呢?为什么不是5个?
联合体有一个特点:它不是给它的每一个成员都开辟一块空间,而是成员之间可以共用空间,所以联合体也叫共用体。
不明白概念?那我们用画图方式来分析一下概念
变量un被创建之后,就会在内存中开辟空间,只要开辟空间,就可以打印它的地址,所以我们可以通过打印变量un、成员c和成员i的地址来分析并解决上面的问题同时也可以理解一下概念。
先写代码:打印变量un、成员c和成员i的地址
printf("%p\n", &un);//打印un的地址
printf("%p\n", &(un.c));//un.c表示找到成员c, &(un.c):打印c的地址
printf("%p\n", &(un.i));
//打印结果:
00D6FBB4
00D6FBB4
00D6FBB4
上面已经计算过变量un所占的内存空间为4个字节,画图如下:
根据画图可知,i和c并不是分别拥有自己独立的一块空间,而是i和c共用了同一块字节的空间,因为i和c的第一个字节的空间重合了。
所以上述代码中c成员和i成员共用一块空间,c占一个字节,i占4个字节,所以只需要分配4个字节的空间就够用了,因此大小为4个字节,所以联合体类型的变量un的大小为4个字节。
==总结:==联合的成员是共用同一块内存空间的,这样一个联合变量的大小。至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
联合体初始化代码示例:
# include
union Un //创建一个联合体类型 Un
{
//大括号里面放的是联合体成员
char c;//1个字节
int i;//4个字节
};
int main()
{
union Un un = {9};//给联合体初始化
return 0;
}
通过查看监视可以看到把9放到联合体所占用的4个字节的内存空间,并且c和i共用一块空间
但是如果想给它里面的每个成员放自己独立的值的话可以写成下面这个样子
int main()
{
union Un un = {9};//给联合体初始化
un.i=1000;
un.c=100;
return 0;
}
但是因为i和c共用同一块空间,所以改c的话可能会影响到i,改i的话可能会影响到c,因此**联合体还有另一个特点就是在同一时间它的成员只能使用一个.**即联合体的成员是共用同一块内存空间的,所以给联合体类型的变量初始化的时候只能初始化一个元素。
当多个成员共同享有其中的某一块空间的时候,此时就可以使用联合体。
面试题:判断当前计算机的大小端存储。
一个数字存到内存里面之后,如果它的大小是大于一个字节的,此时它在内存中存储时就会有顺序问题了。
代码示例:
int main()
{
int a = 0x11223344;
return 0;
}
代码中:a是整型,4个字节,它存进去的这个十六进制刚好也是四个字节;
它有两种存储方式,一种是正着存储的,一种是倒着存储的。
画图解释:
把一个数的低位字节的内容放到高地址处,高位字节的内容放到低地址处,这种存储方式叫大端存储。
把一个数的低位字节的内容放到低地址处,高位字节的内容放到高地址处,这种存储方式叫小端存储。
通过观察图来推出如何判断大小端存储:只观察存到内存里面的第一个字节,假设这个数据是0x11223344,它存到内存里面,如果是大端存储模式的话,那第一个字节的内容放的就是0x11,如果是小端存储模式的话,第一个字节的内容放的就是0x44,此时就可以发现他们的区别,同样就可以判断,只要可以取出数据所占内存的第一个字节的内容是什么就可以判断它是属于大端还是小端,如果是0x11就可以判断它是大端的,如果是0x44就可以判断它是小端的。
再举一个简单的数据,看它的在计算机中的存储情况
int a=1;//假设给a变量这里存个1,1的十六进制表示方式是0x00000001
同样也可以判断,只要可以取出数据所占内存的第一个字节的内容是什么就可以判断它是属于大端还是小端,如果是0x00就可以判断它是大端的,如果是0x01就可以判断它是小端。
完整的代码示例:
# include
int main()
{
int a = 1;//假设给a变量这里存个1,1的十六进制表示方式是0x 00 00 00 01
if (*(char*)&a == 1)//如果拿出的这个字节的内容等于1,也就是图中的01的话,就是小端,否则就是大端
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
打印结果为:小端,表示当前计算机的存储模式就是按小端的模式来存储的。
上述代码中:
- &a表示拿出变量a的地址,但是a是整型,它的类型是int,取地址之后类型为int *;
- (char*)&a表示拿出地址之后将它强制类型转换成char*,转换之后就可以从第一个字节开始向后访问字符了;
- (char)&a表示再解引用,因为char* 指针解引用可以访问一个字节的内容,相当于它从第一个字节开始,拿了一个字节的内容出来,此时就可以进行判断.
也可以将上面这个代码封装成函数来写
我们知道一个函数的功能不应该在函数里面自己打印。而应该是返回什么值,表示什么情况,然后在main函数中进行打印,这样可以保证函数的灵活性和独立性.
将上面这个代码封装成函数来写的代码示例:
check_sys()这个函数是用来检测系统是大端还是小端的。
int check_sys()
{
int a = 1;
if (*(char*)&a == 1)//如果拿出的这个字节的内容等于1,也就是图中的01的话,就是小端,否则就是大端
{
return 1;//返回1,表示小端
}
else
{
return 0;//返回0,表示大端
}
}
int main()
{
int ret=check_sys();
//使用ret接收函数的返回值,然后根据ret的值进行打印即可
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
这个代码想要实现的目的是a虽占的是4个字节,但是我只想拿出它的一个字节的内容,然后就可以检测它是大端存储还是小端存储了。
而在联合体(共用体)里面,i占的是4个字节,c占的是1个字节,给i赋上一个值为1,然后再拿出c的值看一下,此时c正好占的是这4个字节的第一个字节的空间,也就是拿出它第一个字节的内容。给i里面放进去的值,其实把c里面的值给覆盖了,那么c里面放的就是第一个字节的内容,只要拿出c的值就好了。这个地方就可以巧妙的使用联合体来写了。
代码示例:
int check_sys()
{
union U //定义一个联合体类型
{
char c;
int i;
}u;//拿这个联合体类型创建一个小u
u.i = 1;//给成员i赋1值,此时就给内存里面放了个1进去,放进去的是1的十六进制,1放在i这个变量的内存里面之后,那第一个字节的内容也会随之改变,而第一个字节的内容放的地址是0还是1就取决于大小端了
return u.c;//u.c就表示拿出你放进去的那个值,如果c里面放的是1,就返回1,如果放的是0就返回0
//而返回1,就是小端;
//返回0,就是大端
}
int main()
{
int ret = check_sys();
//使用ret接收它的返回值,然后根据ret的值进行打印即可
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
所以什么时候可以使用联合体呢?
那就是允许成员共用同一块空间,共用同一块空间也不会影响使用和设计的时候,就可以使用联合体(共用体)。(如上面这个代码)。
其实联合的大小并不是它最大成员的大小,那是什么呢?
如下面这个代码:
union U
{
char a[5];
int i;
};
int main()
{
union U u;
printf("%d\n", sizeof(u));
return 0;
}
疑问1:如果a和i共用同一块空间的话,需要占几个字节呢?是5个吗?
不是,打印结果为8
疑问2:为什么不是5个字节而是8个字节呢?
因为联合体也是存在对齐的
联合体的大小至少是最大成员的大小。
当最大成员不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
上述代码中:
char a[5]表示char类型的数组放了5个元素,即放了5个char类型的变量,所以a占5个字节,而他的对齐数是1
i占4个字节,它的对齐数是4,所以成员里面的最大对齐数就是4,而最大成员的大小是5,但5不是最大对齐数4的倍数,此时就要对齐到最大对齐数的整数倍,所以结果为8(此时浪费了3个字节),所以联合体的大小至少是最大成员的大小,但却不一定就是最大成员的大小。
练习:
union U
{
char a[5];//a占5个字节,他的对齐数是1
short b[2];//b的对齐数是2,它是成员里面的最大对齐数,5不是最大对齐数的倍数,所以结果为6(浪费了1个字节)
};
int main()
{
union U u;
printf("%d\n", sizeof(u));//打印结果:6
return 0;
}