文章目录
一、结构基础知识
1、概念
引入结构的概念:数据经常以成组的形式存在。例如,雇主必须明了每位雇员的姓名、年龄和工资。如果这些值能够存储在一起,访问起来会简单一些。但是,如果这些值的类型不同(就像现在这种情况),它们无法存储于同一个数组中。在C中,使用结构可以把不同类型的值存储在一起
结构与数组是c的唯二聚合数据类型,但二者有着显著的区别,比如:数组元素可以通过下标访问,而结构中并不能这么做(原因在于:结构变量在表达式中被使用时无法被替换成指针)
2、声明
struct data{
int year;
char month;
float day;
} x;
这个声明创建了一个名叫x的变量,它包含三个成员:一个整数、一个字符和一
个浮点数,同时data为成员列表的名字,后续依旧可以使用。
struct data{
int year;
char month;
float day;
};
这里并没有定义变量,仅仅只是一个标签。
struct{
int year;
char month;
float day;
} stu1,stu2;
若后续不再需要新的结构体变量,只要stu1与stu2,也可以如上图所示。
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,但是事实上内存间存在“裂缝”(详情在堆、栈揭晓)。
值得一提的是,不同结构体中的变量名可以相同。
3、赋值
#include <stdio.h>
int main(){
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
return 0;
}
结果如下:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
另外,struct也可以进行整体赋值
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };
在外面声明可以在其他函数下直接使用,比如:
#include <stdio.h>
struct student //结构体类型的说明与定义分开。声明
{
int age; /*年龄*/
float score; /*分数*/
char sex; /*性别*/
};
int main ()
{
struct student a={ 20,79,'f'}; //定义
printf("年龄:%d 分数:%.2f 性别:%c\n", a.age, a.score, a.sex );
return 0;
}
4、结构成员的访问
直接访问
结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数,左操作
数就是结构变量的名字,右操作数就是需要访问的成员的名字。这个表达式的结果
就是指定的成员。例如,考虑下面这个声明
struct COMPLEX comp;
名字为a的成员是一个数组,所以表达式comp.a就选择了这个成员。这个表达
式的结果是个数组名,所以你可以把它用在任何可以使用数组名的地方。
类似地,成员s是个结构,所以表达式comp.s的结果是个结构名,它可以用于任何可以使用
普通结构变量的地方。
尤其是,我们可以把这个表达式用作另一个点操作符的左操
作符,如(comp.s).a,选择结构comp的成员s(也是一个结构)的成员a。点操作符
的结合性是从左向右,所以我们可以省略括号,表达式comp.s.a表示同样的意思。
间接访问
如果你拥有一个指向结构的指针,你该如何访问这个结构的成员呢?首先就是
对指针执行间接访问操作,这使你获得这个结构。然后你使用点操作符来访问它的
成员。
这样做确实很清晰,不过有个潜在问题,点操作符的优先级高于间接访问操作符,所以你必须在表达式中使用
括号,确保间接访问首先执行。
例如:
void func( struct COMPLEX *cp );
(*cp).f //访问这个变量所指向的结构的成员f
由于这个概念有点惹人厌,所以C语言提供了一个更为方便的操作符来完成这项
工作——->操作符(也称箭头操作符)。和点操作符一样,箭头操作符接受两个操
作数,但左操作数必须是一个指向结构的指针。箭头操作符对左操作数执行间接访
问取得指针所指向的结构,然后和点操作符一样,根据右操作数选择一个指定的结
构成员。但是,间接访问操作内建于箭头操作符中,所以我们不需要显式地执行间
接访问或使用括号。这里有一些例子,像前面一样使用同一个指针。
cp->f
cp->a
cp->s
5、初始化
结构的初始化方式和数组的初始化很相似。一个位于一对花括号内部、由逗号
分隔的初始值列表可用于结构各个成员的初始化。这些值根据结构成员列表的顺序
写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。
结构中如果包含数组或结构成员,其初始化方式类似于多维数组的初始化。一
个完整的聚合类型成员的初始值列表可以嵌套于结构的初始值列表内部。这里有一
个例子:
struct init_Ex{
int a;
short b[10];
Simple c;
} x = {
10,
{1, 2, 3, 4, 5},
{25, 'x', 29}
};
6、结构的存储分配
结构在内存中是如何实际存储的呢?
我们想象的模样肯能与下文(二)例子的图一致,但事实上并不准确。
编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。
以下面这个例子为例:
struct ALIGN {
char a;
int b;
char c;
};
如果某个机器的整型值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这一个结构在内存中的存储将如下所示:
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。因此,成员a(最左边的那个方框)必须存储于一个能够被4整除的地址。结构的下一个成员是一个整型值,所以它必须跳过3个字节(用灰色显示)到达合适的边界才能存储。在整型值之后是最后一个字符。
如果声明了相同类型的第2个变量,它的起始存储位置也必须满足4这个边界,所以第1个结构的后面还要再跳过3个字节才能存储第2个结构。因此,每个结构将占据12个字节的内存空间但实际只使用其中的6个,这个利用率可不是很出色。
你可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。例如,下面这个结构:
struct ALIGN2 {
int b;
char a;
char c;
};
所包含的成员和前面那个结构一样,但它只占用8个字节的空间,节省了33%。两个字符可以紧挨着存储,所以只有结构最后面需要跳过的两个字节才被浪费。
节省空间是非常重要的一件事情,当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就比程序的可读性更为急迫。在这种情况下,在声明中增加注释可能避免可读性方面的损失。
二、结构,指针与成员
声明一个结构体,如图:
它的内存分布情况如下:
1、访问指针与结构
使用一个声明:
可以看出,px指向x的内容
现在考虑表达式px + 1。这个表达式并不是一个合法的左值,因为它的值并不存储于任何可标识的内存位置。px的结果是一个结构。C语言并没有定义结构和整型值之间的加法运算。
如果px指向一个结构数组的元素,这个表达式将指向该数组的下一个结构。但就算如此,这个表达式仍然是非法的,因为我们没办法分辨内存下一个位置所存储的是这些结构元素之一还是其他东西。编译器无法检测到这类错误,所以你必须自己判断指针运算是否有意义。
但表达式(px + 1)又如何呢?如果x是一个数组的元素,这个表达式表示它后面的那个结构。但是,x是一个标量,所以这个表达式实际上也是非法的。
2、访问成员
数值
表达式px->a的右值是:
->操作符对px执行间接访问操作(由实线箭头提示),它首先得到它所指向的结构,然后访问成员a。当你拥有一个指向结构的指针但又不知道结构的名字时,便可以使用表达式px->a。如果你知道这个结构的名字,你也可以使用功能相同的表达式x.a。
在此,我们稍作停顿,相互比较一下表达式px和px->a。在这两个表达式中,px所保存的地址都用于寻找这个结构。但结构的第1个成员是a,所以a的地址和结构的地址是一样的。这样px看上去是指向整个结构,同时指向结构的第1个成员:毕竟,它们具有相同的地址。但是,这个分析只有一半是正确的。尽管两个地址的值是相等的,但它们的类型不同。变量px被声明为一个指向结构的指针,所以表达式px的结果是整个结构,而不是它的第1个成员。
让我们创建一个指向整型的指针。
int *pi;
我们能不能让pi指向整型成员a?如果pi的值和px相同,那么表达式*pi的结果
将是成员a。但是,表达式**pi = px;**是非法的,不过可以使用强制类型转换:
pi = (int *)px;
当然这样有安全隐患,可以有更安全的代码:
pi = &px->a;
->操作符的优先级高于&操作符的优先级,所以这个表达式无需使用括号,这样做一步到位。
数组成员
指向b,直接这样写是非法的:
px->b
应当这样访问:
px->b[1]
这样做才能精准读取到数组成员。
嵌套结构
为了访问本身也是结构的成员c,我们可以使用表达式px->c。它的左值是整个结构。
具体到特定成员,比如px->c.a:
若是px->c.b,则默认指向数组b第一个元素值(-1).
指针成员
表达式px->d的结果正如所料——它的右值是0,它的左值是它本身的内存位置。
不过对于*px ->d ,它指向的是空(NULL)指针。对一个NULL指针进行解引用操作是个错误,但有些环境不会在运行时捕捉到这个错误。在这些机器上,程序将访问内存位置零的内容,把它也当作是结构成员之一,如果系统未发现错误,它还将高高兴兴地继续下去。这个例子说明了对指针进行解引用操作之前检查一下它是否有效是非常重要的。
让我们创建另一个结构,并把x.d设置为指向它。
Ex y;
x.d = &y;
现在我们可以对表达式*px->d求值。
成员d指向一个结构,所以对它执行间接访问操作的结果是整个结构。这个新的结构并没有显式地初始化,所以在图中并没有显示它的成员的值。
正如你可能预料的那样,这个新结构的成员可以通过在表达式中增加更多的操作符进行访问。我们使用箭头操作符,因为d是一个指向结构的指针。
于是,你就可以写出下面的表达式:
px->d->a
px->d->b
px->d->c
px->d->c.a
px->d->c.b[1]
这样就回到了上面所描述的情况。
三、位段
位段是定义 结构体 (或联合体)中的成员变量所占的空间,含有位段的结构体 (联合体)称为位段结构。
位段的声明和结构类似,但它的成员是一个或多个位的字段。这些不同长度的字段实际上存储于一个或多个整型变量中。
位段的声明和任何普通的结构成员声明相同,但有两个例外。首先,位段成员必须声明为int、signed int或unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。
使用位段的情况:
1.int位段被当作有符号数还是无符号数。
2.位段中位的最大数目。许多编译器把位段成员的长度限制在一个整型值的长度之内,所以一个能够运
行于32位整数的机器上的位段声明可能在16位整数的机器上无法运行。
3.位段中的成员在内存中是从左向右分配的还是从右向左分配的。
4.当一个声明指定了两个位段,第2个位段比较大,无法容纳于第1个位段剩余的位时,编译器有可能把
第2个位段放在内存的下一个字,也可能直接放在第1个位段后面,从而在两个内存位置的边界上形成重叠。
使用位段的两个好处:
它能够把长度为奇数的数据包装在一起,节省存储空间。当程序需要使用成千上万的这类结构时,这种节省方法就会变得相当重要;
另一个使用位段的理由是由于它们可以很方便地访问一个整型值的部分内容。
(它常出现于操作系统中)
以一个寄存器为例:
前5个位段每个都占1位,其余几个位段则更长一些。在一个从右向左分配位段的机器上,下面这个声明允许程序方便地对这个寄存器的不同位段进行访问。
假如磁盘寄存器是在内存地址 0xc0200142进行访问的,我们可以声明下面的指针常量:
#define DISK_REGISTER \
((struct DISK_REGISTER_FORMAT *)0xc0200142)
做了这个准备工作后,实际需要访问磁盘寄存器的代码就变得简单多了,如下面的代码段所示。
/*
** 告诉控制器从哪个扇区哪个磁道开始读取。
*/
DISK_REGISTER->sector = new_sector;
DISK_REGISTER->track = new_track;
DISK_REGISTER->command = READ;
/*
** 等待,直到操作完成(ready变量变成真)。
*/
while( ! DISK_REGISTER->ready )
;
/*
** 检查错误。
*/
if( DISK_REGISTER->error_occurred ) {
switch( DISK_REGISTER->error_code ) {
...
使用位段只是基于方便的目的。任何可以用位段实现的任务都可以使用移位和屏蔽来实现。(以后再进行学习介绍)
四、联合
概念上,与结构相同,也是由多个不同类型的数据成员组成的复合数据类型。
联合的声明和结构类似,但它的行为方式却和结构不同。但是,联合体的所有成员共享同一块内存,它们的地址是相同的。因此,联合体的大小等于最大成员的大小。当你想在不同的时刻把不同的东西存储于同一个位置时,就可以使用联合。
下面,通过一个例子来详细介绍联合的使用。
union {
float f;
int i;
} fi;
在一个浮点型和整型都是32位的机器上,变量fi只占据内存中一个32位的字。如果成员f被使用,这个字就作为浮点值访问;如果成员i被使用,这个字就作为整型值访问。
fi.f = 3.14159;
printf("%d\n", fi.i );
首先把π的浮点表示形式存储于fi,然后把这些相同的位当作一个整型值打印
输出。注意这两个成员所引用的位相同,仅有的区别在于每个成员的类型决定了这
些位被如何解释。
BASIC解释器的任务之一就是记住程序所使用的变量的值。BASIC提供了几种不同类型的变量,所以每个变量的类型必须和它的值一起存储。这里有一个结构,用于保存这个信息,但它的效率不高。所以,联合就有了它的就业空间,联合就可以减少这种浪费,它把这三个值字段的每一个都存储于同一个内存位置。这三个字段并不会冲突,因为每个变量只可能具有一种类型,这样在某一时刻,联合的这几个字段只有一个被使用。
变体记录
让我们讨论一个例子,实现一种在Pascal和Modula中被称为变体记录
的东西。从概念上说,这就是我们刚刚讨论过的那个情况——内存中某个特定的区域将在不同的时刻存储不同类型的值。但是,在现在这个情况下,这些值比简单的整型或浮点型更为复杂。它们的每一个都是一个完整的结构。
下面这个例子取自一个存货系统,它记录了两种不同的实体:零件
和装配件
。零件就是一种小配件,从其他生产厂家购得。它具有各种不同的属性如购买来源、购买价格等。装配件是我们制造的东西,它由一些零件及其他装配件组成。
接下来的存货(inventory)记录包含了每个项目的一般信息,并包括了一个联合,或者用于存储零件信息,或者用于存储装配件信息。
这里有一些语句,用于操作名叫rec的INVREC结构变量。
尽管并非十分真实,但这段代码说明了如何访问联合的每个成员。语句的第1部分获得成本(cost)值和零件的供应商(supplier),语句的第2部分获得一个装配件中不同零件的编号以及第1个零件的数量。
在一个成员长度不同的联合里,分配给联合的内存数量取决于它的最长成员的长度。这样,联合的长度总是足以容纳它最大的成员。如果这些成员的长度相差悬殊,当存储长度较短的成员时,浪费的空间是相当可观的。在这种情况下,更好的方法是在联合中存储指向不同成员的指针而不是直接存储成员本身。所有指针的长度都是相同的,这样就解决了内存浪费的问题。当它决定需要使用哪个成员时,就分配正确数量的内存来存储它。
初始化
与结构一致,比如说:
union {
int a;
float b;
char c[4];
} x = {5};
即把x.a初始化为5。
我们不能把这个类量初始化为一个浮点值或字符值。如果给出的初始值是任何其他类型,它就会转换(如果可能的话)为一个整数并赋值给x.a。