C语言结构体的相关知识详解(理论+代码+画图分析=秒懂)

您所在的位置:网站首页 c怎么画图 C语言结构体的相关知识详解(理论+代码+画图分析=秒懂)

C语言结构体的相关知识详解(理论+代码+画图分析=秒懂)

2024-07-15 10:34| 来源: 网络整理| 查看: 265

前言:C语言虽然已经提供了很多内置类型:比如:int、char、short、long、float、double等等,但是这些类型并不能满足一些特殊场景的需求,比如,我想描述一个人的特征,需要名字、年龄、性别、身高、体重等;描述一部手机,需要型号、内存、颜色、价格、配置、品牌等;描述一瓶水,需要品牌、价格、容量等。为此C语言专门提供结构体这种自定义的数据,可以让程序员自己创造所需的数据类型。

1.结构体

结构是一些值的集合,这些值称为成员变量。结构中的每个成员可以是不同的数据类型,如:数组、指针、甚至是其他的结构体。

 1.1 结构体声明 struct test { member_list; }variable-list;

描述一个人:

struct Stduent { char name[20];//姓名 int age;//年龄 char sex[4];//性别 int height;//身高 int weight;//体重 };//注意带上分号 1.2 结构体变量如何定义和初始化

写法有三种:

//代码1:变量的定义 struct Test { char a; char b; }t1;//声明类型的同时定义变量t1 struct Test t2;//定义结构体变量t2 //代码2:对变量初始化 struct Test t3 = { 'h','o' }; struct Phone //类型声明 { char brand[20];//品牌 int capacity;//内存容量 }; struct Phone p1 = { "华为",256};//常见初始化 struct Phone p2 = { .capacity = 256,.brand = "小米" };//指定顺序的初始化 //代码3 struct Node { int data; struct Test t; struct Node* next;//结构体体嵌套初始化 }n = { 10,{'h','y'},NULL}; struct Node nn = { 100,{'z','g'},NULL };//结构体嵌套初始化

2.结构成员访问操作符 2.1 (.)- 结构体成员的直接访问

对结构体成员直接访问使用点(.)操作符。点操作符接受两个操作数。例如:

#include struct Water { char brand[20];//品牌 int capacity;//容量 int price;//价格 }; int main() { struct Water w = { "娃哈哈",596,1.5 }; printf("%s %dml %d元\n", w.brand, w.capacity, w.price); return 0; }

【运行结果】

使用方式:结构体变量.成员名

2.2 结构体成员的间接访问

有时候我们会使用指向结构体指针进行操作。这时就要使用这种箭头(->)操作符。如:

#include struct Test { int a; int b; }; int main() { struct Test t = { 2,6 }; struct Test* pt = &t; pt->a = 3; pt->b = 9; printf("a = %d, b = %d\n", pt->a, pt->b); return 0; }

【运行结果】

 使用方式:结构体指针->成员名

综合使用如下:

#include #include struct Person { char name[20];//姓名 int age;//年龄 }; void PrintPs(struct Person p) { printf("%s %d\n", p.name, p.age); } void set_ps(struct Person* ps) { strcpy(ps->name, "王五"); ps->age = 24; } int main() { struct Person p = { "赵六",18 }; PrintPs(p); set_ps(&p); PrintPs(p); return 0; }

【运行结果】

2.3 结构体的特殊声明

在声明结构体的时,可以不完全声明。

如:

//匿名结构体类型 struct { int n; char c; float f; }; struct { int n; char c; float f; }n[10],*s;

可以看到上面两个结构体在在声明的时候,并没有给它类型(省略了结构体标签(tag)).

留意:如果在此基础上,这样写代码是非法的!!

s = &m;//非法

注意:

编译器会把将上述两个声明当成完全不同的两个类型,所以是非法的。

匿名的结构体,如果没有对结构体类型重命名的化,基本上只能使用一次。

2.4 结构体的自引用

结构体中可以包含一个类型为该结构本身的成员吗?

例如,定义一个链表的节点:

struct Node { int date; struct Node n; };

上面代码是正确吗?若是,那么 sizeof(struct Node)会是多少呢?

可以分析得出,这种写法是不行的,原因是一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大 -- 不合理!

正确的自引用写法,如下:

struct Node { int date; struct Node* next; };

当使用的typedef匿名结构体类型重命名和结构体自引用遇上时,也容易出现一些问题。比如:

typedef struct { int data; Node* next; }Node;

注意:这种写法是不可行的,原因是Node是对前面的匿名结构体类型重命名产生的。所以在匿名结构体中提前使用Node类型来创建成员变量,是不可以的。

匿名的结构体类型是不能实现这种结构体自引用的效果的!!!

如何解决这个问题呢? --- 定义结构体时不要使用匿名结构体定义。

typedef struct Node { int data; struct Node* next; }Node; 3.结构体内存对齐

我们前面已经学习了结构体的基本使用。现在学习一个新的知识点 -- 结构体内存对齐

学习计算结构体的大小。

3.1 对齐规则

掌握结构体的对齐规则:

1.结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处;

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

        对齐数 = 编译器默认的一个对齐数 与  该成员变量大小的较小值。

- VS 中默认的对齐数是 8 ;

-Linux 中 gcc没有默认对齐数,对齐数就是成员自身的大小。

3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员的最大对齐数的整数倍地址处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

分析下面结构体的大小:

小练1:

#include struct T1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct T1)); return 0; }

详解:

【运行结果】:

