目录
在C语言中提供了两种类型的聚合类型,数组和结构。数组是相同类型元素的集合,它的每个元素是通过"下标"引用或“指针”间接访问来选择;结构是一些值的集合,这些值被称为结构的成员,各个成员可能具有不同的类型,由于结构不像数组一样,所以不能使用下标来访它的成员。相反,每个结构成员都有自己的名字,它们是通过名字访问。
聚合数据类型(aggregate data type)能够同时存储超过一个的单独数据。
结构声明
在声明结构时,必须列出它包含的所有成员。不要把声明结构很难,就像声明一个变量一样,这些变量成员用结构封装起来最后用一个新的变量来标记这些变量成员而已。例如:
struct tag {
member-list
}variable_list;
tat:标记结构的标签
variable_list:变量名列表
创建结构的例子:
struct {
int a;
int b;
int c;
}variable_t;
创建一个variable_t变量;
或者
struct {
int a;
int b;
int c;
}variableArr[20],*pxStr_t;
声明创建variableArr和pStr_t。variable是一个数组,它包含20个结构。pStr_t是一个指向结构的指针。
结构之间的使用赋值使用该是怎样的?接下来考虑这种情况使用是否正确?
pxStr_t = &variable_t;答案是非法的。因为variable_t变量和pxStr_t的类型是不同的。那C又是如何解决结构之间赋值使用的呢?通过标签(tag)字段为成员列表提供一个名字,标签允许多个声明使用同一个成员列表,并且创建同一种类型的结构。这里有个列子。
struct SIMPLE {
int a;
char b;
float c;
};
这个例子没有直接声明变量,我们可以像声明一般变量来声明结构变量。例如:
struct SIMPLE variable_t;
struct SIMPLE variableArr[20], *pxStr_t;
这些声明使用标签来创建变量。它们创建和最初两个例子一样的变量,但存在一个重要的区别------现在variable_t、variableArr和pxStr_t都是同一种类型的结构变量。声明结构时可以使用另一种良好技巧是用typedef创建一种新的类型。到后面结构自引用再详细说明,这里先留疑问。
结构成员
到目前为止的例子里,我只使用了简单类型的结构成员。但可以在一个结构外部声明的任何变量都可以作为结构的成员。尤其是,结构成员可以是标量、数组、指针甚至是其他结构。比如下面的例子:
struct COMPLEX{
float f;
int a[20];
long *pl;
struct SIMPLE Str;
struct SIMPLE StrArr[20];
struct SIMPLE *pxStr;
};
从例子中可以看出一个结构的成员名字可以和其他结构的成员的名字相同,所以这个结构成员a并不会与struct SIMPLE Str的成员冲突。正如接下去看到的那样,成员的访问方式允许指定你指定任何一个成员而不至于产生歧义。
结构成员的访问
结构成员的访问有“直接访问”和“间接访问”两种。直接访问是通过点操作符(.)访问的。点操作符接受两个操作数,左操作数就是“结构变量名”,右操作数就是需要访问成员的名字。间接访问通过箭头操作符(->)来访问。箭头操作符接受两个操作数,但左操作数必须是一个指向结构的指针,右操作数就是需要访问成员的名字。直接访问具体看下面的例子:
// 声明一个结构变量(标量)
struct COMPLEX Str_t;
//访问结构成员a
Str_t.a;
//访问结构成员StrArr
Str_t.StrArr; //其类型是数组名,它的值是指针常量,对这个表达式使用下标引用操作f访问数组元素。具体如下:
(Str_t.StrArr)[4]; // 这个元素的类型是一个结构,我们可以使用直接操作访问它的成员,如下所示:
((Str_t.StrArr)[4]).c // 这里使用括号更加容易理解,也可以不用括号
Str_t.StrArr[4].c //下标引用和点操作符具有相同的优先级,它们的结合性都是从左到右,所以我们可以省略所有括号
那间接访问又是如何访问的呢?接下来看具体的例子:
// 声明一个指向结构指针
struct COMPLE *pxComple_t;
// 访问结构成员a
pxComple_t->a;
// 访问结构成员StrArr
pxComple_t->StrArr;
// 访问结构成员StrArr中的元素结构
(pxComple_t->StrArr)[4];
// 访问结构成员StrArr中元素结构的成员a
(pxComple_t->StrArr)[4].a;
结构自引用
在一个结构内部包含一个类型为该结构本身的成员是否合法呢?这里有一个例子,可以说明这个想法。
struct SELF_REF1 {
int a;
struct SELF_REF1 selfStruct;
int c;
};
这种类型的自引用是非法的,因为成员selfStruct是另一个完整的结构,其内部还将包含它自己的成员selfStruct。这第2个成员又是另一个完整的结构,它还包含它自己的成员selfStruct。这样重复下去永无止境。有点类型不会终止的递归程序。接下来这个声明却是合法的,具体有什么区别?
struct SELF_REF1{
int a;
struct SELF_REF1 *selfStruct;
int c;
};
这个声明和前面那个声明的区别在于selfStruct现在是一个指针而不是结构。编译器在结构的长度确定之前就已经知道指针长度,所以自引用是合法的。
还记得上面遗留的一个关于typedef的问题么?接下来讲解具体如何在实际应用中使用。具体如下:
typedef struct {
int a;
SELF_REF *px;
int b;
}SELF_REF;
这个声明的目的是为这个结构创建类型SELF_REF.但是,它失败了。类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。
解决方案是定义一个结构标签来声明px,如下所示:
typedef struct SELF_REF_TAG{
int a;
struct SELF_REF_TAG *px;
int b;
}SELF_REF;
结构的初始化
结构的初始化方式和数组的初始化很相似。一个位于一对花括号内部,由逗号分隔的初始值列表可用于结构各个成员的初始化。如果结构中包含数组或结构成员,其初始化方式类似于多维数组的初始化。这里有一个例子:
struct INIT_STR{
int a;
short b[10];
simple c;
}x={
10,
{1,2,2,3,4,5},
{25, 'x', 1.9}
};
结构、指针和成员
直接或通过指针访问结构和它们的成员的操作符是相当简单的,但是当它们应用于复杂的情形时就有可能引起混淆。以下的例子,能帮助你更好地理解这两个操作符的工作过程。这些例子使用了下面的声明。
typedef struct {
int a;
short b[2];
}Ex2;
typedef struct {
int a;
char b[3];
Ex2 c;
struct Ex *d; // Ex *d是非法的
}Ex;
类型Ex结构可以用下图表示:
用图形更加地直观表达清楚结构,使得例子看上去更加清楚一些。事实上,这张图并不能完全准确,因为编译器会对结构进行优化处理避免成员之间的浪费空间。
对上述例子的声明初始化如下:
Ex x = {
10,
"Hi",
{5, {-1, 25}},
0
};
Ex *px = &x;
上述例子声明后的图形所示如下
访问指针
从指针变量开始分析表达式px的右值是:
px是一个指针变量,但是此处并不存在任何间接访问操作操作符,所以这个表达式的值是px内容。这个表达式的左值是:
从图中可知px的旧值将被一个新值所取代。
现在考虑表达式px+1,这个表达式并不是一个合法的左值。因为它的值并不存在与任何可标识的内存位置。如果px指向一个结构数组的元素,这个表达式将指向该数组的下一个结构。但就是如此,这个表达式仍然是非法的,因为我们没有办法分辨内存下一个位置所存储的是这些结构元素之一还是其他东西。编译器无法检测到这类错误,所以自己判断指针运算的意义。
访问结构
我们可以使用*操作符对指针执行间接访问。表达式*px的右值是px所指向的整个结构。
在执行间接访问操作时,其结构就是整个结构。你可以把你可以把它当作一个标量赋值给另一类型相同的结构,你也可以把它作为点操作符的左操作数,访问一个指定的成员。你也可以把它作为参数传递给函数,也可以把它作为函数的返回值返回。
到这里,结构将接受一个新的值,换句话说,它将接受它的所有成员的新值。表达式*px+1是非法的,因为*px的结果是一个结构。C语言并没有定义结构和型值之间加法运算。但表达式*(px+1)如何呢?如果x是一个数组的元素,这个表达式表示它后面的那个结构。但是,x是一个标量,所以这个表达式实际上是非法的。
访问结构成员、嵌套的结构以及指针成员
表达式px->a右值是:
还记得上面访问结构成员有两种方式么?一种是直接访问,另一种是间接访问(->)。相比较直接访问,间接访问更加容易出错。比如相互比较一下表达式*px和px->a。这两个表达式中,px所保存的地址都用于寻找这个结构体。但结构的第一个成员是a,所以a的地址和结构的地址一样。从 地址上分析px既指向整个结构,又同时指向第一个成员a,毕竟它们地址相同。如果从这个角度分析只对一半。尽管它们的地址相同,但它们的类型不同。变量px是指向整个结构的指针,而不是它的第一个成员。比如下面的表达式会出现警告的:
// 类型不匹配
int* pi = px;
// 使用强制转换才能正确的访问成员a
int* pi = (int*)px;
// 正确访问a
int* px = &px->a;
假设接下来我们要访问结构c,可以使用表达式px->c。如下图所示:
这个表达式可以使用点操作符访问c结构特定成员。例如,表达式px->c.a具有下面的右值:
这个表达式有点复杂对于初学者来说,那就简单地解释一下:因为px声明的是一个指向整个结构的指针所以使用箭头操作符(->),接下来使用点操作符px->c的结果并不是一个指针,而是一个结构。这里有更加复杂的表达式:*px->c.b,如果你对它逐步分析还是比较简单的。它有三个操作符,首先执行的是箭头操作符。px->c的结果是结构,在表达式中.b是对该结构的直接访问成员b。b是一个数组,所以px->b.c的结果是一个指针常量,它指向数组的第一个元素。最后对这个指针执行间接访问。最终的结果是数组元素的第一元素值。图解如下:
对于有结构嵌套的结构,我们又如何去访问呢?表达式px-d的结果是0,它的左值是它本身的内存位置。*px->d是非法的,因为d包含一个NULL指针,对一个NULL指针进行解引用操作是错误的。
结构的存储分配
结构在内存中是如何存储的呢?我们带着这个疑问往下学习,在前面举的列子中内存并不完全准确,编译器会根据结构成员的内存大小进行字节对齐的操作。为了说明这一点,考虑下面的结构:
struct ALIGN {
char a;
int b;
char c;
};
如果某个机器的整型长度为4个字节,并且它的存储地址位置必须能被4整除,那么这个结构在内存中存储情况如下:
编译器在分配结构内存时,遵循字节对齐原则;计算内存步骤如下:
- 先计算没有字节对齐之前总长度length
- 总长度length能被机器长度整除,并且也能被结构成员中最大内存字节数整除
提示:
有时,我们在程序优化内存时可以考虑结构成员进行重排来减少因字节对齐带来的空间损失。
C语言提供了一个操作符可以非常方便计算标量和常量内存大小,这个操作符就是sizeof。
警告:sizeof操作符括号内不做任何运算操作
作为函数参数的结构
结构变量是一个标量,可用作其他标量的可以使用标量的场合。把结构作为函数传递给函数是合法的。我们更加关注的是结构作为参数传递的效率问题。比如下面的例子:
typedef struct {
char product[PRODUCT_SIZE];
int quantity;
float unit_price;
float total_amount;
}Transation;
// 式1
void print_receipt(Transation trans);
// 式2
void print_receipt(Transation* ptrans);
式1、2传递结构那个更有效率?毫无疑问肯定是式2更具有效率,因为C语言的参数传递都是原标量的一份拷贝,显然式2是一个指针,拷贝的内存小效率高,所以式2的效率高。但向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果不期望修改,可以使用const关键字来防止这类修改,修改之后变成void print_receipt(Transtation const* ptrans)。如果还是想提高效率,比如如下表达式:
void print_receipt(register Transtation const* ptrans)
为什么这个表达效率还高呢?因为在执行标量拷贝过程中函数的起始部分还需一条额外的指令,用于把堆栈中的参数复制到寄存器,供函数使用。而标量声明为寄存器变量,是不经过堆栈复制到寄存器。
位段
在嵌入式的源码中,比较结构的位段使用的比较多。位段的声明和结构类型,但它的成员是一个或多个位的字段。这些不同长度的字段实际上存储在于一个或多个整型变量中。例如下面的例子:
提示:
位段成员必须声明为int、singed int或unsigned int类型
struct CHAR {
unsigned ch :7;
unsigned font :6;
unsigned size :19; // 系统16位非法
};
在32位的机器上,这个结构创建有以下2种:
从这个例子中说明了一个使用位段好理由:它能够把长度为奇数的数据包装在一起,节省内存。那么这个结构所占内存大小是多少?答案是:32bit位刚好是4字节,如果size : 32呢?那又是多少呢?答案是64bit,刚刚好是8个字节。
位域的存储规则
使用位域的主要目的是压缩存储,其大致规则为:
1.如果相邻的两个位域字段的类型相同,且其位宽之和小于其类型的sizeof()大小,则其后面的位域字段将紧邻前一个字段存储,直到不能容纳为止;
比如:一个位域变量有三个位域字段a、b、c,且类型完全相同,位域字段a和b的位宽之和小于其类型的sizeof()大小,那么位域字段c紧接着位域字段b后面存储;
2.如果相邻的两个位域字段的类型相同,且其位宽之和大于其类型的sizeof()大小,则后面的位域字段将从下一个存储单元的起始地址处开始存放,其偏移量恰好为其类型的sizeof()大小的整数倍;
比如:拿第1点中的例子来说,如果位域字段a和b的位宽之和大于其类型的sizeof()大小,则位域字段c就从下一个存储单元的起始地址初开始存放,其偏移量恰好是其类型的sizeof()大小的整数倍;
3.如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,VC6采取不压缩方式,GCC和Dev-C++都采用压缩方式;
4.如果位域字段之间穿插着非位域字段,则不进行压缩;
5.整个位域结构体的大小为其最宽基本类型成员大小的整数倍;
分析过程如下:
到这里,结构已经学习的差不多了。接下来简单地看一下联合体。
联合
和结构相比,联合可以说是另一种集合数据类型。它声明与结构类似,联合的所有成员引用的是内存中的相同位置。在同一个时刻只能访问其中的一个成员,不能再次访问联合其他成员了。如果联合的各个成员具有不同的长度,联合的长度就是它最大成员的长度并遵循对齐原则。
总结:通过本篇文章详细地讲解结构和联合的知识点,希望本篇文章对读者有用,谢谢!