C语言笔记之结构
本帖最后由 smile5 于 2019-7-23 10:28 编辑前天做题发现好多知识都给忘了,所以就来复习了一下。——书名是《C和指针(第二版)》
结构(一)
//结构声明struct {
int a;
char b;
float c;
} x;
//这个结构声明了一个名叫x的变量,它包含三个成员:一个整数、一个字符和一个浮点数
struct {
int a;
char b;
float c;
} y, * z;
//创建了y和z,y是一个包含20个结构的数组。z是一个指针,它指向这个类型的结构
/*
**警告:这两个声明被编译器当做两种截然不同的类型,即使他们的成员列表完全相同。因此,y和z的类型和x的类型不同,所以下面这条语句
**z = &x;
**是非法的。
**(——如果有相同的标签就会被当成同一种类型了)
**但是,并不意味着所有的结构必须使用一个单独的声明来创建。标签(tag)字段允许为成员列表提供一个名字,这样它就可以在后续的声明中继续使用,
**标签允许多个声明使用同一个成员列表,并且创建同一种类型的结构。
*/
struct SIMPLE {
int a;
char b;
float c;
};
//这个声明把标签SIMPLE和这个成员列表联系在一起。该声明并没有提供变量列表,所以它并未创建任何变量 。
//标签标识了一种模式用于声明未来的变量。它们创建和最初两个例子一样的变量无论是标签还是模式本身都不是变量。
struct SIMPLE x;
struct SIMPLE y, *z;
//这些声明使用标签来创建变量。它们创建和最初两个例子一样的变量,但存在一个非常重要的区别——现在x、y、z都是同一种类型的结构变量。
//也可以用typedef创建新类型
typedef struct
{
int a;
char b;
float c;
} Simple;
//现在可以用Simple2作为类型声明新的结构体变量
Simple x;
Simple y, *z;
/*提示:如果你想在多个源文件中使用同一种类型的结构,应该吧标签声明或typedef形式的声明放在一个头文件中。
**当源文件需要这个生命时可以使用#include指令把那个头文件包含进来。
**结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
*/
//结构成员(如果一个结构声明里包含另一个结构,那么被包含的结构必须在这个结构之前声明)
struct COMPLEX {
float f;
int a;
long *lp;
struct SIMPLE s;
struct SIMPLE sa;
struct SIMPLE *sp;
};
//一个结构的成员的名字可以和其它结构的成员的名字相同,所以这个结构的成员a并不会与struct SIMPLE s的成员a冲突。
//结构成员的直接访问
/*
**结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数,左操作数是结构变量的名字,右操作数是需要访问的成员的名字。这个表达式的结果就是指定的成员。
*/
struct COMPLEX comp;
/*
**名字为a的成员是一个数组,所以表达式comp.a就选中了这个成员,这个表达式的结果是数组名。
**类似地,成员s是个结构,所以表达式comp.s的结果是个结构名,它可以用于任何可以使用普通结构变量的地方。尤其是,我们可以把这个表达式用作另一个点操作符的左操作符,
**如(comp.s).a,选择结构comp的成员s(也是一个结构)的成员a。点操作符的结合性是左到右,因此可以省略括号,comp.s.a也一样。
*/
//结构成员的间接访问
/*
**如果有一个指向结构的指针,你该如何访问这个结构的成员呢?
**首先是对指针执行间接访问操作,这使你获得这个结构。然后你使用点操作符来访问它的成员。
**但是,点操作符的优先级要高于间接访问操作符,所以你必须在表达式中使用括号,确保间接访问首先执行。
*/
void func( struct COMPLEX *cp );
//函数可以使用下面这个表达式来访问这个变量所指向的成员f
(*cp).f
//对指针执行间接访问将访问结构,然后点操作符访问一个成员。
//间接访问也可以使用->(箭头操作符) ,但是左操作数必须是一个指向成员的指针。间接访问操作符内置于箭头操作符中,所以我们不需要显式地执行间接访问或使用括号。
cp -> f
cp -> a
cp -> s
//结构的自引用
/*
**结构不能引用与自己相同类型的结构,否则会陷入一个无穷的递归循环。
**可以引用一个指向自身结构类型的指针成员,因为,因为指针的大小是确定的。
**警告:
*/
typedef struct {
int a;
SELF_REF3 *b;
int c;
} SELF_REF3;
/*
**这个声明的目的是为这个结构创建类型名SELF_REF3。但是,它失败了,类型名知道声明的末尾才定义,所以在结构声明的内部它尚未定义。
**解决方案是定义一个结构标签来声明b
*/
typedef struct SELF_REF_TAG {
int a;
struct SELF_REF3_TAG *b;
int c;
} SELF_REF3;
//不完整的声明
/*
**有时候必须声明一些相互之间存在依赖的结构。也就是说,其中一个结构包含了另一个结构的一个或多个成员。
**和自引用结构一样至少有一个结构必须在另一个结构内部以指针的形式存在。
**问题在于声明部分:如果每个结构都引用了其它结构的标签,哪个结构该首先声明呢?
**这个问题的解决方案是使用不完整声明(incomplete declaration),它声明一个作为结构标签的标识符。
**然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。
*/
struct B;
struct A {
struct B *partner;
//other declarations
};
struct B {
struct A *partner;
//other declarations
};
//在A的成员列表中需要标签B的不完整声明。一旦A被声明后,B的成员列表也可以被声明
//结构的初始化
/*
**结构的初始化和数组的初始化很相似。一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构各个成员的初始化。
**这些值根据结构成员列表的顺序写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。
**结构中如果包含数组或结构成员,其初始化方式类似于多元数组的初始化。一类完整的聚合类型成员的初始值列表可以嵌套于结构的初始值列表内部。
*/
struct INIT_EX {
int a;
short b;
Simple c;
} x = {
10,
{ 1, 2, 3, 4, 5 },
{ 25, 'x', 1.9 }
};
结构(二)
//访问嵌套结构
/*
**注:px是一个指向结构的指针,c是被包含的另一个结构。
*/
typedef struct {
int a;
short b;
} Ex2;
typedef struct {
int a;
int b;
Ex2 c;
struct Ex *d;
} Ex;
Ex x = { 10, "Hi", { 5, { -1, 25 } }, 0 };
Ex *px = &x;
px -> c.a
/*
**之所以用箭头操作符,是因为px并不是一个结构,而是一个指向结构的指针。接下来之所以用点操作符是因为px -> c的结果并不是一个指针,而是一个结构。
**注:"px -> c" 等价于 "(*px.c)"
*/
//结构的存储分配
/*
**只有当结构成员满足成员之间才可能出现可用于填充的额外内存空间。
**示例结构:
struct ALIGN {
char a;
int b;
char c;
};
**如果一台机器的整形值长度为4个字节并且它的起始位置必须能被4整除,那么这个结构在内存中存储如下所示。
| a|空|空|空|b|b|b|b|c|空|空|空|
| -------- ---------------- |
**系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对其要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。
**如果声明了第二个变量,它的存储位置也必须满足这四个边界,所以第一个结构的后面要跳过三个字节才能存储第二个结构。因此每个结构将占用12个字节但只利用其中的6个。
**如果在声明中对结构成员列表重新排列,让那些对边界要求严格的成员首先出现,对边界要求最弱的最后再出现,可最大限度地减少因边界对齐而带来的内存损失。
struct ALIGN {
int b;
char a;
char c;
};
这个结构和上面的结构成员一样,只占用8个字节。两个字符可以挨着存储。
**如果不考虑相关的成员在一起或是可维护性的话,就应适当重排。
**如果必须确定摩纳哥成员的实际位置,应该考虑边界对齐因素,可以使用宏offsetof(定义于 stddef.h)。
offsetof (type, number)
type是结构的类型,number是你需要的 成员名。表达式返回一个size_t,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。
*/
//因为结构一般比较大,将结构当做形参是不合适的,效率低、费内存。所以应该选择一个指向结构的指针做参数操作结构。
//位段
/*
**位段的声明和结构类似,但它的成员是一个或多个位的字段。这些不同长度的字段实际上存储于一个或多个整型变量中。
**位段的声明与结构成员声明相同,但有两个例外,首先,位段成员必须声明为int、signed int或unsigned int类型。
**其次,在成员名的后面是一个冒号和一个整数,这个这个整数指定该位段所占用的位的数目。
提示:
1、用signed或unsigned显式地声明位段是个好主意。如果把位段声明为int型,它究竟被解释为有符号数还是无符号数是由编译器决定的。
2、注重可移植性的程序应避免使用位段。由于下面这些与实现有关的依赖性,位段在不同的系统中可能有不同的结果。
a、int位段被当做有符号数还是无符号数
b、位段中位的最大数目。许多编译器把位段成员的长度限制在一个整形值的长度范围之内,所以一个能够运行于32位机器上的位段声明可能在16位整数的机器上无法运行。
c、位段中的成员在内存中是从左向右分配的还是从右向左分配的。
d、当一个声明指定了两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时,编译器有可能把第二个位段放在内存的下一个子,
也可能直接放在第一个位段后,从而在两个内存位置的边界上形成重叠。
下面是一个列子:
struct CHAR {
unsigned ch :7;
unsigned font :6;
unsigned size :19;
};
struct CHAR ch1;
这段声明取自于一个文本格式化程序,它可以处理128个不同的字符值(需要7个位)、64种不同的字体(需要6个位)以及0到524287个单位的长度。
这个size位段过于庞大,无法容纳于一个短整型,但其余的位段都比一个字符还短。
位段使程序员能够利用ch和font所剩余的位来增加size的位数,这样就避免了声明一个32位的整数来存储size位段。
许多16位编译器会把这个声明标记为非法,因为最后一个位段的长度超过了整形的长度。
但在32位的机器上,在内存中可能(根据声明顺序从上到下)从左到右创建ch1,也可能从右到左。
使用位段的好处:
1、能把长度为基数的数据包装在一起,节省存储空间。当程序使用成千上万的这类结构时,这种节省方法就很重要。
2、它们可以很方便地访问一个整形值的部分内容
/*
所谓"位段"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
典型的实例:
用 1 位二进位存放一个开关量时,只有 0 和 1 两种状态。
读取外部文件格式——可以读取非标准的文件格式。例如:9 位的整数。
一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs{
unsigned a:4;
unsigned:4; // 空域
unsigned b:4; // 从下一单元开始存放
unsigned c:4
}
在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。
位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k{
int a:1;
int:2; // 该 2 位不能使用
int b:3;
int c:2;
};
从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。
位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名.位域名
位域变量名->位域名
位域允许用各种格式输出。
请看下面的实例:
实例
# include stdio.h>
int main(void){
struct bs{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit,*pbit;
bit.a=1; // 给位域赋值(应注意赋值不能超过该位域的允许范围)
bit.b=7; //* 给位域赋值(应注意赋值不能超过该位域的允许范围)
bit.c=15; //* 给位域赋值(应注意赋值不能超过该位域的允许范围)
printf("%d,%d,%d\n",bit.a,bit.b,bit.c); //* 以整型量格式输出三个域的内容
pbit=&bit; //* 把位域变量 bit 的地址送给指针变量 pbit
pbit->a=0; //* 用指针方式给位域 a 重新赋值,赋为 0
pbit->b&=3; //* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3)
pbit->c|=1; //* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15
printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c); //* 用指针方式输出了这三个域的值
}
运行结果:
1,7,15
0,3,15
--------------------------------
Process exited after 0.04306 seconds with return value 0
请按任意键继续. . .
//比如3个位最大可以表示7——111,复制超过7则进位为0,但不影响其它位置
上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示位域也是可以使用指针的。
来自菜鸟教程————https://www.runoob.com/cprogramming/c-structures.html
*/
本帖最后由 mengmengbi 于 2019-7-25 13:34 编辑
不完整声明好像懂了。
就比如说:
结构体A和结构体B之间存在相互依赖关系,但是我们不知道先声名哪个,所以可以先声明一个不完整的B
//不完整声明B,作为结构标签标识符
struct B;
//对A进行声明
struct A
{
struct *B; //不需要知道B的长度
//other number
};
/对B进行声明
struct B
{
struct *A;
//other number
};
mengmengbi 发表于 2019-7-24 17:32
第一板块的103-105行的那段解释感觉没看懂,
难道那个结构体不是正确的吗?
那个结构体是正确的,空一行下面的注释“//”其实是后面一段的小标题 初来乍到就偶遇这个知识帖,真香
真香,大佬 。。。。。。。。。。。 学习了,很有收获! 谢谢,大佬 学习了,很有收获! struct {
int a;
char b;
float c;
} y, z;
//创建了y和z,y是一个包含20个结构的数组。z是一个指针,它指向这个类型的结构
这里应该是 *z 吧? 感觉都复习了一遍,很好 感觉程式码排版看起来真心累
不知道论坛上有没有如 markdown 语法可做语法排版