 小练2:

#include struct T2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct T2)); return 0; }

详解:

【运行结果】

小练3:

#include struct T3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct T3)); return 0; }

详解:

【运行结果】

 小练4:

#include struct T3 { double d; char c; int i; }; //练习4 - 结构体嵌套问题 struct T4 { char c1; struct T3 t3; double d; }; int main() { printf("%d\n", sizeof(struct T4)); return 0; }

 详解:

【运行结果】

3.2  内存对齐存在的原因

主要原因 - 性能原因:

数据结构(特别是栈)应该尽量的在自然边界对齐。原因是为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存只需一次访问,便可读取全部。假设一个处理器总是从内存中访问8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐到8的整数倍上,那么就可以用一个内存操作来读或值,否则,我们可能需要执行两次内存访问,因为一个对象,可能被存放在两个8字节内存块中。

总结:结构体的内存对齐是拿空间来换取时间的做法。

如果在设计结构体的时,既要内存对齐,又想要节省空间就要:让占用空间小的成员尽量放在一起。

如上述出现过的代码:

struct T1 //占用12个字节 { char c1; int i; char c2; }; struct T2 //占用8个字节 { char c1; char c2; int i; };

可以看到T1 与 T2成员是一样的,但是所占用的内存大小发生了改变。

3.3 如何修改默认对齐数

#pargma 这个预处理指令,它可以改变编译器的默认对齐数。

#include #pragma pack(1) //设置默认对齐数是1 struct T { char c1; int i; char c2; }; #pragma pack()//取消设置的对齐,还原为默认对齐数 int main() { printf("%zd\n", sizeof(struct T)); return 0; }

详解:

【运行结果】

结论: 结构体在对齐方式不合适的时候,可以字节修改默认的对齐数。

3.4 结构体传参 #include struct T { int arr[10]; char c; float f; }; //结构体传参 void Print1(struct T pt) { int i = 0; for (i = 0; i < 6; i++) { printf("%d ", pt.arr[i]); } printf("%c ", pt.c); printf("%lf\n", pt.f); } //结构体地址传参 void Print2(struct T* pt) { int i = 0; for (i = 0; i < 6; i++) { printf("%d ", pt->arr[i]); } printf("%c ", pt->c); printf("%lf\n", pt->f); } int main() { struct T t = { {1,2,3,4,5,6},'h',2.98 }; Print1(t);//传结构体方式 Print2(&t);//传地址方式 return 0; }

【运行结果】

 上述两种传参方式更好? --- 答案是结构体传地址方式。

原因在于:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象时,结构体过大,参数 压栈的系统开销会比较大,这样会导致程序的性能下降。如果指传递地址过去,地址用指针接收,指针的大小在64位平台下只占8个字节(32位平台下,占4个字节),开销不算太大。

 结论:结构体传参时,首选结构体的地址。

4.结构体实现位段

结构体是位段存在的基础。

4.1 位段是什么

位段的作用和结构体的对齐方式作用是类似的。并且位段的声明和结构也是类似的。有两个不同点:

1.位段的成员必须是int、unsigned int 或 signed int,在C99中位段成员类型可以选择其他类型。

2.位段的成员名后面跟着一个冒号和一个数字。

 比如:

#include //1.申请到的一块内存中,从左向右使用,还是从右向左使用,是不确定 //2.剩余的空间,不足下一个成员使用的时候,是被浪费掉,还是继续使用是未知的 struct T { int x : 2;//后面的数值是以bit(位)位单位的 int y : 6; int z : 10; int w : 30; }; int main() { printf("%zd\n", sizeof(struct T)); return 0; }

【运行结果】

 在这里T是一个位段类型。

位段T的内存大小是如何计算的?

4.2 位段的内存分配

1.位段的成员可以是int  、unsigned int 、signed int或者是char等类型。

2.位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式分配的。

3.位段涉及很多不确定因素,并且位段是不支持跨平台的,注重可移植的程序,应避免使用位段。

分析:

#include struct T { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct T t = { 0 }; t.a = 10; t.b = 12; t.c = 3; t.d = 4; return 0; }

分析:

 4.3 位段跨平台问题

1.int 位段被当成有符号数还是无符号数是不确定的。

2.位段中最大位的数目是不确定的。(16位机器最大16位,32位机器最大32位,在16机器下写26会出问题)。

3.位段中的成员在内存中从左向右分配,还是从右向左分配,尚未有标准。

4.当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余位时,剩余位是被利用起来,还是丢弃,这还不确定。

总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但有跨平台问题存在。

4.4 位段使用时需要注意的点

位段是几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配一个地址,一个字节内部的bit是没有地址的。

所以我们不能对位段的成员进行取地址(&),这样就不能使用scanf直接给位段的成员输入值,只可以放在一个变量中,然后赋值给位段的成员。

如:

#include struct T { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct T t = { 0 }; scanf("%d", &t._a);//错误写法 //正确写法 int a = 0; scnaf("%d", &a); t._a = a; return 0; }

看到这里,结构体的相关知识已经学习完毕,恭喜你又掌握了一个知识点。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3