C06-struct-union-bitfield

本文最后更新于:1 小时前

C结构体

在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。

结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。

定义结构

struct 语句的格式如下:

1
2
3
4
5
6
struct tag { 
member-list
member-list
member-list
...
} variable-list ;

tag 是结构体标签。

member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。

variable-list 结构变量,定义在结构的末尾,最后一个分号之前,可以指定一个或多个结构变量。下面是声明 Book 结构的方式:

1
2
3
4
5
6
7
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;

可以采取以下3种方法定义结构体类型变量:

(1)先声明结构体类型再定义变量名
(2)在声明类型的同时定义变量
(3)直接定义结构类型变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
int a;
char b;
double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;

//结构体的标签被命名为SIMPLE并且声明变量
struct SIMPLE
{
int a;
char b;
double c;
} s1;

//这个结构体并没有标明其标签
struct
{
int a;
char b;
double c;
} s1;

也可以用typedef创建新类型

1
2
3
4
5
6
7
8
typedef struct
{
int a;
char b;
double c;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;

结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct B;    //对结构体B进行不完整声明

//结构体A中包含指向结构体B的指针
struct A
{
struct B *partner;
//other members;
};

//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
struct A *partner;
//other members;
};

访问结构成员

为了访问结构的成员,我们使用成员访问运算符(.)。获取结构体成员的一般格式为:

结构体变量名.成员名; 通过这种方式可以获取成员的值,也可以给成员赋值

结构体内存大小对齐原则

  • 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。

  • 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。

结构体中成员变量分配的空间是按照成员变量中占用空间最大的来作为分配单位,同样成员变量的存储空间也是不能跨分配单位的,如果当前的空间不足,则会存储到下一个分配单位中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

typedef struct{
unsigned char a;
unsigned int b;
unsigned char c;
} debug_size1_t;

typedef struct{
unsigned char a;
unsigned char b;
unsigned int c;
} debug_size2_t;

int main(void){
printf("debug_size1_t size=%lu,debug_size2_t size=%lu\r\n", sizeof(debug_size1_t), sizeof(debug_size2_t));
return 0;
}

编译执行输出结果:

debug_size1_t size=12,debug_size2_t size=8

结构体占用存储空间,以32位机为例

1.debug_size1_t 存储空间分布为a(1byte)+空闲(3byte)+b(4byte)+c(1byte)+空闲(3byte)=12(byte)。
1.debug_size2_t 存储空间分布为a(1byte)+b(1byte)+空闲(2byte)+c(4byte)=8(byte)。

结构体数组

