1 结构体
1.1 什么是结构体?
结构体是C语言中的一种数据类型,它有如下特点
1) 结构体是一种构造数据类型
2) 把不同类型的数据组合成一个整体来自定义数据类型
1.2 结构体的定义方式与结构体变量的声明
1) 定义形式
定义结构体类型的同时定义结构体变量,形式如下
struct 结构体名
{
类型标识符 成员名;
类型标识符 成员名;
…………….
}变量名表列;
2) 几种定义方式
结构体一般分为有名结构体、匿名结构体。有如下几种定义方式
a) 方式1
struct Student
{
int num;
char name[30];
}s1;
或
struct Worker
{
char name[20];
int salary;
};
b) 方式2 匿名结构体
struct {
int id;
char level[20];
}vip;
c) 方式3,使用define
#define ds struct DefineStruct
ds
{
int i;
};
d) 方式4 使用typedef
typedef struct date
{
int year;
int month;
int day;
} Date;
// Date date = {2016,12,4};
注意:此时的Date是类型名而不是变量名
错误示例
typedef struct
{
Date* date; // 直到末尾Date才定义,此处引用时还未定义
int year;
int month;
int day;
} Date;
改造如下:
typedef struct _date
{
struct _date* d;
int year;
int month;
int day;
} Date;
3) 结构体类型与结构体变量
-
结构体类型与结构体变量概念不同
1) 类型:不分配内存;变量:分配内存 2) 类型:不能赋值、存取、运算等操作,变量可以
- 结构体可嵌套,但防止递归嵌套
- 结构体成员名可与程序中变量名相同,不会造成混淆
1.3 结构体变量的初始化
有如下几种初始化形式:
a) 形式一
struct 结构体名
{
类型标识符 成员名;
类型标识符 成员名;
…………….
};
struct 结构体名 结构体变量={初始数据};
例 struct person
{ int num;
char name[20];
char sex;
int age;
char addr[30];
};
struct person per={1,"XX",'M',19, "YY"};
b) 形式二
struct 结构体名
{
类型标识符 成员名;
类型标识符 成员名;
…………….
}结构体变量={初始数据};
例 struct person
{ int num;
char name[20];
char sex;
int age;
char addr[30];
}per={1,"XX",'F',18,"YY"};
c) 形式三
struct
{
类型标识符 成员名;
类型标识符 成员名;
…………….
}结构体变量={初始数据};
例 struct
{ int num;
char name[20];
char sex;
int age;
char addr[30];
}per={2,"AA",'M',18,"YY"};
1.4 结构体变量的引用
3种引用成员形式
1) 结构体变量名.成员名
2) 结构体指针名->成员名;
3) (*结构体指针名).成员名;
等价关系描述如下:
1.4.1 引用规则
a) 结构体变量不能整体引用,只能引用变量成员。引用方式如上的第一种,即 结构体变量名.成员名。
例 :
struct person
{
int num;
char name[20];
char sex;
int age;
char addr[30];
}p1,p2;
p1={1,"XX",'F',19,"YY"}; ( 错)
printf(“%d,%s,%c,%d,%f,%s\n”,p2); ( 错)
if(p1==p2) (错,表达式必须包含算术或指针类型 )
……
p1.num=10; // 赋值
p2.age++;
b) 结构体变量间相互赋值
C语言不允许使用一个数组直接为另一个数组赋值,但使用一个结构体变量为另一个结构体变量赋值是合法的,可以使用赋值操作符(=)将一个结构变量赋值给另一个结构变量,如下是正确的:
p2 = p1; ( √ )
c) 结构体嵌套时逐级引用
例 struct person
{
int age;
char name[20];
struct
{ int month;
int day;
int year;
}date ;
}p1,p2;
p1.date.month=12;
d) 结构体的自引用
// 错误使用
struct MyStruct
{
int a;
struct MyStruct my; // 形成了递归引用
};
// 正确使用
struct MyStruct
{
int a;
struct MyStruct* my; // 使用指向结构体的指针
};
1.5 结构体嵌套与匿名结构体
结构体嵌套就是“结构体中有另一个结构体”,某个结构的数据成员也是一个结构体变量,这样可以按层次结构合理组织数据。如下示例:
struct student
{
char name[20];
struct course /*结构体course的定义*/
{
int math;
int English;
}c; /*声明结构体变量c*/
};
匿名结构体
C语言允许定义匿名结构,所谓匿名结构,就是不指定结构体的名称,但一定要在结构体定义的同时声明至少一个结构体变量,否则这种用法没有实际意义,因为外部无法使用它来声明变量。
struct
{
char name[20];
int age;
}a, b ;
// 这样便声明创建了两个结构体变量a和b,可以通过诸如 a.age 等形式来访问其成员,但这种类型没有名称,因此无法在以后的程序中声明这种类型的变量。
1.6 结构体数组
1.6.1定义方式
结构体数组定义的三种形式:
形式一:
struct person
{
char name[20];
char sex;
int age;
};
struct person per[5];
形式二:
struct person
{
char name[20];
char sex;
int age;
}per[3];
形式三:
struct // 匿名
{
char name[20];
char sex;
int age;
} per[2];
- 8
1.6.2 创建结构体数组
struct person s[3] = { { 100,"XX",'M',18},{ 100,"YY",'F',19 },{ 100,"ZZ",'M',20} };
struct person* p = (struct person *)malloc(sizeof(struct person )*3); // 动态开辟
for (int i = 0; i < 3; i++)
{
p[i].num = i;
strcpy(p[i].name, "name");
p[i].sex = i % 2 == 0 ? 'M' : 'F';
p[i].age = i * 10;
}
1.6.3 结构体数组初始化与引用
1) 结构体数组初始化
分行初始化:
struct person
{
int id;
char name[20];
char sex;
int age;
};
struct person per1[3]={{1,"XX",'M',20},{2,"YY",'F',19},{3,"ZZ",'M',19}};
// 全部初始化时维数可省,如下
struct person per2[]={{1,"XX",'M',20},{2,"ZZ",'M',19}};
顺序初始化:
struct person
{
int id;
char name[20];
char sex;
int age;
};
struct person per[] = { 1,"XX",'M',20,10,"YY",'M',19 };
定义时同时初始化
例 struct person
{ int id;
char name[20];
char sex;
int age;
}per[ ]={{……},{……},{……}};
例 struct
{ int num;
char name[20];
char sex;
int age;
}per[ ]={{……},{……},{……}};
2) 引用方式
结构体数组名[下标].成员名
per[i].age += 1;
strcpy(per[1].name, "hhd"); // 改变数组的内容
for (int i = 0; i < 2; i++)
{
printf("%d,%s,%c,%d\n", per[i].id, per[i].name, per[i].sex, per[i].age);
}
1.7 结构体与指针
1) 指向结构体变量的指针
结构体指针存放结构体变量在内存的起始地址, 定义形式:struct 结构体名 *结构体指针名;
例 struct person *p;
2) 使用结构体指针变量引用成员形式
struct person per;
struct person *p=&per; // p指向 per
per.id =1; // 或 (*p).id =1;
使用示例:
struct student
{
long int num;
char name[20];
char sex;
float score;
struct student_info // 结构体嵌套
{
int age;
char adress[20];
}info;
struct student_info* _info; // 结构体指针
}stu1, *p;
p = &stu1; // p 指向 stu1
struct student_info _info = {16,"aaaaa"};
p->_info = &_info;
stu1.num = 101;
strcpy(stu1.name, "XX");
p->sex = 'M';
(*p).score = 89.5;
p->info.age = 18;
strcpy(p->info.adress,"yyyyyy" );
printf("\nNo:%ld\nname:%s\nsex:%c\nscore:%f\nage=%d\nadress=%s\n",
(*p).num, p->name, stu1.sex, p->score,p->info.age,p->info.adress);
printf("\n_info.age=%d\n_info.adress=%s\n", p->_info->age, p->_info->adress); // 访问结构体指针所指向的内容
3) 指向结构体数组的指针
struct person
{
int num;
char name[20];
char sex;
int age;
}stu[3] = { { 001,"AAA",'M',18 },
{ 002,"BBB",'M',19 },
{ 003,"CCC",'F',20 } };
struct person *p = stu; // 指向结构体数组首地址
for (p = stu; p<stu + 3; p++) // 指针每加一,移动一个结构体大小
{ printf("No.=%d,name=%s,sex=%c,age=%d\n", p->num, p->name, p->sex, p->age);
}
1.8 结构体变量、指针作函数参数
对比:
1) 用结构体变量的成员作参数----值传递
2) 用结构体变量作参数----多值传递,效率低
3) 用指向结构体变量或数组的指针作参数----地址传递
4) 使用结构体数组作参数, 地址传递
示例:
// 结构体定义如下
typedef struct date
{
int year;
int month;
int day;
} Date;
使用指向结构体变量或数组的指针作参数
// 指向结构体变量的指针做参数
void changeDate(Date* d)
{
d->day += 1;
d->year -= 1;
d->month -= 1;
}
使用结构体数组做参数
// 使用结构体数组做参数
void changeArr(Date date[])
{
for (int i = 0; i < 2; i++)
{
date[i].day -= 1;
date[i].month -= 1;
date[i].year -= 1;
}
}
用结构体变量作参数
// 值传递,无法改变内容,即使里面有数组元素
void change_struct(Date date)
{
date.day += 1;
date.month += 1;
date.year += 1;
strcpy(date.des, "nice");
printf("change: year=%d,month=%d,day=%d,des=%s\n", date.year, date.month, date.day, date.des);
}
1.9结构体在内存中的存储与字节对齐
结构体的大小本应该是所有成员的类型大小之和,但因为引入字节对齐的缘故,导致结构体真实的大小一般大于等于所有成员大小之和。成员在结构体中的位置对结构体的大小可能会造成影响。
对于MyStruct1,内存分配如下:
1.9.1 结构体字节对齐机制
出于效率的考虑,C语言引入了字节对齐机制,结构体变量占据的内存单元的个数应当大于等于其内部所有数据成员占据内存单元数的和。
一般来说,不同的编译器字节对齐机制有所不同,但还是有以下3条通用准则:
(1) 结构体的大小能够整除其最宽基本类型成员的大小;
(2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
(3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
说明:
字节对齐第(3)条准则提及最宽基本类型的概念,所谓基本类型是指像char、short、int、float、double这样的内置数据类型。“数据宽度”就是指其sizeof的大小。诸如结构体、共用体和数组等都不是基本数据类型。
注意:如下示例都是在32位编译环境下测试的。
示例:
struct MyStruct1
{
char ch1;
char ch2; // 两者不足4,填充
int i1; // 4
int i2; // 4
};
sizeof(struct MyStruct1) // 12
struct MyStruct2
{
int i1; // 4
char ch1;
char ch2; // 两者不足4,填充
char chs[11]; // 12
};
sizeof(struct MyStruct2) // 20
struct MyStruct3
{
char ch1; //4
int i; // 4
char chs[9]; // 12
};
sizeof(struct MyStruct3) // 20
struct MyStruct4
{
char ch1; // 8
double db; // 8
int i; // 4
char chs[9]; // 12
};
sizeof(struct MyStruct4) // 32
struct MyStruct5
{
int i; // 4
short s; // 4
char chs[11]; // 12
};
sizeof(struct MyStruct5) // 20
1.9.2 结构体嵌套结构体
当结构体嵌套结构体时,字节对齐所参考的最宽基本成员不局限与当前结构体,可能是所包含的结构体中的最宽基本成员。
struct MyStruct7
{
int i;
char ch;
};
struct MyStruct8
{
struct MyStruct7 s; //此时参考的是MyStruct7中最宽的int
char ch;
};
printf("%d\n", sizeof(struct MyStruct8)); //输出12
1.9.3 offsetof(s,m)
在stddef.h 头文件中,可以使用offsetof来看某个元素相对结构体的起始位置偏移了多少个字节
typedef struct MyStruct6
{
char ch;
int i;
double d;
};
...
printf("%d,%d\n", sizeof(struct MyStruct6),offsetof(struct MyStruct6, i)); //输出 16,4
1.9.4 手动设置成员对齐
在vs2015中,依次点击 项目 –> 结构体 属性 –> C/C++ –> 代码生成,在如下界面的结构成员对齐选项进行设置:
例:
struct MyStruct7
{
int i;
char ch;
};
...
printf("%d\n", sizeof(struct MyStruct7));
// 当我们设置对齐字节为2字节时
// 输出为6
// 当我们设置对齐字节为8字节时
// 输出为8
注意: 当我么设置的字节数小于结构体最宽基本成员时,以设置的为主;当设置的字节数大于结构体最宽成员时,以结构体最宽基本成员的大小为主:总结即是两者按最小的来计算
1.10 位域(bit field)
位域一般用于限制结构体成员的大小从而起到节约内存的作用,由于涉及到类型的大小,相应的会影响软件的移植性。
形式:
struct 位域结构名
{
位域列表
};
其中位域列表的形式为: 类型说明符 位域名:位域长度;
在C99中规定int、unsigned int和bool(C99新增关键字)可以作为位域类型,但不同编译器在实现时对此都作了相应扩展。使用Dev C++ IDE C99下测试,可以使用于位域的类型有char,short,int,long,bool,而浮点类型float、double则不在支持之列。
注:C99下,使用bool需包含头文件stdbool.h( #include<stdbool.h> )
使用位域的主要目的是压缩存储,其大致规则为:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
使用的一般示例:
struct MyStruct
{
unsigned char ch1:1;
unsigned int i:4;
unsigned long l : 8;
unsigned short sh :2;
// double d:8; // 位域的类型无效
// float f : 4; // 位域的类型无效
};
使用位域案例
通过位域来操作int类型数据,打印出其在内存中的二进制表示形式:
struct bits
{
unsigned char ch1 : 1;
unsigned char ch2 : 1;
unsigned char ch3 : 1;
unsigned char ch4 : 1;
unsigned char ch5 : 1;
unsigned char ch6 : 1;
unsigned char ch7 : 1;
unsigned char ch8 : 1;
};
void main()
{
int i = 4;
struct bits* p = &i;
int len = 4;
while (len--)
{
printf("%d%d%d%d%d%d%d%d ", (p+len)->ch8, (p + len)->ch7, (p + len)->ch6, (p + len)->ch5, (p + len)->ch4, (p + len)->ch3, (p + len)->ch2, (p + len)->ch1);// p每加1移动一个字节
}
system("pause");
}
打印结果:
00000000 00000000 00000000 00000100
2 共用体
2.1共用体定义
共用体也叫联合体,与结构体类似,也是一种构造数据类型,但却有着不同的行为方式。一般至少由一种数据类型构成,所有成员引用的是内存中相同的位置。
2.2用途
在不同的时刻把不同的东西存储与相同的内存位置(使几个不同类型的变量共占一段内存(相互覆盖))
2.3 共用体变量的定义
共用体可采用如下形式定义:
union 共用体名称(或称标识)
{
存储数据列表(或称成员变量列表)
};
注意,结束花括号后的分号(};)不要遗漏,这种定义形式是一个完整的
形式一:
union data
{
int i;
char ch;
float f;
}a,b;
形式二:
union data
{
int i;
char ch;
};
union data a,b,c,*p,d[3];
形式三: 匿名
union
{
int i;
char ch;
}a,b,c;
2.4 共用体的特点
- 所有成员引用的是内存中相同的位置
- 共用体变量任何时刻只有一个成员存在;
- 共用体变量定义分配内存,长度取决于最长成员与最宽基本数据类型成员
2.5 共用体大小
原则上,共用体的大小取决于占据最多内存的成员的长度,但会受到最宽基本类型成员大小的影响。即:共用体的总大小为共用体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)
如下例:
union MyUnion
{
int i;
char chs[11];
};
void main()
{
printf("%d\n", sizeof(union MyUnion));
system("pause");
}
// 输出12
2.6 共用体变量引用
-
引用方式:
(1) 共用体变量名.成员名
(2) 共用体指针名->成员名
(3) (*共用体指针名).成员名 - 引用规则
(1) 不能引用共用体变量,只能引用其成员
(2) 共用体变量中起作用的成员是最后一次存放的成员
(3) 不能在定义共用体变量时对所有成员进行初始化
(4) 可以用一个共用体变量为另一个变量赋值
2.7 结构体变量和共用体变量内存形式的不同
结构体变量中的数据程序是并列关系,而编译器为共用体变量中的数据成员分配的是同一块内存,每个时刻只有一个数据成员有意义,从地址的角度来看两者的差异,形象地表明了这一点。
2.8 结构体与共用体的相互嵌套
某些情况下需要嵌套使用以满足各种需求,如下示例:
union MyUnion2
{
int i;
struct MyStruct
{
int i;
char ch;
} s;
};
struct MyStruct
{
int type;
union
{
int i;
float f;
char *str;
}value;
};
需要注意的是,嵌套后的结构体或共用体的大小仍遵循字节对齐。
2.9 共用体变量的初始化
在声明一个共用体变量的同时,可以完成其初始化,与结构变量的初始化不同的是,只能对共用体变量列表中的一个变量进行初始化。
对共用体MyUnion来说,下列语句是合法的:
union MyUnion mu = { 1 }; // 默认初始化第一个成员
union MyUnion mu2 = { .chs = "hello" }; // 指定要初始化的成员
与结构类似,可以把共用体定义、共用体变量声明及其初始化放在一起,如:
union MyUnion
{
int i;
char chs[11];
}u = { 10 }, u2 = {.chs="niko"}; // 初始化
使用示例:
union MyUnion
{
int i;
char chs[11];
}u = { 10 }, u2 = {.chs="niko"};
void main()
{
union MyUnion mu = { 1 }; // 默认初始化第一个成员
union MyUnion mu2 = { .chs = "hello" }; // 指定要初始化的成员
printf("mu.i=%d,mu.chs=%s\n", mu.i, mu.chs); // 一个成员最后被赋值,其他的会出现垃圾值
printf("mu2.i=%d, mu2.chs=%s\n", mu2.i, mu2.chs);
u2 = u; // 赋值初始化
strcpy(u2.chs, "hello world"); // 给共用体变量成员赋值
printf("u2.i=%d,u2.chs=%s\n", u2.i, u2.chs);
printf("MyUnion = %d\n", sizeof(union MyUnion));
union MyUnion *up = &u; //定义指向共用体的指针
up->i = 100;
printf("%d\n", up->i);
system("pause");
}