目录
C语言中有内置类型(本身支持的),也有自定义类型(struct-结构体,enum-枚举,union-联合体)
1.结构体类型的声明
1.1结构体回顾
结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量
1.1.1结构的声明
struct tag
{
member - list;//成员-1个或者多个
}variable-list;//变量列表
举例:
struct Book
{
char name[20];
char author[20];
float price;
char id[19];
//...
}b3, b4;//全局变量
struct Book b2;//全局变量
int main()
{
struct Book b1;//局部变量
struct Book arr[5];//数组
}
1.1.2结构体变量的创建和初始化
struct Book
{
char name[20];
char author[20];
float price;
char id[19];
//...
};
int main()
{
struct Book b1 = { "C语言","peng",10.0,"1001" };//局部变量初始化
//也可以乱序:
struct Book b2 = { .id = "1002",.author = "shen",.name = "C",.price = 12.9 };
}
1.2结构体的特殊声明
在声明结构体的时候,可以不完全的声明:
比如:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
上面的两个结构体在声明的时候省略掉了结构体标签(tag)——匿名结构体
在上面代码的基础上,p=&x;是不合法的
警告:
- 编译器会把上面两个声明当作完全不同的两个类型,所以非法
- 匿名的结构体类型,如果没有对结构类型重命名的话,基本上只能用一次
这个时候在main函数中出现struct s就会出现报错
需要在最后的;前面命名,即:
#include <stdio.h>
struct
{
int a;
char b;
double c;
}s = { 12,'x',1.0 };
//struct
//{
// int a;
// char b;
// float c;
//}a[20], * p;
int main()
{
printf("%d %c %lf\n", s.a, s.b, s.c);//12 x 1.000000
return 0;
}
给匿名结构体再起一个名字:
typedef struct
{
char c;
int i;
double d;
}S;
int main()
{
S s;
return 0;
}
这个代码无意义
1.3结构体的自引用
在结构体中包含一个类型为该结构体本身的成员是否可以?
比如,定义一个链表的节点:
数据结构:数据在内存中的存储和组织的结构
数据结构:线性数据结构(顺序表、链表、栈、队列)、树形数据结构(二叉树)、图
struct Node
{
int data;
struct Node n;//这种写法是不合理的,找不到sizeof(struct Node),无限进行下去
};
这种写法不合理,下面进行改进:
正确的自引用方式:
struct Node
{
int data;
struct Node* n;//指针——自引用
};
typedef struct Node
{
int data;
//Node* next;//出现报错——还没有Node类型
//匿名的结构体类型不能实现这种效果
struct Node* next;
}Node;
int main()
{
struct Node n1;
Node n2;
return 0;
}
2.结构体内存对齐(重量级)
我们已经掌握了结构体的基本使用,现在深入讨论一个问题:计算结构体的的大小
这涉及到一个热门考点:结构体内存对齐
2.1对齐规则
下面看一段代码:
#include <stdio.h>
struct S
{
char c1;//1
int i;//4
char c2;//1
};
int main()
{
struct S s = { 0 };
printf("%zd", sizeof(s));//12
return 0;
}
结果为什么是12?原因:
结构体的对齐规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
-VS中默认的值为8
-Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
练习2:
struct S2
{
char c1;//1 8——1
char c2;//1 8——1(对齐数为1)
int i;//4 8——4
};
int main()
{
//struct S s = { 0 };
struct S2 s2 = { 0 };
printf("%zd", sizeof(s2));//8
return 0;
}
练习3:
#include <stdio.h>
struct S3
{
double d;//8 8——8
char c;//1 8——1
int i;//4 8——4
};
int main()
{
struct S3 s3 = { 0 };
printf("%zd", sizeof(s3));//16
return 0;
}
练习4:
#include <stdio.h>
struct S3
{
double d;//8 8——8
char c;//1 8——1
int i;//4 8——4
};
struct S4
{
char c1;//1 8——1
struct S3 s3;//16 8——8
double d;//8 8——8
};
int main()
{
struct S4 s4 = { 0 };
printf("%zd", sizeof(s4));//size_t
//32
return 0;
}
2.2为什么存在内存对齐?
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定的数据,否则抛出硬件异常
2.性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了,否则,我们可能需要执行两次,因为对象可能被分在两个8字节内存块中(浪费时间)
总体来说:结构体的内存对齐是拿空间来换取时间的做法
那在设计结构体的时候,我们既要满足对齐,又要节省空间:
基于以上两个例子,我们可以得出让占用内存小的成员尽量集中在一起
2.3修改默认对齐数
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数(一般改成2的次方数)
#pragma这个预处理指令,可以改变编译器的默认对齐数:
#include <stdio.h>
#pragma pack(1)//设置默认对齐数
struct S
{
char c1;//1 1——1
int i;//4 1——1
char c2;//1 1——1
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
//输出的结果是?
printf("%d\n", sizeof(struct S));//6
return 0;
}
3.结构体传参
#include <stdio.h>
struct S
{
int arr[1000];
int n;
double d;
};
void print1(struct S tmp)//造成时间和空间上的浪费
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("%d ", tmp.n);
printf("%lf", tmp.d);
}
void print2(const struct S* ps)//传的是地址,显然print2更好,效率更高
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
printf("%d ", ps->n);
printf("%lf", ps->d);
}
int main()
{
struct S s = { {1,2,3,4,5},100,3.14 };
//print1(s);//1 2 3 4 5 100 3.140000
print2(&s);//1 2 3 4 5 100 3.140000
return 0;
}
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
结论:
结构体传参的时候,要传结构体的地址
4.结构体实现位段
位段基于结构体
4.1什么是位段
专门用来节省内存
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是int ,unsigned int或signed int,在C99中位段成员的类型也可以选择其他类型
- 位段的成员名后边有一个冒号和一个数字
比如:
#include <stdio.h>
struct S
{
int _a : 2;//_a仅仅占2个比特位(不再是32个,节省了30个)
int _b : 5;//只占5个比特位
int _c : 10;//只占10个比特位
int _d : 30;//位段式的结构
//位——2进制位
};
int main()
{
printf("%zd", sizeof(struct S));//8
return 0;
}
4.2位段的内存分配
- 位段的成员可以是int ,unsigned int,signed int或char等类型
- 位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
在申请内存空间时,VS中是从右向左使用,剩余的空间,不足下一个成员使用的时候将会被浪费掉
#include <stdio.h>
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;
printf("%d", sizeof(s));//3
return 0;
}
4.3位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题)(32位和64位机器上int的长度是4个字节,16位机器上是2个字节)
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用是不确定的
总结:
跟结构相比,位段可以达到同样的结果,并且可以很好的节省空间,但是有跨平台的问题存在
因此根据不同的平台写不同的代码
4.4位段的应用
下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的结果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的
4.5位段使用的注意事项
位段的几个成员共用同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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;
}