一个结构体变量中可以存放一组数据(如一个学生的学号,姓名,成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数据值型数组不同之处在于每个数组元素都一个结构体类型的数据,它们分别包括各个成员(分量)项。

定义结构体数组
和定义结构体变量的方法相仿,只需说明其为数组即可。

1
2
3
4
5
6
7
8
9
10
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct student stu[3];

以上定义了一个数组 stu,其元素为 struct student 类型数据,数组有 3 个元素。也可以直接定义一个结构体数组。如:

1
2
3
4
5
6
struct student
{
int num;
....

}stu[3];

1
2
3
4
5
struct
{
int num;
 ...
}stu[3];

结构体数组的初始化

与其它类型数组一样,对结构体数组可以初始化如:

1
2
3
4
5
6
7
8
9
10
11
struct student
{
int mum;
char name[20];
char sex;
int age;
float score;
char addr[30];
}stu[3] = {{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}};

定义数组 stu 时,元素个数可以不指定,即写成以下形式:

stu[] = {{...},{...},{...}};

编译时,系统会根据给出初值的结构体常量的个数来确定数组元素的个数。

结构体指针

当一个指针变量指向结构体时,我们就称它为结构体指针。C语言结构体指针的定义形式一般为:

struct 结构体名 *变量名;

下面是一个定义结构体指针的实例:

1
2
3
4
5
6
7
8
9
10
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

一个完整实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# include <stdio.h>
# include <string.h>
struct AGE
{
int year;
int month;
int day;
};
struct STUDENT
{
char name[20]; //姓名
int num; //学号
struct AGE birthday; //生日
float score; //分数
};
int main(void)
{
struct STUDENT student1; /*用struct STUDENT结构体类型定义结构体变量student1*/
struct STUDENT *p = NULL; /*定义一个指向struct STUDENT结构体类型的指针变量p*/
p = &student1; /*p指向结构体变量student1的首地址, 即第一个成员的地址*/
strcpy((*p).name, "小明"); //(*p).name等价于student1.name
(*p).birthday.year = 1989;
(*p).birthday.month = 3;
(*p).birthday.day = 29;
(*p).num = 1207041;
(*p).score = 100;
printf("name : %s\n", (*p).name); //(*p).name不能写成p
printf("birthday : %d-%d-%d\n", (*p).birthday.year, (*p).birthday.month, (*p).birthday.day);
printf("num : %d\n", (*p).num);
printf("score : %.1f\n", (*p).score);
return 0;
}

输出结果是:
name : 小明
birthday : 1989-3-29
num : 1207041
score : 100.0

从该程序可以看出:因为指针变量 p 指向的是结构体变量 student1 第一个成员的地址,即字符数组 name 的首地址,所以 p 和 (*p).name 是等价的。

但是,“等价”仅仅是说它们表示的是同一个内存单元的地址,但它们的类型是不同的。指针变量 p 是 struct STUDENT* 型的,而 (*p).name 是 char* 型的。所以在 strcpy 中不能将 (*p).name 改成 p。用 %s 进行输入或输出时,输入参数或输出参数也只能写成 (*p).name 而不能写成 p。

同样,虽然 &student1 和 student1.name 表示的是同一个内存单元的地址,但它们的类型是不同的。&student1 是 struct STUDENT* 型的,而 student1.name 是 char* 型的,所以在对 p 进行初始化时,“p=&student1”不能写成“p=student1.name”。因为 p 是 struct STUDENT* 型的,所以不能将 char* 型的 student1.name 赋给 p。

获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName
或者:
pointer->memberName

第一种写法中,.的优先级高于(*pointer)两边的括号不能少。因为成员运算符“.”的优先级高于指针运算符“”,所以如果 *p 两边的括号省略的话,那么 *p.memberName 就等价于 *(p.memberName),这样意义就完全不对了。

第二种写法中,->是一个新的运算符,称为指向运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。

以下 3 种形式是等价的:(1) 结构体变量.成员名。(2) (*指针变量p).成员名。(3) 指针变量p->成员名。

如果定义一个结构体指针变量并把结构体数组的数组名赋给这个指针变量的话,就意味着将结构体数组的第一个元素,即第一个结构体变量的地址,也即第一个结构变量中的第一个成员的地址赋给了这个指针变量.

分析以下几种运算符:

p -> n 得到 p 指向的结构体变量中的成员 n 的值
p -> n ++ 得到 p 指向的结构体变量中的成员 n 的值,用完值后使它加 1
++p -> n 得到 p 指向的结构体变量中的成员 n 的值使之加 1 (先加)

指向结构体数组的指针

指向结构体数组的指针的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#inlcude <stdlib.h>

struct student{
int num;
char name[20];
char sex;
int age;
};

struct student stu[3] = {{10101, "Li Lin", 'M', 18},
 {10102, "Zhang Fun", 'M', 19},
 {10103, "Wang Min", 'F', 20}};

int main(){
struct student *p;
printf("No. name sex age\n");
for(p=stu; p<stu+3;p++)
printf("%5d %-20s %2c %4d\n", p->num, p->name, p->sex, p->age);
system("pause");
}

运行结果如下:

1
2
3
4
No.    name        sex        age
10101 Li Lin M 18
10102 Zhang Fun M 19
10103 Wang Min F 20

注意以下两点:

(1)如果 p 的初值为 stu,即指向第一个元素,则 p + 1 后指向下一个元素的起始地址。例如:

(++p) -> num 先使 p 自加 1 ,然后得到它指向的元素中的 num 成员的值(即10102)。

(p++) ->num 先得到 p->num 的值(即10101),然后使 p 自加 1 ,指向 stu[1]。

注意以上二者的不同。

(2)程序已定义了指针 p 为指向 struct student 类型数据的变量,它只能指向一个 struct student 型的数据(p 的值是 stu 数组的一个元素的起始地址),而不能指向 stu 数组元素中的某一成员,(即 p 的地址不能是成员地址)。例如,下面是不对的:

p = &stu[1].name编译时将出错。

用结构体变量和结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

将一个结构体变量的值传递给另一个函数,有3个方法:

(1)用结构体变量的成员作参数,例如:用 stu[1].num 或 stu[2].name 作函数实参,将实参值传给形参。用法和用普通变量作实参是一样的,属于 值传递 方式。应当注意实参与形参的类型保持一致。

(2)用结构体变量作参数。老版本的C系统不允许用结构体变量作实参,ANSI C取消了这一限制。但是用结构体变量作实参时,采取的是 值传递 的方式,将结构体变量所占的内存单元全部顺序传递给形参。形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在空间和时间上开销较大,如果结构体的规模很大时,开销是很可观的,此外由于采用值传递方式,如果在执行被调用函数期间改变了形参(也是结构体变量)的值,该值不能返回主调函数,这往往造成使用上的不便。因此一般较少用这种方法。

(3)用指向结构体变量(或数组)的指针作实参,将结构体变量(或数组)的地址传给形参。

实例:指向结构体变量的指针作实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#define FORMAT "%d\n%s\n%f\n%f\n%f\n"

struct student{
int num;
char name[20];
float score[3];
}stu = {12345, "Li Li", 67.5, 89, 78.6};

void print(struct student *p){
printf(FORMAT, p->num, p->name, p->score[0], p->score[1], p->score[2]);
printf("\n");
}

void main(){
print(&stu);
}

C共用体

共用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

共用体定义

1
2
3
4
5
6
7
8
union [union tag]
{
/*成员列表*/
member definition;
member definition;
...
member definition;
} [one or more union variables];

union tag 共用体名是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。

一个共用体的实例:

1
2
3
4
5
6
union Data
{
int i;
float f;
char str[20];
} data;

共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。使用成员访问运算符(.)访问共用体的成员。

结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

共用体可用于判断大小端机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union
{
char str;
int data;
};
data=0x01020304;
if(str==0x01)
{
cout<< "此机器是大端!"<<endl;
}
else if(str==0x04){
cout<<"此机器是小端!"<<endl;
}
else{
cout <<" 暂无法判断此机器类型!"<<endl;
}

注:大端机高位存在低位,小端机反之

一些概念:

位:”位(bit)”是电子计算机中最小的数据单位。每一位的状态只能是0或1。
字节:8个二进制位构成1个”字节(Byte)”,它是存储空间的基本计量单位。1个字节可以储存1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。
字:”字”由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。例如一台8位机,它的1个字就等于1个字节,字长为8位。如果是一台16位机,那么,它的1个字就由2个字节构成,字长为16位。字是计算机进行数据处理和运算的单位。
一般的计算机都已经到了64位机 也就是说 一个基本单位就是64位,也就是8字节了。这样再综合上面的分析就不难看出,结构体,共用体,位域的定义中,按顺序分配内存,下一个字段所占大小如果超出了上一个字段占的内存单元剩余部分,那么它会重新申请下一个内存单元,而上一个多出部分将空着。

字节对齐与对齐原则:

【原则1】数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
【原则2】结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
【原则3】结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

位域

带有预定义宽度的变量被称为位域。所谓”位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
所以位域就是在结构体定义时,指定某个成员变量所占用的二进制位数(Bit)。

典型的实例:

用 1 位二进位存放一个开关量时,只有 0 和 1 两种状态。
读取外部文件格式——可以读取非标准的文件格式。例如:9 位的整数。

位域的定义

位域定义与结构定义相仿,其形式为:

1
2
3
4
struct 位域结构名 
{
位域列表
};

其中位域列表的形式为:
type [member_name] : width ;

下面是有关位域中变量元素的描述:

type:只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
member_name:位域的名称。
width:位域中位的数量。宽度必须小于或等于指定类型的位宽度。

1
2
3
4
5
struct bs{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
};

‘:’后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。

位域的几点说明:

一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

1
2
3
4
5
6
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 从下一单元开始存放 */
unsigned c:4
}

在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。

位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,’: ‘后面的数字不能超过这个长度。

位域可以是无名位域,这时它只用来作填充或调整位置。因为没有名称,无名的位域是不能使用的。例如:

1
2
3
4
5
6
struct k{
int a:1;
int :2; /* 该 2 位不能使用 */
int b:3;
int c:2;
};

从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

位域的具体存储规则如下:

  1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。

  2. 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。

  3. 如果成员之间穿插着非位域成员,那么不会进行压缩。

(1)结构体内存分配原则:

原则一:结构体中元素按照定义顺序存放到内存中,但并不是紧密排列。从结构体存储的首地址开始 ,每一个元素存入内存中时,它都会认为内存是以自己的宽度来划分空间的,因此元素存放的位置一定会在自己大小的整数倍上开始。

原则二: 在原则一的基础上,检查计算出的存储单元是否为所有元素中最宽的元素长度的整数倍。若是,则结束;否则,将其补齐为它的整数倍。

(2)定义位域时,各个成员的类型最好保持一致,比如都用char,或都用int,不要混合使用,这样才能达到节省内存空间的目的。

位域的使用
位域的使用和结构成员的使用相同,其一般形式为:

位域变量名.位域名 或 位域变量名->位域名

位域允许用各种格式输出。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!