-
匿名结构体类型
结构体的完全声明:
struct tag
{
member_list;
}variable_list;
//结构体的标签:tag
//结构体的类型:struct tag
//结构的成员列表:member_list
//结构体变量列表:variable_list
当不完全声明时,比如说省略结构体的标签,这时,这种结构体就被称为匿名结构体。
匿名结构体也称为未命名结构体,由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。
值得注意的是,匿名结构体类型可以说是“一次性用品”,用了一次以后再也用不了了。
struct
{
char c;
int a;
double d;
}s1;
struct
{
char c;
int a;
double d;
}* p1;//一个看上去指向和s1相同类型的指针
int main()
{
p1 = &s1;//这样写是不可以的,编译器会认为它是两个不同的类型
return 0;
}
如上代码是错误的,虽然两个匿名结构体,看上去它们的成员都是一样的,表面上是同一类型,但实际上他们是两种不同的类型。因此,因为在定义的时候没有写结构体类型名称,编译器会把它们当做两种不同的类型,然后报错。
匿名结构体的优点:
嵌套在结构体中的结构体为匿名结构时,可以直接访问其成员。
#include <stdio.h>
struct Stu
{
char* name;
char gender;
int age;
struct//嵌套定义了这个匿名结构体
{
int Student_ID;
long phone_number;
};
};
int main(void)
{
struct Stu A= {"A", 'M', 19, {30, 13930422035}};
printf("%d\n", A.Student_ID);
}
如果不使用匿名结构体,则上述例子会变成这样:
#include <stdio.h>
struct Stu_information
{
int Student_ID;
long phone_number;
};
struct Stu A
{
char* name;
char gender;
int age;
struct Stu_information;
};
int main(void)
{
struct Stu A = {"A", 'M', 19, {30, 13930422035}};
printf("%d\n", A.Stu_information.Student_ID);
}
对比上述两个例子可以看出:
使用匿名结构体,结构体对象 A 可以通过 A.Student_ID直接访问匿名结构体成员变量 .Student_ID,代码比较简洁,反之则必须通过A.Stu_information.Student_ID 来访问结构体成员变量 ,比较繁琐。
-
结构体的自引用
我们不由地思考一个问题,结构体把自己当作自己的一个成员变量,这是否可行呢?
struct Node
{
int date;
struct Node n;//错误!
};
结论是:这样写是绝对错误的!
请想一想,如果我们想要求struct Node 的大小,其中,成员data占4个字节,那么struct Node n成员的大小是多少呢?
这就陷入了一个思维上的死循环:要求它自己的大小,那么首先就要求它自己的大小?这是什么鬼啊?!
所以这样写是绝对不可以的!!!
那么,如果想要正确的自引用,我们应该怎么做呢?
这里要先补充一点关于“数据结构”的知识:
数据的存储可以有很多方式,比如“顺序存储”、“二叉树”等等,这些存储方式都可以让我们很顺利地从一个数据找到另一个数据。
所以,关于结构体的自引用,我们可以把它看作是通过一个结构体,可以找到下一个结构体。这样一个一个像线一样访问数据,即“线性数据结构(通过链表方式可以顺序访问内存空间中不相邻的数据)”,我们就要使用到结构体指针。而指针的大小跟其所指向的类型无关,仅跟编译器有关,32位平台上指针大小为4个字节,在64位平台上,指针大小为8个字节。所以,通过指针大小的确定性,我们就可以确定结构体类型的大小了。
struct Node //结构体的自引用
{
int date;
struct Node* next;
};
int main()
{
struct Node n1;
struct Node n2;
n1.next = &n2;//这样就可以通过n1找到n2了.
return 0;
}
相当于每个结构体,前面存数据,后面下一个结点地址的指针,通过该结点可以找到下一个结构体。
匿名结构体的重命名+自引用:
typedef struct
{
int data;
int d;
}Node; //一种错误的写法
typedef struct Node
{
int data;
int d;
}Node; //正确
struct
{
int data;
struct Node* next;
}Node1;
//结构体在未定义Node之前就定义了一个Node*的指针,这样是错误的!!!
typedef struct
{
int date;
struct Node* next;//在此之前,都没有出现Node
}Node;
//把一个匿名结构体重命名为Node?不可以!
//正确的写法是这样的:
typedef struct Node
{
int data;
struct Node* next;
}Node;
-
结构体的初始化:
struct S
{
int a;
char c;
};
struct F
{
int a;
char arr[10];
struct S;
};
int main()
{
struct F pd= { 12,"sgdkjklaj",{345,'o'}};//结构体的嵌套初始化
struct S s2 = { 2000,'p'};//默认顺序初始化
struct S s3 = { .c = 'r',.a = 2000 };//自己确定初始化的顺序
}
-
结构体的内存对齐原则:
1.第一个成员永远在结构体变量偏移量为0的地址处。
2.从第二个成员开始,以后每个成员变量都要对齐到某个数字(对齐数)的整数倍的地址处。
这个对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
-
vs2017环境中,VS编译器默认的值为8
-
一个是Linux环境,gcc编译器下,没有默认对齐数,对齐数就是成员自身的大小
3、当成员全部存放进去后,结构体总大小必须是所有成员的对齐数中最大对齐数(每个成员变量都有一个对齐数)的整数倍。如果不够,则浪费空间。
4、如果嵌套了结构体的情况,嵌套的结构体成员要对齐到自己成员的最大对齐数的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
接下来,让我们举一些例子:


为什么会存在内存对齐呢?
1.平台原因(移植原因)∶不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(所以我们对齐到能够被硬件访问的位置,在对齐位置进行存储数据)
⒉性能原因∶数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;
而对齐的内存访问仅需要一次访问。它能够提高效率。
总体来说︰ 结构体的内存对齐是拿空间来换取时间的做法。

