前言
C语言里自定义类型有3种,分别是结构体,枚举,联合
结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明
结构体的声明形式如下
struct tag
{
member-list;
}variable-list;
示例
struct Book
{
char name[30];
double price;
char author[30];
}book1;
在上面的例子中,Book是结构体标签,在这段代码之后我们可以用如下的命令来创建这种类型的结构体变量
struct Book book2;
匿名结构体
结构体的标签名是可以省略的,但这样这种结构体就只能在大括号后面直接定义变量。 示例
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
由于匿名结构体没有标签,我们无法像前面的例子一样用
struct+标签+变量名;
这样的方式创建变量,所以我们一般不用匿名结构体。
使用结构体时的两个易错点
易错点1
这里再举一个关于匿名结构体的错误示范
struct
{
int a;
char b;
double c;
}s;
struct
{
int a;
char b;
double c;
}*ps;
ps=&s;
这样的代码是有问题的,看起来ps指向的类型和s的类型是同一种类型,但是编译器会把他们认为是两种不同的结构体类型,虽然会得出想要的结果,但是编译器会提出警告。
易错点2
结构体声明知识创建了一种类型,并没有实际分配空间。
#include<stdio.h>
#include<string.h>
struct BOOK
{
float price;
char name[30];
}*ps;
int main()
{
ps->price = 88.9f;
strcpy(ps->name, "C primer plus");
printf("%f\n", ps->price);
printf("%s\n", ps->name);
return 0;
}
大家可以自行尝试运行如上代码,程序会挂掉
正确的写法应该是下面这样
#include<stdio.h>
#include<string.h>
struct BOOK
{
float price;
char name[30];
}*ps,s;
int main()
{
ps = &s;
ps->price = 88.9f;
strcpy(ps->name, "C primer plus");
printf("%f\n", ps->price);
printf("%s\n", ps->name);
return 0;
}
结构体自引用的两个易错点
易错点1
结构体的成员也可以是结构体,但是不能是自己这种结构体类型; 比如如下的代码是无法通过编译的
struct Node
{
int data;
struct Node next;
};
这样写,在创建struct Node类型的变量时,由于有一个成员的就是struct Node,我们需要知道它的大小才能为它分配空间,但是在创建完struct Node之前并不知道它的大小,这样就造成了逻辑上的死循环。
但是下面这种自引用是可以的
struct Node
{
int data;
struct Node* next;
};
上面这种结构体其实就是链表的结点。
易错点2
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
结构成员的间接访问
如果有一个指向结构的指针,要访问这个结构的成员有两种方式
方法1
先对指针执行间接访问操作,从而获得这个结构,然后再使用点操作符获得这个成员。 需要注意的是,点操作符的优先级高于间接访问操作符,所以必须在表达式中使用括号,确保间接访问首先执行。 比如
struct Book
{
char name[30];
double price;
char author[30];
}book1;
struct BOOK *pb;
pb=&book1;
可以用
(*pb).name
来访问name这个元素
方法2
使用
->
操作符(箭头操作符) 箭头操作符接受两个操作数,左操作数必须是一个指向结构的指针。 箭头操作符对左操作数执行间接访问取得指针指向的结构, 然后根据右操作数选择一个指定的结构成员。 由于间接访问操作内置于箭头操作符中,所以不需要显式地执行间接访问或使用括号。 比如上面地例子,我们可以这样访问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. - 结构体总大小为最大对齐数(每个成员都以一个对齐数)的整数倍。
- 如果嵌套了结构体,则该结构体对齐到自己的最大对齐数的整数倍数处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍处。
下面给出几个例题
例1
struct S1
{
char c1;
int i;
char c2;
};
例2
struct S2
{
char c1;
char c2;
int i;
};
例3
struct S3
{
double a;
char b;
int c;
};
例4
struct S4
{
char a;
struct S3 b;
double c;
};
验证
修改默认对齐数
使用#pragma预处理指令,可以修改编译器默认对齐数 例如
#include<stdio.h>
#pragma pack(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
offsetof是一个定义在stddef.h的宏,它接收两个参数一个是结构体,另一个是结构体成员,返回一个size_t的参数表示该成员相对结构起始位置的偏移量。 例如我们知道在下面这个结构体中i相对于结构体起始位置的偏移量为4
struct S2
{
char c1;
int i;
char c2;
};
结构体传参
结构体是标量,自然也可以其他基本类型一样传参。 即我们可以选择对结构体传值调用,也可以对结构体传址调用, 但是在我前面关于函数栈帧的博客间接提到过
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
考虑到结构体的大小对于传参时程序性能的影响,我们对结构体传参时,几乎都是选择传址调用。 比如
#include<stdio.h>
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;
}
位段(bit field)
结构体拥有实现位段的能力。 我们可以把位段理解为成员是一个或多个位的字段的结构体。 位段的声明和结构体类似,与结构体的区别是
- 它的成员必须声明为int、signed int或unsigned int类型。
- 在成员名的后面是一个冒号和一个整数。
比如
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字节空间的。
如下 需要注意的是,注重可移植性的程序应该避免使用位段。由于位段与实现有关的依赖性,位段在不同的系统中可能有不同的结果。
- int 被当成有符号数还是无符号数。
- 位段中位的最大数目。许多编译器把位段成员的长度限制在一个整形值的长度内,所以一个能在32位整数的机器上的位段声明可能在16位整数的机器上无法运行。
- 位段中的成员是从左向右还是从右向左分配取决于实现。
- 当一个声明指定了两个位段,且第二个位段比较大,无法容纳于第一个位段剩余的位时,编译器有可能把第二个位段放在内存的下一个字,也可能直接放在第一个位段后面。
注:字指多个字节。
位段的意义
- 节省空间
比如如下的例子
struct CHAR
{
unsigned ch : 7;
unsigned font : 6;
unsigned size : 19;
};
如果使用一般的结构每个这样的结构变量需要占用12字节,但是使用位段的话只需要4字节就足够了。 在《C和指针》上是这样描述这种好处的:
它能够把长度为奇数的数据包装在一起,节省内存空间。当程序需要使用成千上万的这类结构是,这种节省方法会变得相当重要。
- 位段可以方便的访问一个整型值得部分内容。
这类应用出现在操作系统设计中,所以这里不举例子。有兴趣可以参考《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
};
枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
枚举的使用
这里要注意的就是,尽量用用枚举常量给枚举变量赋值,避免类型冲突。 且枚举常量是常量,不可修改。
联合
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
联合的声明
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<stdio.h>
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;
}
|