目录
我们以前见到的 char,bool,short,int,long long,float,double,long double都是内置类型。
自定义类型,从名字上来看,也就是我们自己创造定义的类型包括数组类型,结构体类型(struct),枚举类型(enum),联合体类型(union)
结构体
概念
结构是⼀些 值的集合 ,这些值称为 成员变量 。
结构的每个成员 可以是不同类型的变量 。
结构体声明(注意:结构体最后有一个分号)
cpp
struct tag
{
member-list;
//成员变量列表
}variable-list;
//结构体变量列表
例:描述一个学生
cpp
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[20];//学号
}st1,st2;//结构体变量列表
结构体变量的创建和初始化
方法一:直接在结构体后面创建结构体变量和初始化
cpp
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
}st1 = { "张三",18,"男","1234567" },st2 = { "李丽",24,"女","1234568" };//结构体变量列表
#include<stdio.h>
int main()
{
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
方法二:在需要的函数中进行结构体变量的创建和初始化
cpp
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
};//分号
#include<stdio.h>
int main()
{
struct Student st1 = { "张三",18,"男","1234567" };
struct Student st2 = { "李丽",24,"女","1234568" };
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
当然,在初始化的时候我们也可以按照自己的顺序给结构体变量进行初始化
cpp
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
}st1,st2;//分号
#include<stdio.h>
int main()
{
struct Student st1 = { .name="张三",.sex="男",.number="1234567",.age=18 };
//也可以按照自己的顺序进行初始化
struct Student st2 = { "李丽",24,"女","1234568" };
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
结构体的自引用
我们说结构体里面可以是不同类型的变量,那么结构体里面可不可以包含一个类型为该结构本身的成员呢?
答案是不可以的,比如我们定义一个链表的结点。
cpp
struct Node
{
int data;
struct Node next;
};
这样子进行定义的话,⼀个结构体中再包含⼀个同类型的结构体变量,那么结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。
正确方式:
cpp
struct Node
{
int data;
struct Node* next;
//next保存下一个结点的地址
};
我们也可以进行一定的优化(使用重命名的方式)
cpp
typedef struct Node
{
int data;
struct Node* next;
//虽然后面结构体重命名为Node,但是到这里编译器还没有识别,依然写成
//struct Node的形式
}Node;
结构体的内存对齐
计算结构体大小,就需要知道结构体内存对齐的规则
- 结构体的 第⼀个成员对⻬到和结构体变量起始位置偏移量为0 的地址处
- 其他成员变量要对齐到 对齐数整数倍 的地址处
对⻬数 = 编译器默认对⻬数 与该 成员变量⼤⼩ 的 较⼩值
VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为 最⼤对⻬数 (结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍 。
- 如果嵌套了结构体的情况, 嵌套的结构体成员 对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是 所有最⼤对⻬数 (含嵌套结构体中成员的对⻬数)的 整数倍 。
看了这规则,不如来几个题练练手
cpp
#include<stdio.h>
int main()
{
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%zd\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%zd\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%zd\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%zd\n", sizeof(struct S4));
return 0;
}
请在评论区留下你的答案,我们马上揭晓谜底~
我们通过下面的来进行理解答案
cpp
#include<stdio.h>
int main()
{
//练习1
struct S1
{
//vs默认对齐数为8
char c1;//1 8 1
int i; //4 8 要对齐到该成员变量int类型字节大小4的整数倍------4+4
char c2;//1 8 8+1------9
//结构体总大小为成员变量最大对齐数4的整数倍
//12
};
printf("%zd\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;//1 8 1
char c2;//1 8 1+1------2
int i; //4 8 2+4
//结构体总大小为成员变量最大对齐数4的整数倍
//8
};
printf("%zd\n", sizeof(struct S2));
//练习3
struct S3
{
double d;//8 8 8
char c; //1 8 8+1------9
int i; //4 8 12+4------16
//结构体总大小为成员变量最大对齐数8的整数倍
//16满足
};
printf("%zd\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1; //1 8 1
struct S3 s3; //16 8 8+16------24
//结构体S3大小为16,最大对齐数为8
double d; //8 8 24+8------32
//结构体总大小为成员变量最大对齐数8的整数倍
//32满足
};
printf("%zd\n", sizeof(struct S4));
return 0;
}
内存对齐存在的原因
那么为什么会有结构体对齐呢?
平台原因 (移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据 的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因
数据结构(尤其是栈)应该尽可能地 在⾃然边界上对⻬ ,原因在于, 为了访问未对⻬的内存,处理器需要作两次内存访问 ;⽽ 对⻬的内存访问仅需要⼀次访问 。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以用 ⼀个内存操作来读或者写值。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总结:结构体的内存对⻬是拿 空间来换取时间 的做法,提高程序的运行效率
合理设计结构体
如果在设计结构体的时候,我们既要 满⾜对⻬ ,⼜要 节省空间 ,我们可以怎么做呢?
方法一
让占⽤空间⼩的成员尽量集中在⼀起
比如下面这两个结构体:
cpp
#include<stdio.h>
int main()
{
struct s1
{
char c1;
char c2;
int n;
};
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
struct s2
{
char c1;
int n;
char c2;
};
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
return 0;
}
我们可以看见虽然这两个结构体的包含的成员变量相同,但是它们的结构体大小是不一样的,这是因为在对齐结构体的时候它们的对齐位置是不一样的。
所以定义一个结构体的时候让占⽤空间⼩的成员尽量集中在⼀起,就可以节省空间。
方法二
修改默认对⻬数
我们知道VS的默认对齐数是8,事实上,结构体在对⻬⽅式不合适的时候,我们可以修改这个默认对齐数。
使用 #pragma 这个预处理指令,改变编译器的默认对⻬数
cpp
#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
char c1;
char c2;
int n;
};
struct s2
{
char c1;
int n;
char c2;
};
void test1()
{
printf("test1:\n");
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
}
int main()
{
test1();
return 0;
}
如果想让它恢复成原来编译器的默认对齐数只需要加上
#pragma pack()//恢复到编译器默认对齐数
cpp
#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
char c1;
char c2;
int n;
};
#pragma pack()//恢复到编译器默认对齐数
struct s2
{
char c1;
int n;
char c2;
};
void test1()
{
printf("test1:\n");
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
}
int main()
{
test1();
return 0;
}
结构体传参
在前面学习函数的时候我们知道有传值传参和传址传参
cpp
#include<stdio.h>
struct S
{
int data[10];
int num;
};
struct S s = { {1,2,3,4}, 100 };
//结构体传参
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;
}
在上面的代码中,无论传参是传结构体还是传结构体的地址,都达到了我们想要的效果。
那么结构体传参哪一个更好呢?
答案是结构体地址传参
原因
1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2.如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,
会导致 性能的下降 。
结构体实现位段
什么是位段
位段的声明和结构是类似的,但是存在两个不同:
- 位段的成员必须是 int 、 unsigned int 或 signed int (结构体可以有其他类型)
在C99中位段成员的类型也可以 选择其他类型。
- 位段的成员名后边有⼀个冒号和⼀个数字(结构体成员名后面没有内容)
例如:
cpp
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:20;
};
A就是⼀个位段类型,那么位段A所占内存的⼤⼩是多少呢?
位段内存大小为8,这是为什么呢?
我们首先需要知道位段的内存分配是什么样子的。
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
- 位段的空间上是 按照需要 以4个字节( int )或者1个字节( char )的⽅式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
我们来看看一个例子:
cpp
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("test1:%zd\n", sizeof(s));
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
}
我们在VS编译器上进行验证,我们猜想首先位段成员类型为char,按照需要以一个字节(8个比特位)空间来开辟,a要占用2个比特位,b占用4个比特位,c占用5个比特位,d占用4个比特位,a、b一起占用了6个比特位(剩余两个比特位,不够存储c,浪费掉这两个比特位,另外开辟一个字节),c占用五个字节,剩余三个比特位,不够存储d,浪费掉这三个比特位,另外开辟一个字节。所以位段A的字节大小为3,我们一起来验证一下。
所以说明在VS编译器上,剩余的比特位是浪费掉的,如果没有浪费那么应该是两个字节(16个比特位)。
后面的代码再对a,b,c,d所在的内存放入数据。
通过调试我们发现是正确的。
所以这个代码可以得出:
在VS编译器上:
1.char------一个字节一个字节进行内存空间开辟
2.一个字节内部从右向左使用
3.剩余的比特位不够下一个成员使用时,浪费掉,开辟新的内存进行存放
所以在最开始的代码中, 位段的空间上按4个字节(32个比特位)进行开辟,_a要占用2个比特位,_b占用5个比特位,_c占用10个比特位,_d占用20个比特位,_a和_b和_c一起占用了17个比特位,剩余15个比特位不够_d使用,另外开辟四个字节进行存储,所以字节大小为8.
位段的跨平台问题
- int 位段被当成有符号数还是⽆符号数是不确定的。
- 位段中最⼤位的数⽬不能确定
(16位机器最⼤16,32位机器最⼤32,如果写成27,在16位机器会 出问题)- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这也是不确定的。
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在,我们应该根据实际情况进行使用。
注意
位段一般是 ⼏个成员共有同⼀个字节 ,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。
内存中 每个字节分配⼀个地址 ,⼀个字节内部的 bit位是没有地址的 。
所以不能对位段的成员使⽤&操作符,这样就 不能使⽤scanf直接给位段的成员输⼊值 ,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
cpp
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 20;
};
int main()
{
struct A sa = { 0 };
//scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
联合体
概念
像结构体⼀样,联合体也是 由⼀个或者多个成员构成 ,这些成员可以不同的类型。
编译器 只为最⼤的成员分配⾜够的内存空间 。
联合体的特点是 所有成员共⽤同⼀块内存空间 ,所 以联合体也叫:共⽤体。
如果 给联合体其中⼀个成员赋值,其他成员的值也会跟着变化。
cpp
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
//计算联合体变量的大小
printf("%d\n", sizeof(un));
return 0;
}
大小为4个字节,这是为什么呢?
我们前面提到联合体的特点是所有成员共⽤同⼀块内存空间,编译器只会为最大的联合体成员分配足够的空间。
我们来看看下面的代码
cpp
#include <stdio.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//输出的结果是什么?
printf("%zd\n", sizeof(union Un1));
printf("%zd\n", sizeof(union Un2));
return 0;
}
上面进行了分析和解释,所以
⼀个联合变量的大小,⾄少是最⼤成员的大小(因为联合至少得有能⼒保存最⼤的那个成员)
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
验证
cpp
#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;
}
我们可以发现,这三个地址是一样的,这也就验证了联合体成员共用了一块内存空间。
我们来看看下面的代码:
cpp
#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;
}
我们发现将i的第4个字节的内容修改为55了,这也就是因为联合体成员共用一个内存空间。
优点
使用联合体有什么好处呢?
显而易见,使⽤联合体是可以 节省空间 的,因为联合体成员是共用一块内存空间的,联合体会至少开辟最大成员的空间大小,而结构体会给每一个成员开辟相应的空间。
比如我们需要保存 三种商品:图书、杯⼦、衬衫的信息,每⼀种商品都有:库存量、价格、商品类型和商品类型这些共同信息,每一个商品还有一些特殊信息【 图书:书名、作者、⻚数 】 【杯⼦:设计 】【 衬衫:设计、可选颜⾊、可选尺⼨】
我们可以写出下面的结构体
cpp
struct goods
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//页数
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
};
结构体里面包括了所有的属性,需要哪一个再去进行初始化以及使用,但是这样就会导致结构体偏大,造成空间的浪费。我们可以一起使用结构体和联合体来进行改造,比如:
cpp
struct goods
{
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;
};
联合体就会为占内存空间最大的结构体分配足够的空间,这也就节省了内存空间。
小应用
写⼀个程序,判断当前机器是⼤端?还是⼩端?
什么是大小端?
超过⼀个字节的数据 在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分
为 ⼤端字节序存储 和 ⼩端字节序存储。不同的编译器存储顺序不一样。
⼤端(存储)模式:
数据的 低位字节 内容保存在 内存的⾼地址 处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:
数据的 低位字节 内容保存在 内存的低地址 处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
用联合体判断大小端
cpp
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;//返回1是⼩端,返回0是⼤端
}
int main()
{
if (check_sys)//1为真
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
编译器取地址会从低地址开始取,这里返回了1,也就是低字节的内容存放在低地址处,说明VS为小端机器。
枚举
概念
枚举就是⼀⼀列举, 把可能的取值⼀⼀列举 。
比如,我们实际生活中一周有七天,一年有12个月,这些都是可以一一列举的,我们用枚举来表示,就是
cpp
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
//中间用逗号隔开
};//末尾有分号!!!
enum Month
{
Jan,
Feb,
Mar,
Apr,
May,
Jun,
Jul,
Aug,
Sept,
Oct,
Nov,
Dec
};
以上定义的 enum Day , enum Month 都是枚举类型。
{ }中的内容是枚举类型的可能取值,也就是枚举常量 。
这些可能取值都是有值的, 默认从0开始,依次递增1 ,当然在声明枚举类型的时候也可以赋初值。
例:
cpp
#include<stdio.h>
enum Color//颜⾊
{
RED,
GREEN = 3,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
//第一个枚举常量默认为0,也可以初始化,没有初始化的枚举常量是上一个枚举常量的值加一
优点
我们可以使⽤ #define 定义常量,为什么要使⽤枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
- 便于调试,预处理阶段会删除 #define 定义的符号
- 使⽤⽅便,⼀次可以定义多个常量
- 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
使用注意
在C语⾔中是可以 拿整数给枚举变量赋值 的,但是在C++是不⾏的,C++的类型检查⽐较严格。
cpp
enum Color//颜⾊
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值