64位平台上,编译器一次访问8个字节。

但是,节省空间不能无脑地节省空间啊。
如果既要满足对齐,又要节省空间,怎么办呢?
解决方案:把占用空间较小的数据集中在一起。
struct s1
{
char c1;
int i;
char c2;
};//12个字节
struct s2
{
char c1;
char c2;
int i;
};//8个字节
设置默认对齐数:
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数来满足需求。
如何修改默认对齐数呢?通过#pragma修改。
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct s1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1,这样其实相当于根本没有对齐数了。
struct s2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
offsetof,这是一个宏,用来计算结构体成员相对于结构体起始位置的偏移量

#include<stddef.h>
struct S
{
char c1;
char c2;
int i;
};
int main()
{
struct S s = {0};
printf("%d\n",offsetof(struct S,c1)); //0
printf("%d\n", offsetof(struct S, c2));//1
printf("%d\n", offsetof(struct S, i)); //4
return 0;
}
-
结构体传参
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1, 2, 3, 4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d \n", s.num);
}
// 结构体地址传参
void print2(struct S* ps)
{
printf("%d \n", ps->num);
}
int main()
{
print1(s);//传结构体
print2(&s);//传结构体地址,用一个结构体指针接收
return 0;
}
上面的print1和print2哪一个更好呢?

print2更好一点
对于结构体的传参首选传递地址,原因如下:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
那么如何应对传地址的不安全性呢?很简单,用一个const 就可以了。
-
位段
什么是位段 ?
位段的声明和结构是类似的,但是有两点不同︰
1.位段的成员必须是int、unsigned int,signed int、char这些整型家族的类型。
2.位段的成员名后边有一个冒号和一个数字。
#include<stdio.h>
struct A
{
int _a : 2; //int _a;
int _b : 5; //int _b;
int _c : 10; //int _c;
int _d : 30; //int _d;这是结构体
};
int main()
{
struct A sa={0};
printf("%d\n", sizeof(sa));
return 0;
}
位段位段,“位”指的是二进制位。
如果_a只会存储0,1,2,3(00,01,10,11),那么这个时候2个比特位就够了。所以位段是一种更节省空间的方法。
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};//47个比特位
//实际上占了8个字节
//当然,这已经很节省空间了!
位段的空间上是按照需要以4个字节(int )或者1个字节(char)来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。(所以一些细节方面不能够确定下来)
现在还有一些问题待确定:
1、空间是从左向右使用还是从右向左使用?
2、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用呢?
#include<stdio.h>
struct S
{
char a : 4;
char b : 3;
char c : 5;
char d : 4;
}s;
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}


但是,这些在不同编译器上都是不同的!
位段的跨平台问题:
1.int位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32),写成27,在16位机器会出问题。
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总的来说,跟结构体相比,位段可以达到同样的效果,并且可以很好地节省空间,但是有跨平台问题。
位段的应用:

关于位段的一些练习题:
#define MAX_SIZE A+B
struct _Record_Struct
{
unsigned char Env_Alarm_ID : 4;
unsigned char Para1 : 2;
unsigned char state;
unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer =
(struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);
问:当A=2, B=3时,pointer分配( )个字节的空间?
位段成员作为unsighed char类型,是按1个字节开辟内存的,第一个成员Env_Alarm_ID开辟一个字节等于8个bit位,被Env_Alarm_ID用去4个bit,还剩4个bit可以给Para1用。Para1用去2个bit,故位段Env_Alarm_ID , Para1一共用一个字节;
state不是位段重新开辟一个字节的内存,它直接占一个字节;
位段avail只占1个bit,但也要开辟1个字节的空间用来存放。
所以前面的算下来总共开辟了3个字节的空间。
(sizeof(struct _Record_Struct) * MAX_SIZE)=3*2+3(这里一定要注意宏定义并没有写成(A+B)的形式,所以并不是先计算A+B)=9.
故pointer分配的空间为9个字节
int main()
{
unsigned char puc[4];
struct tagPIM
{
unsigned char ucPim1;
unsigned char ucData0 : 1;
unsigned char ucData1 : 2;
unsigned char ucData2 : 3;
}*pstPimData;
pstPimData = (struct tagPIM*)puc;
memset(puc,0,4);
pstPimData->ucPim1 = 2;
pstPimData->ucData0 = 3;
pstPimData->ucData1 = 4;
pstPimData->ucData2 = 5;
printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
return 0;
}
代码结果是?
画个图:

最后,想要补充一个非常易错的练习题:
#include<stdio.h>
struct ord
{
int x,y
}dt[2] = {1,2,3,4};
int main()
{
struct ord* p = dt;
printf("%d,", ++p->x);
printf("%d\n", ++p->y);
return 0;
}
这个程序的运行结果是什么?

是不是有点意想不到?
让我们来简单分析一下:
结构体其实也是可以这样赋值的,和数组是很类似的。
printf("%d,", ++p->x);
printf("%d\n", ++p->y);
这两条语句中,->这个操作符的优先级是高于++的,所以其实是先取出p指向的x和y这两个数值,然后再对这两个数++。
所以实际上,p这个指针自始至终都指向dt[1]。
文章讨论了C语言中的匿名结构体的使用,包括它们在嵌套结构中的作用和简化代码的优点。同时,解释了结构体的自引用问题,指出错误的自引用示例以及如何正确实现自引用。文章还深入介绍了内存对齐的原则和其背后的平台及性能考虑,并提供了实例展示。此外,提到了位段的概念,强调了位段在节省空间上的优势,但同时也指出位段的跨平台问题。最后,文章通过示例探讨了结构体的初始化、传参以及位段的应用。

5730

被折叠的 条评论
为什么被折叠?



