c语言基础--结构和联合
==================
结构和联合
==================
聚合数据类型能够同时存储超过一个的单独数据
c提供了两种类型的聚合数据类型:数组和结构
数组是相同类型的元素的聚合,它的每个元素都是通过下标引用或指针间接访问来选择的。
结构也是一些值的集合,这些值成为他的成员。但一个结构的各个成员可能具有不同的类型。
每个结构成员都有自己的名字,他们是通过名字来访问的。这个区别很重要,结构不是一个它
自身成员的数组。和数组不同,当结构变量在表达式里使用时,它并不被替换成指针,结构
变量也无法使用下标来选择特定的变量,
结构变量属于标量类型,所以你可以像对待其他标量类型一样执行相同类型的操作。结构也可
以作为传递给函数的参数,它们也可以作为返回值从函数返回,
相同类型的结构变量之间可以相互赋值,你可以声明指向结构的指针,取一个结构变量的地址,
也可以声明结构数组
结构声明
在声明结构时,必须列出所包含的成员
struct {
int a;
char b;
float c;
}a;
这个声明创建一个名为a的变量,它包含3个成员一个整数,一个字符和一个浮点数。
struct {
int a;
char b;
float c;
}y[20],*p;
这个声明创建了y,p y是一个数组,它包含了20个结构。p是一个指针,它指向这个类型的结构
警告这个声明被编译器当做两种完全不同的类型,即使它们的成员是相同的,这三种类型完全不同,使用
z=&x是非法的
struct simple{
int a;
char b;
float c;
}
这个声明把标签SIMPLE和这个成员列表联系在一起。
struc simple a ,y[10],*z;这三个变量是同一个类型的
声明结构时可以使用另一种良好技巧是用typedef创建一种新的
类型如,typedef struct {
int a;
}simple;
这个技巧和声明一个结构标签的效果几乎相同,区别是simple现在是个类别名而不是个结构标签
所以后续的就是simple x;
提示
如果你想在多个源文件中使用同一个结构体,就应该把标签或者typedef形式的声明放到一个头文件中
结构成员
到目前为止只使用了简单类型的结构成员,但可以在一个结构外部声明的任何变量都可以作为结构成员,尤其
是结构成员可以是标量,数组、指针设置其他结构。
这里有一个复杂的例子:
struct COMPLEX{
float f;
int a[10];
long *fp;
struct simple s;
struct simple sa[10];
struct simple *sp;
};
结构成员的直接访问
结构变量的成员是通过 . 操作符访问的,点操作符接受两个操作数,左操作数就是结构变量的名字,右操作数就是
需要访问的成员的名字。这个表达式的结果就是指定的成员。
间接访问使用->
结构的自引用
在一个结构的内部包含一个类型为该结构体本身的成员是否合法
例子
struct ss{
struct ss d;
}
这种是违法的因为d是一个完整的结构,会一直永无止境的重复下去
struct ss{
struct ss *d;
}
这个声明和前面的差别在于d是一个指针而不是一个结构,编译器在结构长度的确定之前就已经知道指针的长度
所以这种类型的自引用是合法的
如果你觉得一个结构内部包含一个指向该结构本身的指针有些奇怪,请记住它事实上所指向的是同一种类型的不同结构。
更加高级的数据结构,如链表和树都是用这种技巧实现的。每个结构指向链表的下一个元素或树的下一个分支。
警惕下面的陷阱
typedef struct {
int a;
sl *s;
}sl;
这个声明的目的是为这个结构创建类型名sl,但是失败了,因为类型名知道声明的末尾才定义,所以在结构声明的内部还尚未定义
解决方案是定义一个结构标签来声明s
typedef stuct ss{
struct ss *s;
}ssss;
对于一个结构可以使用一个指针指向该结构,但是结构是一个标量,如果*p+1和*(p+1)都是非法的。因为p现在是一个结构,c语言没有定义结构和
整型值的加法运算,p是一个标量,作为一个标量而言,后面的表达式是非法的。
->操作符对结构指向间接访问。
表达式*p和p->a:
在这个表达式中p保存的是地址适用于寻找这个结构。但结构的第一个成员是a,所以a的地址和结构的地址是相同的,这样p看上去是指向一个结构
其实是指向结构的第一个成员,毕竟他们具有相同的地址,但是这个分析只有一半是正确的。尽管他们的地址相同,但是他们的类型不同,变量p是一个指向结构
的指针,所以表达式*p的结果是整个结构,而不是它的第一个成员。
让我们创建一个指向整型指针pi,int *pi;能不能让pi指向整型成员a呢?如果pi的值和p相同,那么表达式*pi就是成员a的值
但是表达式pi=p;是非法的,因为他们的类型不同,但是使用类型强制转换就能奏效;
pi=(int *)p;但是这种方法是危险的因为他避过了编译器的类型检查。正确的表达式更为简单-使用&操作符取得一个指针p->a的指针
pi=&p->a
->的优先级要比&的优先级高所以这个表达式无需使用括号
表达式p->b的值是一个指针常量,因为b是一个数组。所以这个表达式不是一个合法的左值。
如果我们对这个表达式执行间接访问操作,它将访问数组的第一个元素,使用小标引用或者指针运算我们还可以访问数组的其他元素
访问嵌套的结构
为了访问本身也是结构的成员c,我们可以使用p->c它的左值是整个结构,这个表达式可以使用点操作符访问c的特定成员。例如,
表达式p->c.a具有下面的右值,这个表达式既包含了点操作符,也包含了箭头操作法,之所以使用箭头操作符,是因为px并不是一个结构
而是一个指向结构的指针。接下来之所以要使用点操作符是因为p->c的结果并不是一个指针而是一个结构
这里有一个更为复杂的表达式:
*p->c.b
如果你 逐步对它进行分析,这个表达式还是比较容易弄懂的。它有三个操作法,首先执行的是箭头操作法。px->c的结果是结构c
使用.b访问结构c的成员b,b是一个数组,所以p->b.c的结果是一个指针,它指向数组的第一个元素。最后对这个指针执行间接访问,所以表达式的最终结果是数组的第一个元素
访问指针成员
表达式px->d的结果正如你所料----它的右值是0.它的左值是它本省的内存位置。表达式*p->d更为有趣。这里间接访问操作符作用于
成员d所存储的指针值。,但d包含了一个NULL指针,所以他不指向任何东西。对一个NULL指针进行解引用操作是个错误。这个例子说明了对指针
进行解引用操作之前检查一下他是否有效是非常重要的。
让我们创建另一个结构,并把x.d设置为指向他。
EX y;
x.d = &y;
现在我们可以使用表达式*p->d 求值。
成员d指向一个结构,所以对它执行间接访问操作的结果是整个结构,这个新的结构并没有显式地初始化,所以在图中并没有显示他的成员的值
======================
结构的存储分配
======================
结构在内存中是如何实际存储的呢?
编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正确的边界对齐要求是,成员之间才可能出现用于填充的
额外内存空间。
例子
struct ALLGN{
char a;
int b;
char c;
};
如果某个机器的整形值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这个结构在内存中的存储将如何
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始位置必须是结构中边界要求最严格
的数据类型所要求的位置。因此,成员a必须存储一个能够被4整除的地址,结构的下一个成员是一个整型值,所以它必须跳过3个字节
到达合适的边界才能存储,在整型值之后是最后一个字符。
如果声明了相同类型的第2个变量,它的起始存储位置也必须满足4这个边界,所以第一个结构的后面还要再跳过3个字节才能存储第2个结构,因此
结构将占据12个字节的内存空间,但实际只使用其中的6个,
你可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员是首先出现,对边界要求最弱的成员最后出现。这种做法
最大限度的减少因边界对齐而带来的内存空间损失。
===================
作为函数参数的结构
===================
结构变量是一个标量,它可以用于其它标量可以使用的任何场合。因此,把结构作为参数传递
给一个函数是合法的,但这种做法往往并不适宜。
printfr(Tran c){
}
调用时printfr(curr——c);
这个方法能够产生正确的结果,但它的效率很低,因为c语言的参数传值调用方式要求把参数的一份拷贝传递给函数。如果
PRODUCT_SIZE为20,而且在我们使用的机器上整型和浮点型都占4个字节,那么这个结构将占据32个字节的空间。要想把它作为参数进行传递
我们必须把32个字节复制到栈中,以后再丢弃。
把前面那个函数和下面这个进行比较
void print_roce(Transac *tran){
}
这个函数可以这样调用
print_rec(¤t_trans);
这次传递给函数的是一个指向结构的指针,指针比整个结构要小的多,所以把它压到堆栈上效率提高了很多。
传递指针另外需要付出的代价是我们必须在函数中使用间接访问来访问结构的成员。结构越大,把指向他的指针传递给函数的效率
就越高。
在很多机器上,你可以把参数声明为寄存器变量,从而进一步提高指针传递方案的效率。在有些机器上,这种声明在函数中的起始部分还
需要一条额外的指令,用于把堆栈中的参数复制到寄存器,供函数使用。但是如果函数对这个指针的间接访问次数超过两三次,那么使用这个方法
所节省的空间远远高于一条额外指令所话费的时间。
向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。经过
这两个修改之后,现在函数的原型将下所示:
void print_rec(register Transaction const *trans)
====================
====================
联合UNION
====================
====================
联合的声明和结构类似,但它的行为方式却和结构不同。联合的所有成员引用的是内存中的相同位置
当你想在不同的时刻把不同的东西存于同一个位置的时候,就可以使用联合。
union{
float f;
int i;
}fi;
在一个浮点型和整型都是32位的机器上,变量fi只占据内存中一个32位的字。如果成员f被使用,这个字就作为浮点值访问,如果成员i都被使用
这个字就作为整型值访问,所以,下面这段代码fi.f=3.13 printf(“%d\n”,fi.i);
首先把π的浮点数表示形式存储于fi,然后把这些相同的位当做一个整型值打印输出。注意这两个成员所引用的位相同,仅有的区别在于每个成员
的类型决定了这些位被如何解释。
为什么人么有时想使用类似此例的形式呢?如果你想看看浮点数是如何存储在一种特定的机器中但又对其他东西不感兴趣,联合就可能有所帮助
。这里有一个更为现实的例子。BASIC编译器的任务之一就是记住程序所使用的变量的值。BASIC提供了几种不同类型的变量,所以每个变量的类型
必须和它的值一起存储。但是效率不高
在BASIC程序中的一个变量被创建时,解释器就创建一个同样的结构并用记录变量的类型,然后根据变量的类型,把变量的值存储在这三个
值字段的其中一个。
这个结构的低效在于它所占用的内存,每个VARIABLE结构存在两个未使用的值字段。联合就可以减少这种浪费,它把这两个值字段的每一个都存储于同一个内存位置。这三
个字段并不会冲突,因为每个变量只可能具有一种类型,这样在某一时刻,联合的这几个字段只有一个被使用。
struct VARIABLE{
enum {INT,FLOAT,STRING} type;
union{
int i;
float f;
char *s;
}value;
};
现在对于整型变量,你将把type字段设置为INT,并把整型值存储于value.i字段。对于浮点值,你可以使用value.f字段,当以后得到这个变量的值是,对type字段
进行检查决定使用哪个值字段。这个选择决定内存位置如何被访问,所以同一个位置可以用于存储这三种不同类型的值。注意编译器并不对type
字段进行检查证实程序使用的是正确的联合成员。维护并检查type字段是程序员的责任。如果联合的各个成员具有不同的长度,联合的长度就是它最长成员的
长度。