自定义数据的类型:
结构体
在C语言中有内置的类型如char、int、long等类型,但这些类型都不能满足我们的需求,所以有自定义的数据类型。其中包括有:结构体、枚举、联合体。下面为结构体各部分的意义:
// ↓-结构体标签
struct peop
//↑-结构体关键字
{
char name[10];
int tele;
int age;//←结构体成员列表
};//s1,s2定义的是全局变量
//↑-变量列表
struct peop s2;//全局变量
int main()
{
struct peop s1 = {"王五",1888888,15};//结构体定义的局部变量
return 0;
}
其中,结构体的标签也可以省去,此时结构体变成匿名结构体。这种匿名结构体需要在声明的同时创造变量
struct
{
char a;
int e;
}x;//←创造结构体变量
虽然char* 可以用来定义char类型的指针变量,但下面的匿名结构体指针类型不可以用来定义有相同成员变量的结构体类型的变量
struct
{
char a;
int b;
}a;
struct
{
char a;
int b;
}* pa;
//pa!=&a即使成员相同,编译器还是会将它们理解为两种不同的类型
结构体的自引用
与函数的递归不同,结构体这中不能包含这个结构体的类型
struct s
{
char a;
struct s b;//这种写法是错误的
};
那么结构体的正确自引用方式是将一个结构体内容一分为二,一部分存储内容,一部分存储下个结构体的地址。
struct s
{
char a;
struct s* next;
};
结构体的成员变量的定义和初始化
定义和初始化如下:
struct sa
{
int a1;
char a2;
};
struct s
{
char b1;
int b2;
struct sa b3;
};
int main()
{
struct s st = { 'e',22,{7,'a'}};//初始化
st.a = 'e';
st.c.a = 10;
return 0;
}
结构体内存对齐:
1.第一个成员在与结构体变量偏移量为0的地址处
2.其余的成员要对齐到某个数(对齐数)的整数倍地址处
对齐数=编译器默认的对齐数与该成员类型大小的较小值(VS为8)
3.结构体的内存大小等于所有成员对齐数中最大对齐数的整数倍
4.如果嵌套了结构体,那么嵌套的结构体就对齐到它的最大对齐数的整数倍,最后整个结构体的的最大对齐数(含嵌套结构体的对齐数)
struct s1
{
char a1;
int a2;
char a3;
};
struct s2
{
char b1;
char b2;
int b3;
};
int main()
{
struct s1 s1;
struct s2 s2;
printf("%d %d", sizeof(s1), sizeof(s2));//12 8
return 0;
}
结构体内存对齐的原因大致有两点:
1.是便于在不同平台移植2.是用空间来换时间,减少多次访问的必要。
利用这个特点我们可以在定义变量时尽量将占用内存小的变量定义在先,占内存大的变量定义在后边,这样有利于我们减少结构体占用的内存空间。
当然,我们也可以通过下面的方法改变默认对齐数
#pragma pack(4)//默认对齐数改为4
struct s1
{
char a;
double b;
};
#pragma pack()//撤销改变的默认对齐数
struct s2
{
char a;
double b;
};
int main()
{
printf("%d\n", sizeof(struct s1));//12
printf("%d", sizeof(struct s2));//16
return 0;
}
结构体传参
类似于C语言内置的类型,结构体也可以作为参数传给函数,结构体传参可以以传递结构体指针的方式传递给函数,这样可以在函数的内部直接改变结构体的内容。其次也可以在函数内开辟一块自定义类型的空间。
位段
位段是一种特殊的结构体,它允许结构体中以位为内存长度指定成员变量大小。这样做可以节约内存的空间。
位段的声明如下:
struct S
{
int a : 2;//一般里面的变量是int、unsigned int等整型类型的变量
int b : 4;
int c : 10;
int d : 30;
};
位段有特殊的存储方式,
struct S
{
int a : 3;
int b : 4;
int c : 10;
int d : 30;
};
int main()
{
printf("%d", sizeof(struct S));//8
return 0;
}
位段里面的变量结构大致为类型(int、unsigned int等)+变量名+冒号+数字(<=32);这里面的冒号后面的数字决定了为这个类型的变量开辟几个比特位大小的空间。因为若直接使用一般的结构体而变量a的实际大小并不能取到很大,那么将会造成空间的浪费,于是使用位段,将变量的空间进行削减。
而位段的存储方式与一般结构体又有区别,位段在空间上是按照4个字节(int)或者1个字节(char)大小开辟的。以上面的为例。有4个整型变量a、b、c、d。所以先开辟一块4个字节的空间,共有32个比特位,其中a占3个、b占4个、c占10个,共占了17个比特位。此时还剩下15个比特位,而d需要30个比特位,所以不够的部分需要另外开辟一块4个字节的空间给d。所以加起来共需要8个字节的空间大小。
再如下面的例子
struct S1
{
char a : 4;
char b : 2;
char c : 6;
char d : 3;
};
int main()
{
struct S1 a = {9,1,68,6};
return 0;
}
在内存中的分布是19 04 06,二进制位表示是00011001 00000100 00000110。要是二进制位存不下会弃掉高位。
位段也会有跨平台的问题:
1.int被当成有符号还是无符号不确定,因为有时会省去int的最高位符号位
2.位段中变量放置的顺序方向不确定
3.位段中最高位位数有时不能确定,例如char a:2但是a=8,此时会舍去100中的1变成00放入。
4.位段中剩余的空间如何处理不确定,如char a:3;char b:4;则多出来的一个比特位如何处理不确定。
枚举
枚举是将所有可能一一列举出来的结构类型,大括号里面是枚举常量,其中枚举常量的大小是4个字节。
enum alphabet
{
//枚举常量
A,//0
B,//1
C,//2
D//3
};
枚举的常量一般从数字0开始到后面一直递增,当然,也可以在里面修改某个常量对应的数字的值,后面的常量对应的数字也会跟着变化。
枚举相对于#define的优点:
1.#define定义的是标识符,而枚举是一种类型,更严谨
2.#define出错不易察觉
3.可以防止命名被污染,即#define定义后的符号在后面都会有独特的意义
联合(共用体)
联合体是一种特殊的结构体,里面的成员共用一块内存空间,所以它们的大小至少是所有成员里面最大的那个内存大小,且当联合体最大成员的大小不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍。
union un
{
//默认对齐数:8
int a;//大小:4 对齐数:4
char a[5];//大小:5 单个元素大小为:1 对齐数:1
//数组算的是元素的对齐数
//最大对齐数:4,最大成员:5
};
//sizeof(union un);//8
而且联合体的成员不能同时使用。如下为联合体的声明:
union un
{
char a;
int b;
};
因为它们共用一块内存空间,所以有&un.a和&un.b和&un是同一个地址。