在之前C语言学习中我们接触了整形类型、浮点型类型、指针类型、数组类型等,但是我们发现要描述一个复杂对象的时候,如描述一个人,需要有姓名、身高、年龄、体重等属性,并且每个属性可能是不一样的数据类型。我们发现之前学习的数据类型都无法满足。学过面向对象编程语言的肯定会想到通过定义对象来实现,在C语言中我们通过结构体来实现上述功能。
结构体:一些值的集合,这些值称为它的成员,但结构体的各个成员可以具有不一样的数据类型
注意:结构体的各个成员可能具有不一样的数据类型,即各个成员占内存大小是不一定相同的,因此无法通过下标进行访问。相反,每个成员都有自己名字,它们是通过名字访问的
语法:
struct tag
{
member-list;
}variable-list;
tag :可选,结构体标签名
member-list:成员列表,成员由数据类型与成员名组成
variable-list:可选,结构体变量列表
结构体类型:struct关键字+tag
例如
//声明一个结构体为struct person类型,有4个成员变量,分别为姓名、身高、年龄、体重
struct person
{
char name[10];
float height;
int age;
float weight;
}; //注意一定要;结尾
注意:结构体声明并不占用内存空间,只有结构体变量定义时才会跟它分配内存空间,结构体声明相当于画图纸,结构体定义相当于照着图纸做房子
小技巧:
如果一个结构体类型在多个源文件中需要使用,可以把结构体类型放在头文件中,如果源文件需要用到这个结构体,可以使用#include
指令把头文件包含进来
例一:
在声明时并定义结构体变量
struct person
{
char name[10];
float height;
int age;
float weight;
}s1; //在声明结构体类型时,并定义全局结构体变量s1
例二:
在声明时并定义结构体数组及结构体指针
struct person
{
char name[10];
float height;
int age;
float weight;
}arr[5],*p; //在声明结构体类型时,并定义结构体数组arr,arr有5个元素,每个元素类型为struct person.并定义结构体指针p
例三:
声明匿名结构体
struct //没有写tag,结构体类型由struct关键字+tag组成,除了声明时能创建结构体变量外,其他地方不能创建结构体变量,因为无法写结构体类型
{
char name[10];
float height;
int age;
float weight;
}s1; //定义全局结构体变量s1
例四:
结构体类型重定义
typedef struct //类型重定义,person现在是结构体类型名而不是结构体变量
{
char name[10];
float height;
int age;
float weight;
}person;
在之前结构体声明例子中,只使用了简单类型的结构体成员,但结构体成员类型可以是:数组、指针甚至是其他结构体
struct A
{
int i;
char ch[5];
};
struct B //结构体类型struct B中有4个成员,分别是浮点数、整形指针、整形数组、一个结构体
{
double d;
int* p;
int arr[10];
struct A a;
};
结构体变量的定义可以在结构体声明时定义,也可以在其他位置定义,它们的区别在于结构体变量是全局变量还是局部变量
结构体变量定义后,内存会根据结构体类型大小跟它分配空间
#include
struct person
{
char name[10];
float height;
int age;
float weight;
}s1; //全局变量
struct person s2; //全局变量
int main()
{
struct person s3; //局部变量
return 0;
}
初始化:在结构体变量定义时对其赋值。一个位于一对花括号内部,由逗号分隔的初始值列表,这些值根据结构体成员顺序写出。如果初始列表的值不够,剩余结构体成员被赋值为0
struct person
{
char name[10];
float height;
int age;
float weight;
}s1 = {"小明",1.78,20,100}; //定义并初始化
struct person s2 = {"小红",1,62,18,90}; //定义并初始化
警告:在一个结构体内部包含一个类型为该结构体本身的成员是非法的
struct A
{
int i;
struct A a;
};
这种结构体自引用是非法的,成员a是另一个完整结构体,其内部还将包含它自己的成员吧,这第二个成员又是另一个完整结构体,它还将包含自己成员a,这样无限套娃下去,没有任何意义。
在一个结构体内部包含一个类型为该结构体指针类型的成员是合法的。因为指针大小是确认的,32位为4字节(64位8字节)。高级数据结构如链表、树都是这种自引用实现的
struct A
{
int i;
struct A * a;
};
警告:警惕下面这种声明陷阱
这个声明的目的是创建一个类型名为A的结构体,但是它失败了。类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义
typedef struct
{
int i;
A * a; //此时A没有定义
}A;
正确声明
typedef struct A_TAG
{
int i;
struct A_TAG * a;
}A;
结构体变量的成员可以通过点运算符访问。点运算符接受两个操作数;左操作数是结构体变量的名字,右操作数是需要访问的成员名字
#include
struct person
{
char name[10];
float height;
int age;
int weight;
};
int main()
{
struct person s1 = {"小明",1.78f,20,100}; //定义并初始化结构体变量s1
//直接访问,通过.运算符,左操作数是结构体变量s1,右操作数是成员名
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", s1.name, s1.height, s1.age, s1.weight);
s1.age = 18;
s1.height = 1.80f;
s1.weight = 120;
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", s1.name, s1.height, s1.age, s1.weight);
return 0;
}
输出
姓名:小明 身高:1.78 年龄:20 体重:100
姓名:小明 身高:1.80 年龄:18 体重:120
当拥有一个指向结构体变量的指针,可以通过对指针解引用间接访问结构体变量,并通过点运算符访问它的成员。由于点运算符优先级高于间接访问运算符,所以要在表达式中使用括号,确保间接访问运算符先执行。但是这种访问方式比较繁琐,不推荐
#include
struct person
{
char name[10];
float height;
int age;
int weight;
};
int main()
{
struct person s1 = {"小明",1.78f,20,100}; //定义并初始化结构体变量s1
struct person * p = &s1; //声明结构体指针变量p并指向结构体变量s1的地址
//(*p)获取结构体变量s1,并通过点运算符访问成员
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", (*p).name, (*p).height, (*p).age, (*p).weight);
(*p).age = 18;
(*p).height = 1.80f;
(*p).weight = 120;
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", (*p).name, (*p).height, (*p).age, (*p).weight);
return 0;
}
输出
姓名:小明 身高:1.78 年龄:20 体重:100
姓名:小明 身高:1.80 年龄:18 体重:120
当拥有一个指向结构体变量的指针,可以通过箭头运算符访问它指向的结构体变量的成员,,这种是推荐访问方式。箭头运算符接受两个操作数,左操作数必须是一个结构体指针,箭头运算符对左操作数执行间接访问取得指针指向结构体变量,然后和点运算符一样,右操作数是一个结构体成员名。本质上箭头运算符《=》间接访问运算符+点运算符
#include
struct person
{
char name[10];
float height;
int age;
int weight;
};
int main()
{
struct person s1 = {"小明",1.78f,20,100}; //定义并初始化结构体变量s1
struct person * p = &s1; //声明结构体指针变量p并指向结构体变量s1的地址
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", p->name, p->height, p->age, p->weight);
p->age = 18; //首先对p解引用找到指向结构体变量并通过箭头运算符内置的点运算符访问成员名
p->height = 1.80f;
p->weight = 120;
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", p->name, p->height, p->age, p->weight);
return 0;
}
输出
姓名:小明 身高:1.78 年龄:20 体重:100
姓名:小明 身高:1.80 年龄:18 体重:120
void print1(struct person s)
{
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", s.name, s.height, s.age, s.weight);
}
当使用传值调用时,结构体变量的一份副本做为参数传递给函数,当这个结构体变量比较大时,会有时间和空间上较大开销
void print1(struct person * p)
{
printf("姓名:%s 身高:%.2f 年龄:%d 体重:%d\n", p->name, p->height, p->age, p->weight);
}
这次传递给函数的是一个指向结构体的指针,指针大小比结构体变量小的多,所以开销较小。向函数传递结构体指针的缺陷在于函数可以修改指向的结构体变量,如果不希望如此,函数形参应该用const关键字来防止被修改,如:const struct person * p
说明:对于结构体传参我们推荐使用传递结构体指针方式,只有当结构体变量大小小于指针时,使用传递结构体变量效率更高,但这种情况非常少见
当问你struct A
类型结构体大小时,你可以会脱口而出是6个字节,因为char类型为1字节,int类型为4字节。但其实结果是12字节。你可能会很诧异,但你学习了结构体内存对齐规则后就不感到意外了
struct A
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
输出
12
偏移量:当前地址距离结构体变量内存起始地址相差的字节数
结构体内存对齐规则:
1. 结构体第一个成员存放在偏移量为0的地址处。
2. 其他成员存放在偏移量为对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
3. 结构体总大小为最大对齐数(每个成员有一个对齐数)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
根据对齐规则1:c1是结构体的第一个成员,所以存放在偏移量0位置,并且c1是char类型所以占1字节
根据对齐规则2:编译器默认对齐数为8,int类型大小为4,所以对齐数为4,即成员i存放在偏移量为4的倍数地址处,并且i是int类型所以占4字节
根据对齐规则2:编译器默认对齐数为8,char大小类型为1,所以对齐数为1,即成员c2存放在偏移量为1的倍数地址处,并且c2是char类型所以占1字节
当前结构体已占9字节大小,根据对齐规则3:结构体总大小为最大对齐数(即i的对齐数4)的整数倍,所以还要用3个字节大小空间,即结构体struct A类型大小为12字节
根据对齐规则1:d是结构体的第一个成员,所以存放在偏移量0位置,并且d是double类型所以占8字节
根据对齐规则2:编译器默认对齐数为8,char类型大小为1,所以对齐数为1,即成员c存放在偏移量为1的倍数地址处,并且c是char类型所以占1字节
根据对齐规则2:编译器默认对齐数为8,int类型大小为4,所以对齐数为4,即成员i存放在偏移量为4的倍数地址处,并且i是int类型所以占4字节
当前结构体已占16字节大小,根据对齐规则3:结构体总大小为最大对齐数(即d的对齐数8)的整数倍,所以结构体struct B类型大小为16字节
测试环境编译器默认对齐数为8
根据对齐规则1:d是struct B 结构体的第一个成员,所以存放在偏移量0位置,并且d是float类型所以占4字节
根据对齐规则4:a一个结构体变量,a中c成员的对齐数为1,a中i成员对齐数为8。所以结构体变量a最大对齐数为8,所以存放在偏移量为8的倍数地址处,并且a是struct A结构体类型所以占16字节
当前结构体struct B 已占24字节大小,根据对齐规则3:结构体总大小为最大对齐数(即成员a中的成员i的对齐数8)的整数倍,所以结构体struct B类型大小为24字节
1. 性能原因
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以2字节,4字节,8字节,16字节甚至32字节为单位来存取内存,当一个处理器以4字节存取int类型变量,该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以在任意地址处存放,现在一个int变量假设从内存地址1处开始存放,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作。现在有了内存对齐的,int类型只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
2. 移植原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如一个硬件只能访问内存地址为4的倍数处地址,不使用内存对齐有些数据访问不到
结构体的内存对齐是拿空间来换取时间的做法
在设计结构体的时候,我们既要满足对齐,又要节省空间,让占用空间小的成员尽量集中在结构体内的前面
#include
struct A
{
char c1;
int i;
char c2;
};
struct B
{
char c1;
char c2;
int i;
};
int main()
{
printf("A:%d\n", sizeof(struct A));
printf("B:%d\n", sizeof(struct B));
return 0;
}
输出
A:12
B:8
可以看到结构体类型struct A与struct B内的成员一模一样,但是struct B中将成员大小较小的c1、c2放在前面,较大的i放在后面就省下4字节大小
虽然使用1.9.3的小技巧可以节省空间,但由于结构体内存对齐或多或少会浪费一些空间,我们可以通过修改编译器默认对齐数方式继续节省空间
可以使用#pragma pack()
预处理指令更改编译器默认对齐数
说明:
#pragma pack()
使用编译器的默认对齐数
#pragma pack(value)
将编译器默认对齐数改为value
#include
#pragma pack(1) //将编译器默认对齐数设置为1
struct A
{
char c1;
int i;
char c2;
};
#pragma pack() //将编译器的对齐数从1恢复到默认值
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
输出
6
因为struct A变量在内存分配空间时,默认对齐数设置为1,所以不存在为了对齐而浪费空间情况,此时struct A大小为6字节
虽然通过结构体内存对齐规则可以手动计算出一个成员在结构体中的偏移量,但是非常麻烦。C语言标准提供了一个宏 offsetof
用于计算一个成员在结构体的偏移量,需要引用头文件stddef.h
功能:
该宏以函数形式返回成员在结构体或联合类型中的字节偏移值。返回的值是size_t类型的无符号整型值,包含指定成员与其结构体开头之间的字节数
offsetof (type,member)
type :结构体类型,不能是结构体变量
member :成员名
返回值:返回成员在结构体(或联合体)中的偏移量
#include
#include
struct A
{
char c1;
int i;
char c2;
};
int main()
{
printf("成员 c1 在结构体中偏移量为: %u\n", offsetof(struct A, c1));
printf("成员 i 在结构体中偏移量为: %u\n", offsetof(struct A, i));
printf("成员 c2 在结构体中偏移量为: %u\n", offsetof(struct A, c2));
return 0;
}
输出
成员 c1 在结构体中偏移量为: 0
成员 i 在结构体中偏移量为: 4
成员 c2 在结构体中偏移量为: 8
信息的存取一般以字节为单位。实际上,有时存储一个信息不必用一个或多个字节,例如,“真”或“假”用0或1表示,只需1位(二进制位,1 bit大小)即可。但是C语言数据类型最小的char类型也有1字节大小,剩余的7位被浪费掉了。C语言允许在一个结构体中以位为单位来指定其成员所占内存大小,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
位段优点:
1. 可以节省储存数据的空间,当一个成员可以用1 位(bit)存取时就不需要用char类型(1 字节)储存
2. 位段可以很方便访问一个整形值的部分内容,而避免使用移位运算符实现
位段的声明和结构体是类似的,但有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int、char
2.位段的成员名后边有一个冒号和一个数字
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
本例中编译器是从左向右分配。当一个字节包含两个位段,第2个位段成员比较大,无法容纳于第一个位段剩余的位时,此字节剩余位舍弃,开辟1个新的字节存放第2个位段
int
位段被当成有符号数还是无符号数是不确定的,需要显著声明为signed int
或unsigned int
相对于结构体,位段可以达到同样的效果,并且可以节省空间,但是由于C语言标准对于位段具体实现细节没有定义,所以不同编译器实现可能有所不同,所以不具有移植性