在C语言中,数据类型可以分为内置类型(char short int long float double...)和自定义类型。内置类型是由编程语言本身定义的基本数据类型,而自定义类型是由程序员根据需要创建的数据类型。
自定义类型: 结构体 ,联合体(共用体),枚举。
结构体:用于组合多个不同类型的数据项。结构体允许在一个单独的数据结构中存储不同类型的数据。每个数据项在结构体中称为一个成员(member),结构体的每个成员可以是不同类型的变量。
//结构体类型的定义
struct Stu //struct: 结构体关键字 Stu: 结构体标签名(tag)
{ //成员列表 在这不能初始化 因为是创造的类型 并未分配内存 类似于int flaot这种 在使用该结构体类型创建变量时,才会为这些成员分配内存空间。
char name[20];
char id[10];
int age;
}s1,s2; //变量列表 s1与s2是struct Stu类型的变量为全局变量
int main()
{
struct Stu s3;//s3为局部变量
return 0;
}
匿名结构体:指在声明结构体变量时不给出结构体类型名称,直接定义结构体的成员。这种方式常用于临时的数据,无法在其他地方再次使用同样的结构体定义。只能用一次。
//匿名结构体
struct //省略了结构体标签(tag)
{
char name[20];
char id[10];
int age;
}s1;
结构体的自引用:指在结构体的定义中包含指向相同结构体类型的指针
#include <stdio.h>
// 定义一个包含自引用的结构体 Node
struct Node {
int data;
struct Node *next; // 指向下一个 Node 结构体的指针
};
int main() {
// 声明结构体变量
struct Node node1, node2, node3;
// 设置节点数据
node1.data = 10;
node2.data = 20;
node3.data = 30;
// 建立节点之间的关系
node1.next = &node2;
node2.next = &node3;
node3.next = NULL; // 最后一个节点的 next 指针通常设为 NULL
// 遍历链表并输出数据
struct Node *current = &node1;
while (current != NULL) {
printf("%d\n", current->data);
current = current->next;
}
return 0;
}
typedef 对于结构体名的影响
typedef struct Node
{
int data;
struct Node* next;
} Node;
typedef 关键字被用来为结构体 Node 创建一个别名,可以在代码中使用 Node 代替 struct Node 来定义该结构体。在这里,Node 是结构体 Node 的别名,可以像使用其他数据类型一样使用它来声明变量,如Node Node1。
struct Node
{
int data;
struct Node* next;
}Node;
无typedef 关键字,Node表示定义了一个结构体变量 Node,即在定义结构体的同时也创建了一个名为 Node 的结构体变量。所以,Node 在这种情况下是一个具体的结构体变量,而不是一个类型别名。如果需要创建另一个 Node 结构体变量,可以使用 struct Node Node2。
结构体变量的定义和初始化:
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu
{
char name[20];
int age;
};
struct Stu s = {"PTM", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体内存对齐:
在 C 语言中,结构体内存对齐(Memory Alignment)是指编译器为结构体成员分配内存时,为了提高访问效率,会使结构体成员按照一定规则对齐在内存中。内存对齐的规则因编译器和平台而异,通常受到结构体成员的数据类型和编译器的设定影响。以下是一些内存对齐的一般规则:
默认对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
大多数编译器会使用“最严格对齐原则”,即结构体成员的偏移量应该是成员大小的整数倍,且结构体的大小应该是成员中最大成员大小的整数倍。
结构体成员对齐:
基本数据类型通常按其大小对齐(如 char 一般按1字节对齐,int 通常按4字节对齐)。
结构体成员的对齐通常受到平台的影响,比如 32 位平台一般按4字节对齐,64 位平台一般按8字节对齐。
指定内存对齐:
可以使用 #pragma pack(n) 指令(n 为对齐字节数)来改变默认对齐规则,但这种做法可能会增加内存访问的开销。
结构体大小计算:
结构体的大小通常是其成员大小的总和,但因为对齐规则的存在,结构体的大小可能会大于成员大小的总和。
内存对齐优化:
内存对齐可以提高访问效率,但也可能会引起内存浪费,特别是在结构体的成员排列上。
总的来说,内存对齐是为了提高程序的性能和效率,但也需要注意可能带来的内存浪费问题。在实际编程中,可以通过合理设计结构体成员的排列顺序和使用 #pragma pack 等方式来优化内存对齐。
结构体的内存对齐是用空间换时间,将结构体成员按照大小递减的顺序排列,让占用空间小的成员尽量集中在一起。
#include <stdio.h>
#include <stddef.h>
#pragma pack()//恢复到默认对齐
//x64
struct S1 //0(a) 1 2 3 4(b) 5 6 7 8(c) 9 10 11 12 13 14 15
{
char a;
int b;
double c;
};
struct S2 //0(b) 1 2 3 4 5 6 7 8(c) 9 10 11 12 13 14 15 16(a) 17 18 19 20 21 22 23 24
{
int b;
double c;
char a;
};
#pragma pack(4)//更改默认对齐为4字节
struct S3 //0(b) 1 2 3 4(c) 5 6 7 8 9 10 11 12(a) 13 14 15
{
int b;
double c;
char a;
};
int main()
{
printf("%zd\n", sizeof(struct S1));//16
printf("%zd\n", sizeof(struct S2));//24
printf("%zd\n", sizeof(struct S3));//16
printf("\n");
printf("%zd\n", offsetof(struct S1, a));//0
printf("%zd\n", offsetof(struct S1, b));//4
printf("%zd\n", offsetof(struct S1, c));//8
printf("\n");
printf("%zd\n", offsetof(struct S2, a));//16
printf("%zd\n", offsetof(struct S2, b));//0
printf("%zd\n", offsetof(struct S2, c));//8
printf("\n");
printf("%zd\n", offsetof(struct S3, a));//12
printf("%zd\n", offsetof(struct S3, b));//0
printf("%zd\n", offsetof(struct S3, c));//4
return 0;
}
结构体传参:结构体传参的时候,最好传结构体的地址。
#include <stdio.h>
struct S
{
char arr[20];
int n[3];
int num;
};
void print1(struct S s)
{
int i = 3;
printf("%s\n",s.arr);
while (i--)
{
printf("%d ", s.n[i]);
}
printf("%d \n", s.num);
}
void print2(const struct S* s)
{
int i = 3;
printf("%s\n", (*s).arr);
while (i--)
{
printf("%d ", s->n[i]);
}
printf("%d ", s->num);
}
int main()
{
struct S s = { "hello world",{1,2,3},-10 };
print1(s); //传值调用 在传值方式下,函数会复制整个结构体的内容,函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销,会导致性能的
下降。
print2(&s); //传址调用 通过传递结构体指针的方式,可以减少复制结构体的开销。
return 0;
}
位段:位段(Bit Fields)是 C 语言中一种特殊的结构体成员,用于在结构体中按位对数据进行存储。通过位段,可以将一个整数类型的数据按照指定的位数拆分成多个字段,从而节省内存空间。
struct {
type member_name : width;
} variable_name;
type
:表示位段的数据类型,可以是int
、char
等整数类型。member_name
:位段成员的名称。width
:指定该位段的位宽,即占用的位数。
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的跨平台问题 :
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
总结: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举: 用于存储一组固定的常量值,把可能的取值一 一列举出来。
#include <stdio.h>
enum Day //enum: 枚举关键字 Day: 枚举标签
{
Mon,//枚举常量 这些取值都是有值的,默认从0开始,一次递增1。
Tues,
Wed=5,//赋初值从5开始,一次递增1。
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Day d = Fri;//声明一个名为d的枚举类型变量,并将其赋值为Fri。
d = Mon;
printf("%d\n", Mon);//0
printf("%d\n", Tues);//1
printf("%d\n", Wed);//5
printf("%d\n", Thur);//6
printf("%d\n", Fri);//7
return 0;
}
联合体:允许在相同的内存位置存储不同的数据类型。在联合体中,所有成员共享相同的内存空间又叫共用体,因此联合体的大小等于最大的成员大小。
#include <stdio.h>
union Un //union: 联合体关键字 Un: 联合体标签
{
int a;//4
char b;//2
};
int main()
{
union Un u;//创建一个联合体变量
printf("%d\n",sizeof(u));//4
printf("%p\n", &u);
printf("%p\n", &(u.a));
printf("%p\n", &(u.b));
u.a = 0x12345678;
u.b = 0x11;
printf("%#x\n", u.a);//0x12345611
return 0;
}
#include <stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int check_sys2()
{
union //匿名联合体 只使用一次
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
//判断存储模式(大端模式/小端模式)
int main()
{
//int a = 1;//0x 00 00 00 01
//低地址----------高地址
//01 00 00 00 --- 小端存储
//00 00 00 01 --- 大端存储
if (check_sys2())
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
联合体大小的计算:
联合体的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include <stdio.h>
union Un
{
char arr[9];//9
long long i;//8
}u;
int main()
{
printf("%d\n",sizeof(long));//4
printf("%d\n", sizeof(long long));//8
printf("%d\n", sizeof(u));//16
return 0;
}