目录
一、结构体
(一)结构体类型的定义
定义一个结构体类型的一般形式:
例如定义一个学生结构体类型:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不要漏
注意:
① 结尾的分号不能漏写;
② 结构与数组的对比:
相同点:都是一些值的集合,元素有一个或者多个;
不同:数组中的元素类型相同,而结构中的元素类型可以是不同的。
(二)结构体变量的创建和初始化
1、结构体变量的创建
结构体变量的创建有三种方法:
(1)定义完结构体后再创建变量
如下演示:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
struct Student s1;//创建结构体变量
return 0;
}
(2)在定义结构体的同时创建变量
如下演示:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s2, s3;
一般形式为:
(3)匿名结构体变量创建
如下演示:
struct
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s4;
一般形式如下:
注意:
匿名结构体(不完全定义)只能在声明时创建变量;
两个结构体匿名的情况下,即使成员完全相等,编译器也会把这两个结构体当作是完全不同的;
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
2、结构体变量的初始化
如下所示:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
(三)结构成员的引用
前面定义了结构体类型,也定义了某些变量为该类型的变量,接下来学习结构成员的引用。
① 不能将结构体变量作为一个整体进行引用,如上面定义的s1、s2,都是结构体变量但不能拿来直接用,只能对结构体变量中的各个成员分别引用。
引用的方式为:
例如,s.age 表示 s 变量中的 age 成员,可以对该成员直接赋值。看如下代码
s.age = 21;//将21赋给s变量中的成员age
上面的 “ . ” 叫作结构体成员运算符,它的优先级非常高,与圆括号( )平级。
② 若结构体中嵌套结构体,要使用低级的结构体成员,就要使用 “ . ” 结构体成员运算符一级一级地向下找结构体变量。
③ 结构体中的成员又称为成员变量,可以直接接看作普通变量,像普通变量一样进行备种运算,如下代码:
s.age = s.age + s2.age;
④ 因为成员变量也相当于普通变量,所以它们也是有地址的,如下代码:
int* p = &s.age;
⑤ 结构体指针中,使用“ ->” 对结构体成员进行引用
(四)结构的自引用
要实现结构的自引用,就要在成员中包含同结构类型的指针,如下代码:
struct Node
{
int data;
struct Node* next;
};
(五)结构体内存对齐
在计算结构体大小(占多少字节)的时候,就会涉及到结构体内存对齐的问题
1、对齐规则
开始:
① 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处;
中间:
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
③ 对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值;
-VS中默认对齐数为 8;
- Linux中gcc没有默认对齐数,对齐数就是成员自身大小。
④ 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
结尾:
⑤ 结构体总大小为最大对齐数(结构体中每个成员变量通过与默认对齐数比较后,都有⼀个对齐数,选出所有对齐数中最大的)的整数倍;
2、练习
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
结果如下:
3、为什么存在对齐
大部分的参考资料都是这样说的:
① 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
② 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。
假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
4、优化
既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起:
//例如:
struct S1
{
char c1;
int i;
char c2;
};//大小为12
struct S2
{
char c1;
char c2;
int i;
};//大小为8
5、修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
结果为:6
(六)结构体传参
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;
}
以上函数,print2更好,原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,需要传结构体的地址
(七)结构体实现位段
1、位段的定义
位段的定义和结构是类似的,有两个不同:
① 位段成员必须是 int、unsigned int 或 signed int,在C99中位段成员的类型也可以选择其他类型;
② 位段的成员名后边有一个冒号和一个数字。
例如:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段类型。
注意:
位段中的位,指的是二进制位(bit位);
成员后面的数字是指可以使用多少二进制位(bit位)。
2、位段的内存分配
① 位段的成员可以是int、unsigned int、 signed int或者是char等类型;
② 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的;
③ 位段涉及很多不确定因素,位段不是跨平台的,注重可移植的程序应该避免使用位段。
④ 不确定因素:
(①)给定了空间后,位段在空间内部是从左向右使用,还是从右向左使用,这个不确定;
(②)当剩下的空间不足以放下下一个成员的时候,空间是浪费还是使用,不确定。
例子:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
上面代码空间开辟过程:
① 假设从右向左使用空间,当空间不够用时,直接开辟新空间;
② 因为是按照需要开辟空间的,首先先开辟 8 个比特位,空间从右向左的3位bit位存放s.a二进制截取的三位,然后存放s.b二进制截取的4位,剩下1位不够放s.c,直接开辟新的8个比特位空间,从右向左存放s.c二进制截取的5位,剩下的3位又不够存放s.d,所以再开辟8个比特位的空间,从右向左存放s.d二进制截取的4位;
③ 上面过程中总共开辟了3次8个比特位的空间,所以该位段的内存大小为3个字节。
过程图示如下:
3、位段的跨平台问题
① int 位段被当成有符号数还是无符号数是不确定的。
② 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,若写成27,代码应用在16位机器会出问题)
③ 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
④ 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4、位段的使用
在网络协议中,经常使用位段。
5、位段使用的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的;
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
如下所示:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
二、联合
(一)联合体类型的定义
像结构体⼀样,联合体也是由一个或者多个成员构成,这些成员可以不同的类型。
但是编译器只为最大的成员分配足够的内存空间,联合体的特点是所有成员共用同一块内存空间,所以联合体也叫:共用体。
给联合体其中一个成员赋值,其他成员的值也跟着变化。
如下代码演示:
#include <stdio.h>
//联合类型的定义
union Un
{
char c;
int i;
};
int main()
{
//联合变量的创建
union Un un = { 0 };
//计算连个变量的大小
printf("%d\n", sizeof(un));
return 0;
}
结果为:4。
(二)联合体的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
联合体中的成员,同一时间只能使用一个,因为他们公用一个空间。
代码一:
#include <stdio.h> //联合类型的定义 union Un { char c; int i; }; int main() { //联合变量的创建 union Un un = { 0 }; printf("%p\n", &(un.i)); printf("%p\n", &(un.c)); printf("%p\n", &un); return 0; }
输出的结果为:
代码二:
#include <stdio.h> //联合类型的定义 union Un { char c; int i; }; int main() { //联合变量的创建 union Un un = { 0 }; un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i); return 0; }
输出结果为:
分析:代码1输出的三个地址一模一样,代码2的输出,我们发现将 i 的第4个字节的内容修改为55了,内存分布图如下:
再对比一下相同成员的结构体和联合体的内存布局情况:
结构体:
struct S { char c; int i; }; struct S s = {0};
联合体:
union Un { char c; int i; }; union Un un = { 0 };
内存布局图如下:左边是结构体,右边是联合体。
(三)联合体大小的计算
① 联合的大小至少是最大成员的大小。
② 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
如下代码演示:
#include <stdio.h>
union Un1
{
char c[5]; //大小为5,对齐数为1
int i; //大小为4,对齐数为4
};//最大字节为5,最大对齐数为4,所以应该扩大到对齐数的倍数,最终大小为8
union Un2
{
short c[7];//大小为14,对齐数为2
int i;//大小为4,对齐数为4
};//最大字节为14,最大对齐数为4,所以应该扩大到对齐数的倍数,最终大小为16
int main()
{
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
结果为:8 和 16。
(四)联合体的使用
1、使用例之商品展示
使用联合体是可以节省空间的,例如:
要搞一个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯子、衬衫。
每一种商品都有:库存量、价格、商品类型和商品类型相关的其他信息,相关信息如下:
图书:书名、作者、页数
杯子:设计
衬衫:设计、可选颜色、可选尺寸
我们可以直接写出结构体来包括所有内容:
struct gift_list
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//页数
char design[30];//设计
int colors;//颜色
int sizes;//尺寸
};
上述的结构其实设计的很简单,用起来也方便,但是结构的设计中包含了所有礼品的各种属性,这样使得结构体的大小就会偏大,比较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息 是常用的。比如:商品是图书,就不需要design、colors、sizes。
所以我们就可以把公共属性单独写出来,剩余属于各种商品本身的属性使用联合体起来,这样就可以减少所需的内存空间,⼀定程度上节省了内存。
如下代码所示:
struct gift_list
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
union
{
struct
{
char title[20];//书名
char author[20];//作者
int num_pages;//页数
}book;
struct
{
char design[30];//设计
}mug;
struct
{
char design[30];//设计
int colors;//颜色
int sizes;//尺寸
}shirt;
}item;
};
2、使用例之判断机器大小端
如下代码可判断当前机器的大小端
int main()
{
union
{
int i;
char a;
}un;
un.i = 1;
printf("%d", un.a);
return 0;
}
如果打印出1,则是小端机器;如果打印出0,则是大端机器。
三、枚举
(一)枚举类型的定义
枚举的关键字是:enum,是单词Enumerate的缩写,意思为列举、排列说明的意思。
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
① 一周的星期一到星期日是有限的7天,可以一一列举
② 性别有:男、女、保密,也可以一一列举
③ 月份有12个月,也可以一一列举
④ 三原色,也是可以意义列举
入下代码所示:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum color//颜色
{
RED,
GREEN,
BLUE
};
上面定义的enum Day、enum Sex、enum color都是枚举类型。
{ } 中的内容是枚举类型的可能取值,也叫枚举常量,枚举常量之间使用逗号隔开,最后一个枚举常量不加符号结尾。
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值,如下代码所示:
enum color//颜色
{
RED = 2,
GREEN = 6,
BLUE = 10
};
(二)枚举类型的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
① 枚举可以让文字对应数字,增加代码的可读性和可维护性。
② 和 #define 定义的标识符比较,枚举有类型检查,更加严谨。
③ 便于调试,预处理阶段会删除 #define 定义的符号。
④ 使用方便,一次可以定义多个常量。
⑤ 枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。
(三)枚举类型的使用
使用枚举常量给枚举变量赋值。
如下代码所示:
enum color//颜色
{
RED = 2,
GREEN = 6,
BLUE = 10
};
enum color clr = RED;//使用枚举常量给枚举变量赋值
在C语言中,可以拿整数给枚举变量赋值,但是在c++是不行的,c++的类型检测比较严格。
以上内容仅供分享,若有错误,请多指正