结构体回顾
结构体
自定义的类型:结构体、联合体、枚举
结构是一些值的集合,这些值成为成员变量,结构的每个成员可以是不同类型的变量
//描述一本书:书名、作者、定价、书号
//结构体类型---类似于整型、浮点型
struct Book
{
char book_name[20];//书名
char author[20];//作者
float prince;//价格
char id[18];//书号
}; b4, b5, b6;//结构体变量’
//这里的结构体变量个下面的本质是一样的
//但是这里的是全局变量,而下面的是局部变量
int main()
{
//结构体需要用大括号进行初始化,因为结构体里面不只一个值
//初始化的时候我们是根据成员进行初始化的
//下面的初始化是按照成员顺序进行初始化的
struct Book b1 = {"鹏哥c语言","鹏哥",38.8f,"PG20240520"};
//如果不按照成员顺序进行初始化可以这么写
struct Book b2 = {.id="DG20240520",.book_name="蛋哥Linux",.author="蛋哥",.prince=55.5f};
//两个操作符. ->
printf("%s %s %f %s\n", b1.book_name, b1.author, b1.prince, b1.id);
printf("%s %s %f %s\n", b2.book_name, b2.author, b2.prince, b2.id);
return 0;
}
//鹏哥c语言 鹏哥 38.799999 PG20240520
//蛋哥Linux 蛋哥 55.500000 DG20240520
//为什么这个数据会和我们设置的有差异呢?
//
//因为浮点数在内存中 有可能是不能进行精确保存的
/*
判断浮点数是否相等的方法
if(fabs(f-3.45)<0.00000001)
{
printf("相等");
}
else
{
printf("不相等");
}
*/
结构体的声明
在声明结构体的时候,可以不完全的声明---把名字省略掉
struct //直接把结构体名字去掉
{
char c;
int i;
double d;
}s1;//直接在这里进行结构体变量的创建
//匿名结构类型创建变量
//创建变量只能用一次
int main()
{
//struct S s2;这种创建的方式就不行了
return 0;
}
struct //直接把结构体名字去掉
{
char c;
int i;
double d;
}* ps;//匿名结构类型的指针类型
//匿名结构体类型+ *就是匿名结构体类型指针
//这里的ps就是指针变量
int main()
{
ps = &s1;
return 0;
}
//两个类型虽然成员是一样的,但是编译器会认为这两个匿名结构体的类型是不一样的
//所以结构体指针也是不一样的
//编译器会认为一种匿名结构体类型是一种类型,而另一种就是另一种类型
//反正是没有相同的匿名结构体类型的
//我们只有在仅仅只使用一次的情况下才会使用匿名结构体类型
//编译器会把两个匿名结构体类型当成两个不同类型的匿名结构体类型的
数据结构---数据结构其实是数据在内存中的组织结构
//struct Node
//{
// int data;//存放数据
// struct Node next;//访问下一个节点---类似递归
// //但这中写法是错误的
//};
//结构体能自己找到同类型的下个节点,实现自引用,但是上面的方式是不对的
//那么在一个结构体中寻找下一个同类型的结构体的变量的方法
struct Node
{
int data;//存放数据----数据域
struct Node* next;//---指针域
//next存放的是下个节点的地址
//这样我们不仅能存储这个节点的数据,还能存储下个节点的地址
//这样我们就能使结构体能自己找到同类型的下个节点,实现自引用
//存放这个节点的数据,还能找到下个节点的数据,那么我们添加一个结构体指针就行了
};
int main()
{
return 0;
}
/*这里的是一个结构体类型的
struct Node
{
int data;//存放数据----数据域
struct Node* next;//---指针域
};*/
//那么我们对这个结构体类型进行重命名Node
typedef struct Node
{
int data;//存放数据----数据域
struct Node* next;//---指针域
//错误写法:Node* next;//---指针域
//我们是先创建这个结构体类型,再进行重名名的,所以在顺序上面,这个代码就是错的
//重命名是后来的,所以不能将里面的代码进行改变
}Node;
int main()
{
return 0;
}
2.结构体内存对齐
我们在涉及计算结构体的大小的问题的时候,就会面临结构体内存对其的问题了
offsetof-----宏--头文件stddef.h
计算结构体成员相较于结构体变量起始位置的偏移量
offsetof(type,member)
offsetof计算的是每个成员在内存中相较于起始位置的偏移量
//struct S1
//{
// char c1;
// char c2;
// int n;
//};
//struct S2
//{
// //顺序不一样
// char c1;
// int n;
// char c2;
//};
//int main()
//{
// printf("%zd\n", sizeof(struct S1));//占8个字节
// printf("%zd\n", sizeof(struct S2));//占12个字节
// return 0;
//}
//为什么输出的大小一个是8一个是12呢?
//那么这里就涉及到了结构体的对齐问题了
//结构体的成员在内存中是存在对齐现象的
//结构体内对齐
//offsetof-----宏
//计算结构体成员相较于结构体变量起始位置的偏移量
struct S1
{
char c1;
char c2;
int n;
};
struct S2
{
//顺序不一样
char c1;
int n;
char c2;
};
int main()
{
//printf("%zd\n", sizeof(struct S1));//占8个字节
//printf("%zd\n", sizeof(struct S2));//占12个字节
struct S1 s1 = { 0 };
//计算偏移量
/*printf("%zd\n", offsetof(struct S1, c1));//0
printf("%zd\n", offsetof(struct S1, c2));//1
printf("%zd\n", offsetof(struct S1, n));*///4
printf("%zd\n", offsetof(struct S2, c1));//0
printf("%zd\n", offsetof(struct S2, n));//4
printf("%zd\n", offsetof(struct S2, c2));//8
return 0;
}
//第一个字节给c1,第二个字节给c2,后面的两个字节空着了,
// 因为n是从第4个字节开始的,占了4个字节
//offsetof计算的是每个成员在内存中相较于起始位置的偏移量
//画图:
/*
第一行是c1占的字节
第二行是c2占的字节
第二三行是空的
n是从第四行到第七行的
总共算下来就是8个字节
*/
//printf("%zd\n", offsetof(struct S2, c1));//0
//printf("%zd\n", offsetof(struct S2, n));//4
//printf("%zd\n", offsetof(struct S2, c2));//8
//对s2进行计算得到的就是0 4 8
/*
第0行是c1
1 2 3行是空的
4 5 6 7放的是n,因为n占4个字节,并且李起始点有4个字节
第8行方的是c2
*/
//但是为什么之前计算的是S2占12个字节呢?
//不管是S1还是S2,他们的成员在内存中不是一个放完就放另一个的
//其实是存在对齐现象的
结构体对其的规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他的成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
--vs中,默认对其的值是8
--Linux中qcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数。(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)整数倍
4.如果嵌套了结构体的情况。嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整数大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
//那么结构体是如何对齐的呢?
//结构体成员在内存中到底是怎么存放对齐的?
//那么我们就了解一下结构体对其的规则
//struct S1
//{
// char c1;
// char c2;
// int n;
//};
//
//int main()
//{
// struct S1 s1;
//
// return 0;
//}
//1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
/*
2.对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
--vs中,默认对其的值是8
*/
//在这个代码中,c1就是第一个成员,占一个字节,那么c1就占了内存中第0行的空间位置
//c2是1个字节,小于vs规定的对齐数8,所以c2要对齐在1的倍数的位置
//那么刚好1是1的倍数,那么c2就占了第1行的空间位置
//因为除了第一个成员,剩下的都是其他成员
//因为n的大小是4个字节,vs默认对齐数是8,4<8,那么c2的对齐数就是4
//所以n此时此刻要对齐在4的倍数上面,就是偏移量是4的倍数就行了
//所以我们是从第4行开始占的,占4个字节
//中间的2 3行是被空着的,因为这两个字节我们用不上了
/*
现在我们将所有成员都放进内存去了,占了8个字节
但是结构体的大小还不能因此判断
我们还要根据第三条进行判断
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,
所有对齐数中最大的)的整数倍
这三个成员的默对齐数是最大对齐数是4,
那么在这里结构体的大小就是4的倍数,因为这里刚好8是4的倍数,所以我们直接进行判断结构体的大小是8个字节
*/
//对s2进行讲解
struct S2
{
char c1;
int n;
char c2;
};
int main()
{
struct S2 s2;
return 0;
}
/*
根据规则
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
因为c1占一个字节,那么c1就放在第一行的位置
对于n,n的大小是4个字节,vs上默认对齐数是8,因为4<8
那么取这两个数的较小数就是4作为n的对齐数,所以n要对齐4的倍数
那么我们就只能从第四行开始,因为是4个字节大小,所以从第4行到第7行都是n的范围
在这里我们又发现中间的1 2 3行是空的,说明我们又浪费三个字节
对于c2,因为c2的字节大小是1,vs默认对齐数是8,1<8,所以c2的对齐数是1
所以c2找到1的倍数就行了
刚好第8行是1的倍数,那么c2就将第8行占了
到目前为止S2占了9个字节了
但是真的是9个字节吗?
我们还得根据后面的规则进行判断
在S2中,最大的对齐数是4,那么结构体的总大小是4的倍数
但是9 10 11行都不是4的倍数,所以我们只能取第12行,
所以我们中间又浪费了3个字节的大小
所以最终S2结构体的大小是12
*/
struct S3
{
double d;//0-7
char c;//8
int i;//10-12
//最大对齐数是8,那么最后的位置固定在8的倍数上面,就是16
};
int main()
{
printf("%d\n", sizeof(struct S3));//16
return 0;
}
//因为double类型是8个字节,根据结构体对其的规则,构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
//那么0-就是d占了
/*
对于c来说,c大小1个字节,vs默认对齐数是8,因为1<8,所以c的对齐数是1
那么只要是1的倍数就行了
所以c就将第8行占了
对于i,i是4个字节,vs默认对齐数是8,4<8所以i的对齐数是4的倍数
所以 9 10 11这三块字节浪费了,直接从12开始,因为是4个字节大小,所以i的空间就是12-15行
因为这三个数据的最大对齐数是8,所以最后的结构体的大小要取8的倍数,,那么我们只能取16了
所以最后这个结构体大小就是16
*/
//练习4-结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s4;
printf("%d\n", sizeof(struct S4));//32
return 0;
}
/*
这种就是结构体嵌套了一种其他类型的结构体变量
我们在之前就将S3的大小算出来了,是16个字节的大小
S4成员c1因为是第一个成员,肯定是放在偏移量为0的地址处
因为c1类型是char类型,一个字节大小,所以第0行被c1占了
c1放完我们放s3
因为我们计算出的s3是16个字节,但是这个16个字节从哪里开始放呢?
根据规则4
如果嵌套了结构体的情况。嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,
结构体的整数大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
所以s3要对齐到字节的结构体内最大对齐数8的整数倍处
所以s3对齐到8的倍数就行了,因为第一个8的倍数是8
所以s3是从第8行开始对齐的,因为大小是16个字节
所以s3占了8-23行
所以中间的1-7行的7个字节被浪费了
对于d来说,因为类型是double,大小是8个字节,vs默认对齐数是8,
那么d的对齐数是8,所以d要对齐8的倍数位
因为24是8的对齐数,所以d从24行开始对齐,d的大小是8个字节
所以24-31行是d占了
现在我们已经占了32个字节了,从0-31行
那么32是不是我们最终的大小呢?
在规则4中我们还说到
结构体的整数大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
因为这哥结构体所有成员中对齐数里面最大对齐数是8
并且我们结构的大小最终还是要取决最大对齐数的倍数,
因为最大对齐数的倍数是8
所以最终这个结构体的大小我们取32
*/
那么为什么会存在内存对齐的问题呢
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。
- 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
内存对齐可以提升读取的效率
所以内存对齐还是很有必要的
那么在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起
//那我们只能眼睁睁的看着字节被浪费吗?
/*
我们是否有办法使内存少浪费一些
那么在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起
*/
/*这个时候我们就可以拿出之前的S1和S2了
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;//对齐数是1
char c2;//对齐数是1
int i;
};
这两个类型成员是一样的,只是内部成员顺序不同
S1占12个字节
S2占8个字节
不难发现,我们将占用空间较小的成员集中放在一起,
原本要浪费的空间就被利用起来了
*/
修改默认对齐数
pragma这个预处理指令,可以改变编译器的默认对齐数
//vs上默认对齐数是8
#pragma pack(1)//将默认对齐数设置为1
struct S
{
char c1;
int n;
char c2;
};
#pragma pack()//恢复默认对齐数,恢复默认对齐数为8
//如果想恢复默认对齐数,我们pack()括号内可以直接不写
int main()
{
//printf("%zd\n", sizeof(struct S));//12个字节---默认对齐数为8的时候
printf("%zd\n", sizeof(struct S));//6个字节-=--默认对齐数是1的时候
return 0;
}
3.结构体传参
//结构体传参
struct S//我们必须将这个结构体类型的创建放在前面,不然会报错的
{
int arr[1000];
int n;
char ch;
};
void print1(struct S tmp)//传过来的是一个结构体变量,那么我们就创建一个结构体变量进行接收
{
for (int i = 0; i < 10; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("\n");
printf("n=%d\n", tmp.n);
printf("ch=%c\n", tmp.ch);
}
void print2(const struct S* ps)//传过来的是结构体变量的地址,那么我们就创建一个结构体指针进行接收
{//那么ps就指向了s
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
printf("n=%d\n", ps->n);
printf("ch=%c\n",ps->ch );
}
int main()
{
struct S s = { {1,2,3,4,5,6,7,8,9,10},10,'w' };//创建结构体变量s并进行初始化
//利用函数将结构体内数据打印出来
print1(s);//直接将结构体变量s传过去-----传值调用
print2(&s);//直接将结构体变量s的地址传过去
return 0;
}
//分析:那种更好一些
/*
在print1中,我们传过去的是结构体变量,
相当与传值调用
在调用的时候,我们还要创建一个结构体变量tmp用来接收传过来的数据
在这个调用的时候,我们还要额外的空间进行结构体变量的创建
假如传过来的变量占用空间很大,我们又要创建一个一模一样的空间进行存储
我认为这么很浪费空间,还很消耗时间
*/
/*
但是我们利用print2传的仅仅只是地址,地址的大小是4个字节或者8个字节
我们在创建形参的时候仅仅只需要创建一个指针ps进行接收,
指针ps就能找到这个结构体变量内的数据
这样也不需要创建结构体指针变量进行数据的拷贝,我们直接利用地址进行数据的调用和访问
*/
//传值能做到的,传地址一定能做到
//传地址能做到的,传值不一定能做到
//所以我们在结构体传参的时候,要传结构体的地址
//如果不想因为地址传过去的原因,地址指向的数据被改变了,我们直接加上const
在我们进行结构体传参的时候,我们传地址就行了
4.结构体实现位段
结构体讲完就得讲讲结构体实现位段的能力
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int 、unsigned int 或signed int,在C99中位段成员的类型也可以选择其他类型
2.位段的成员名后边有一个冒号和一个数字
位段中的位指的是二进制中的位
//二进制中一位就是一个比特位
//一个字节是8个比特位
struct S
{
/*int _a:2;*///_a占2个比特位
/*
假设我们在设定a的值的时候,我们只想要a是0 1 2 3 这4个数字
那么二进制位表达出来就是
0---00
1---01
2---10
3---11
这四个数刚好满足两个比特位的要求
如果我们给a32个比特位的话就有点浪费空间了
*/
int _a : 2;
int _b:5;//_b占5个比特位
int _c:10;//_c占10个比特位
int _d : 30;//_d占30个比特位
//int _e : 50;//这种写法是错的,对于一个整型来说,最大也就32个比特位,没有50
};
int main()
{
printf("%zd\n", sizeof(struct S));//8
/*
如果不进行位段的调整,将是16个字节
*/
return 0;
}
//这种设计相当于限制了大小,可以减小空间,避免空间浪费
//8个比特位等于1个字节
通过位段的使用,可以减少空间的占有,减少空间浪费
限制数据的长度、大小
节省空间
位段的内存分配
那么位段时如何改变内存的分配的呢?
-
位段的成员可以是 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;
//
// return 0;
//}
/*
因为这里的数据是char类型的,1个字节,8个比特位
那么我们先开辟8个比特位,我们先用,不够的话再开辟
1.给定了空间后,在空间内部是从左向右使用还是从右向左使用呢?这个是不确定的
c语言并没与规定这个方向
那么我们假设从右到左
1 2 3 4 5 6 7 8---这里表示的是比特位的位置
b b b b a a a
放完a和b还只有一个比特位了,不够的话我们再开辟一个字节
2.当剩下的空间不足以存放喜爱一个成员的时候,空间是浪费还是使用,这个是不确定的
那么我们就假设:是浪费
//那么1号位就浪费了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
b b b b a a a c c c c c
放完c之后,就放d,d要4个比特位,但是现在只剩下3个比特位了
那我们就再次开辟一个字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
b b b b a a a c c c c c d d d d d
此时s b c d都已经放好了,我们用了3个字节
假如我们不浪费的话只要2个字节就够了
那么我们来测试一下
*/
//struct S
//{
// char a : 3;
// char b : 4;
// char c : 5;
// char d : 4;
//};
//int main()
//{
// struct S s;
// printf("%zd\n", sizeof(struct S));//3
// return 0;
//}
/*
这个结果和我们判断的是一样的,占了3个字节大小的空间
可事实真的是这样的么?
那么我们就再举个例子进行判断
*/
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = {0};//将每个比特位都设置为0
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd\n", sizeof(struct S));
return 0;
}
/*
//初始化的情况如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24-----比特位
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 b b b b a a a 0 0 0 c c c c c 0 0 0 0 d d d d
*/
/*
s.a = 10;
往a里面放10,,10的二进制序列就是1010,可是a只有3个比特位,这里的10是4个比特位,3个比特位放不下1010
那么我们只能从地位到高位进行存放,存的是010
s.b = 12;
往b里面放12,12的二进制位是1100,b是4个比特位,那么刚好放得下1100
s.c = 3;
往c里面放3,c占5个比特位,3的二进制是11,那么我们就将00011放进去
前三个比特位还是0
s.d = 4;
往d里面放4,4的二进制是100,d占4个比特位,那么我们就将0100放进去,第一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24-----比特位
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 b b b b a a a 0 0 0 c c c c c 0 0 0 0 d d d d
0 1 1 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0
放置结果如上:
因为4个二进制位写一个16进制,那么我们将16进制表示出来
6 2 0 3 0 4
我们进行调试,和我们分析的是一模一样的
*/
/*总结:
在当前的vs环境下,开辟空间是从右向左使用,如果剩下的空间不够,那我们就浪费这些空间,再开辟一个字节
*/
/*
我们再将一开始的例子拿过来进行判断下
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
因为类型是int
那么我们开辟4个字节,23个比特位,但是a只要2个比特位,剩下30个比特位,
b用5个,还有25个比特位
c用10个,还有15个比特位
d说要用30个比特位,剩下的15个不够了
那么我们再开辟4个字节,就是32个比特位
那么我们给d用30个比特位,剩下2个
算下来我们总共开辟了8个字节,浪费了17个比特位
这就是我们之前为什么算出来是8个比特位了
*/
1.给定了空间后,在空间内部是从右向左使用
2.当剩下的空间不足以存放喜爱一个成员的时候,空间是浪费的
上面两种是不确定的,需要我们实时进行探究
位段的跨平台问题
-
int 位段被当成有符号数还是⽆符号数是不确定的。
-
位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
出问题。
-
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
-
当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的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;
}