C语言里自定义类型有3种,分别是结构体,枚举,联合
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明形式如下
struct tag//tag是标签名
{
member-list;//大括号里面是结构体成员
}variable-list;//大括号后面分号前是变量列表,可以在这里定义变量。注意最后的分号不能少
示例
struct Book
{
char name[30];
double price;
char author[30];
}book1;
在上面的例子中,Book是结构体标签,在这段代码之后我们可以用如下的命令来创建这种类型的结构体变量
struct Book book2;//struct Book是结构体类型,book2是结构体变量
结构体的标签名是可以省略的,但这样这种结构体就只能在大括号后面直接定义变量。
示例
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;//这里a是结构体数组,p是结构体指针
由于匿名结构体没有标签,我们无法像前面的例子一样用
struct+标签+变量名;
这样的方式创建变量,所以我们一般不用匿名结构体。
这里再举一个关于匿名结构体的错误示范
struct
{
int a;
char b;
double c;
}s;
struct
{
int a;
char b;
double c;
}*ps;
ps=&s;
这样的代码是有问题的,看起来ps指向的类型和s的类型是同一种类型,但是编译器会把他们认为是两种不同的结构体类型,虽然会得出想要的结果,但是编译器会提出警告。
结构体声明知识创建了一种类型,并没有实际分配空间。
#include
#include
struct BOOK
{
float price;
char name[30];
}*ps;
int main()
{
ps->price = 88.9f;
//ps->name = "C primer plus";//注意字符串拷贝不能用等号!
strcpy(ps->name, "C primer plus");
printf("%f\n", ps->price);
printf("%s\n", ps->name);
return 0;
}
正确的写法应该是下面这样
#include
#include
struct BOOK
{
float price;
char name[30];
}*ps,s;
int main()
{
ps = &s;
ps->price = 88.9f;
//ps->name = "C primer plus";//注意字符串拷贝不能用等号!
strcpy(ps->name, "C primer plus");
printf("%f\n", ps->price);
printf("%s\n", ps->name);
return 0;
}
结构体的成员也可以是结构体,但是不能是自己这种结构体类型;
比如如下的代码是无法通过编译的
struct Node
{
int data;
struct Node next;
};
这样写,在创建struct Node类型的变量时,由于有一个成员的就是struct Node,我们需要知道它的大小才能为它分配空间,但是在创建完struct Node之前并不知道它的大小,这样就造成了逻辑上的死循环。
但是下面这种自引用是可以的
struct Node
{
int data;
struct Node* next;
};
上面这种结构体其实就是链表的结点。
typedef struct
{
int data;
Node* next;
}Node;
这段代码的意思是,创建了一种匿名结构体,包含两个类型分别为int和Node*的成员,typedef把这个结构体重命名为Node。
但是这里存在一个问题是,在到达
}Node;
这一句之前,Node者种类型还是不存在的,匿名结构体中类型Node*的那个成员无法创建,又造成了逻辑上的死循环。
结构体变量的成员是通过点操作符(.)访问的。点操作符接收两个操作数:左操作数是结构体变量的名字,右操作数是需要访问的成员的名字。表达式的结果就是指定的成员。
比如我要访问下面这个结构体的成员name
struct Book
{
char name[30];
double price;
char author[30];
}book1;
那么只要这样写
book1.name;//这样就相当于拿到了一个字符数组的数组名
//比如
scanf("%s",book1.name);
需要注意的是,对于复杂的结构体,在访问成员的时候要注意操作符的优先级及结合性(可以参照我前面的博客C语言——操作符笔记)。
例如
struct SIMPLE
{
int a;
char b;
float c;
};
struct COMPLEX
{
float f;
int a[20];
long *lp;
struct SIMPLE sa[10];
struct SIMPLE *sp;
};
struct COMPLEX comp;
对于下面这个表达式
((comp.sa)[4]).c
成员sa是一个结构数组,所以
comp.sa
是一个数组名,它的值是一个指针常量。对这个表达式使用下表解引用操作
(comp.sa)[4]
将选择一个数组元素,但是这个元素本身是一个结构,所以可以使用另一个点操作符取得它的成员之一。
比如
((comp.sa)[4]).c
考虑到引用和点操作符具有相同的优先级,它们的结合性都是从左到右,所以可以省略所有的括号,即下面的表达式和上面的表达式等效。
comp.sa[4].c
如果有一个指向结构的指针,要访问这个结构的成员有两种方式
先对指针执行间接访问操作,从而获得这个结构,然后再使用点操作符获得这个成员。
需要注意的是,点操作符的优先级高于间接访问操作符,所以必须在表达式中使用括号,确保间接访问首先执行。
比如
struct Book
{
char name[30];
double price;
char author[30];
}book1;
struct BOOK *pb;
pb=&book1;
可以用
(*pb).name
来访问name这个元素
使用
->
操作符(箭头操作符)
箭头操作符接受两个操作数,左操作数必须是一个指向结构的指针。
箭头操作符对左操作数执行间接访问取得指针指向的结构,
然后根据右操作数选择一个指定的结构成员。
由于间接访问操作内置于箭头操作符中,所以不需要显式地执行间接访问或使用括号。
比如上面地例子,我们可以这样访问name这个元素
pb->name
这样和
(*pb).name
是等效的
结构体的初始化和数组初始化类似。
一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构中各个成员的初始化。
这些值根据结构成员列表的顺序写出。
如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。
结构中如果包含数组或结构成员,其初始化方式类似于多维数组的初始化。
一个完整的聚合类型成员的初始值列表可以嵌套于结构的初始值列表内部。
例如
struct EXAMPLE
{
int a;
short b[10];
struct SIMPLE c;
}x={
10,
{1,2,3,4,5},
{25,'x',1.9}
};
结构体内存对齐指的是结构体成员在内存中的是如何分配的。
不同的分配方式将导致同样成员的结构体其占用内存空间不同。
在这里先给出结构体内存对齐规则
- 第一个成员在于结构体变量偏移量为0的地址处。
- 其他成员变量要对其到对齐数的整数倍地址处。
对齐数:编译器默认的一个对齐数于该成员大小的较小值。
注:vs默认对齐数是8.- 结构体总大小为最大对齐数(每个成员都以一个对齐数)的整数倍。
- 如果嵌套了结构体,则该结构体对齐到自己的最大对齐数的整数倍数处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍处。
下面给出几个例题
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
double a;
char b;
int c;
};
struct S4
{
char a;
struct S3 b;
double c;
};
使用#pragma预处理指令,可以修改编译器默认对齐数
例如
#include
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zu\n", sizeof(struct S1));
printf("%zu\n", sizeof(struct S2));
return 0;
}
offsetof是一个定义在stddef.h的宏,它接收两个参数一个是结构体,另一个是结构体成员,返回一个size_t的参数表示该成员相对结构起始位置的偏移量。
例如我们知道在下面这个结构体中i相对于结构体起始位置的偏移量为4
struct S2
{
char c1;
int i;
char c2;
};
结构体是标量,自然也可以其他基本类型一样传参。
即我们可以选择对结构体传值调用,也可以对结构体传址调用,
但是在我前面关于函数栈帧的博客间接提到过
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
考虑到结构体的大小对于传参时程序性能的影响,我们对结构体传参时,几乎都是选择传址调用。
比如
#include
struct S
{
int a;
char b;
};
void fun(struct S*s)
{
printf("%d\n", s->a);
printf("%c\n", s->b);
}
int main()
{
struct S s;
s.a = 10;
s.b = 'k';
fun(&s);
return 0;
}
结构体拥有实现位段的能力。
我们可以把位段理解为成员是一个或多个位的字段的结构体。
位段的声明和结构体类似,与结构体的区别是
比如
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
就是一个位段声明,这个结构包含了4个位段。
位段每次以4字节为单位开辟空间。如果已开辟的空间无法容纳所有的位段,将会另外再开辟4字节。
那么上面的结构A前三个位段需要17个位(bit),可以放在4字节中;4字节放了17个位之后还剩下15个位,第四个位段需要30个位,第一个开辟的4字节显然无法容纳30位,必然要另外开辟4字节。虽然标准没有明确规定这第四个位段是接着前三个位段放还是在第二个开辟的4字节开始放,但是可以确定的是无论是这两种中的哪一种,一个这样的结构都是需要8字节空间的。
如下
需要注意的是,注重可移植性的程序应该避免使用位段。由于位段与实现有关的依赖性,位段在不同的系统中可能有不同的结果。
注:字指多个字节。
struct CHAR
{
unsigned ch : 7;
unsigned font : 6;
unsigned size : 19;
};
如果使用一般的结构每个这样的结构变量需要占用12字节,但是使用位段的话只需要4字节就足够了。
在《C和指针》上是这样描述这种好处的:
它能够把长度为奇数的数据包装在一起,节省内存空间。当程序需要使用成千上万的这类结构是,这种节省方法会变得相当重要。
枚举就是把可能的值一一列举,用一个名称来表示一个整形值。(但是注意枚举是枚举类型,并不是整形类型)
在某种程度上,我们可以把枚举看成是对#define定义宏常量的一种替换。
比如
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
他们每一个都表示一个整形值,默认从0开始递增。
比如
RED表示0,GREEN表示1
他们代表的值可以在定义的时候自己设定
比如
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
需要注意的是,枚举定义的时候,各个枚举常量之间用逗号分隔(不是分号!),并且最后一个枚举常量的最后不用写逗号或分号。
但是和结构体一样,大括号后面不要忘记写分号。
这里要注意的就是,尽量用用枚举常量给枚举变量赋值,避免类型冲突。
且枚举常量是常量,不可修改。
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
//声明
union Un
{
char c;
int i;
};
//定义变量
union Un un;
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
关于联合的大小我们有如下规则
比如
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
在了解了结果提对齐的计算方法后,结合联合的特点,我们不难计算出以上两个联合的大小分别是8和16字节
最后,根据联合的特点,我们可以重新写个程序判断当机器使用大端还是小段模式。
#include
union Un1
{
int a;
char b;
};
int main()
{
union Un1 k;
k.a = 0x1;
if (k.b == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}