中山华企立方网站建设公司,中山微网站建设报价,个人微信公共号可以做微网站么,动易网站cms引言
能看到结构体#xff0c;说明C语言想必学习的时间也不少了#xff0c;在之前肯定也学习过基本数据类型#xff0c;包括整型int#xff0c;浮点型float等等。可是在日常生活中#xff0c;想要描述一个事物并没有那么简单。比如#xff0c;你要描述一本书#xff0c…引言
能看到结构体说明C语言想必学习的时间也不少了在之前肯定也学习过基本数据类型包括整型int浮点型float等等。可是在日常生活中想要描述一个事物并没有那么简单。比如你要描述一本书关于本书需要描述出书名定价以及作者等信息单靠整数或者是字符数组都没办法一次性描述清楚这里就引申出了一种新的可自定义类型——结构体。在一个结构体中可以定义多种相同或者不同的数据类型有了结构体我们便可以根据需要创建自己的数据类型来描述一些复杂的事物了。同时本篇还要介绍以下另外两种自定义类型联合体和枚举。
结构体struct
结构体的创建以及应用
下面写出结构体基本定义
struct tag //tag为类型名称
{member-list;//这里可以定义多行数据类型
}variable-list; //变量创建可以在此创建结构体变量 在描述完上方定义后可能你会有些疑惑但别急接下来给大家举个例子现在我们定义一个关于学生的结构体
struct Stu //这是创建了一个结构体类型
{char name[30];int age;char number[30];
};
struct Stu student{zhangsan,19,2003020102};
//这时在创建结构体类型后运用其创建结构体变量,同时对变量初始化
struct Stu //这时创建类型的同时创建了三个结构体变量
{char name[30];int age;char number[30];
}student1, student2, student3;像上方这样 我们的结构体类型以及变量就创建好了在创建的同时还可以进行初始化。下面代码将应用这些变量。以结构体变量.成员的形式来访问结构体变量中的元素。
#include stdio.h
struct Stu
{char name[30];int age;char number[30];
};
int main()
{struct Stu student { zhangsan,19,2001040302 };printf(%s\n%d\n%s\n, student.name, student.age, student.number);return 0;
}结构体指针形式访问
运用一个指针指向结构体通过-符号可以直接访问元素下面是使用样例。
#include stdio.h
struct Stu
{char name[30];int age;char number[30];
};
int main()
{struct Stu student { zhangsan,19,2001040302 };//printf(%s\n%d\n%s\n, student.name, student.age, student.number);struct Stu* p;p student;printf(%s\n%d\n%s\n, p-name, p-age, p-number);return 0;
}结果依然相同
结构体传参
现在看下面这样一组代码
#includestdio.h
struct S
{int data[1000];int num;
};
void print1(struct S t)
{printf(%d %d\n, t.data[1], t.num);
}
void print2(struct S* ps)
{printf(%d %d\n, ps-data[1], ps-num);
}
int main()
{struct S s { {1,2,3,4,5},100 };print1(s); //传递结构体print2(s);//传递结构体地址return 0;
} 这两种传递的方式都是正确的但是直接传递结构体的方式却有很大的弊端就是直接传递传递结构体需要重新开辟一片新的空间当结构体变量比较大的时候是极其消耗栈的内存空间的因此会降低计算机运行效率。但是传递结构体的过程中有时并不想改变其中元素怎么办呢我们可以给结构体加上const从而保护其中元素使其在函数中无法通过指针改变结构体中的变量。
#includestdio.h//此代码很好的在运行函数时保护了结构体变量s
struct S
{int data[1000];int num;
};
void print2(const struct S* ps)
{printf(%d %d\n, ps-data[1], ps-num);
}
int main()
{struct S s { {1,2,3,4,5},100 };print2(s);//传递结构体地址return 0;
}结构体特殊声明 在结构体声明中有一些特殊的声明它们没有类型名同时没有对应类型也被称作匿名结构体。这种结构体的变量只能在声明时创建。
#includestdio.h
struct
{int a;char b;
}x;
struct
{int a;char b;
}*p;
int main()
{p x;return 0;
}
当你写出上方这样的代码时编译器会报错因为匿名结构体没有对应的类型就算元素相同编译器也会将它们当成不同类型。
给结构体类型起名typedef
#includestdio.h
typedef struct
{int a;char b;
}S;//此时类型名为S
int main()
{S data { 20,x };//此时可以用类型名S来这样初始化结构体变量return 0;
}
结构体的自引用
奇思妙想一下能否将结构体的元素定义成定义的结构体呢 像以下这种方式
struct Node
{int data;struct Node next;
};
其中data表示的是存储在节点Node中的元素而next放置的是下一个结构体元素其中包含的元素同样是一个data和一个Node。这样的想法很美好但如果将结构体自己定义成自己的元素那么一层一层套下去最后导致的结果就是使Node的大小无法被定义故此方式不可行。
不过我们可以采取另一种方式将next定义成一个结构体指针指向下一个结构体像下面这样
struct Node
{int data;struct Node* next;
};
data是节点存储的数据而next指向下一个结构体的地址就像一个链条一样将结构体串联了起来这就是数据结构中链表的内容了不是本篇内容的重点不做深究。
结构体内存对齐
在学习结构体中不知道你是否考虑过这样的问题结构体是怎么分配内存的接下来我们将通过一个代码引入这个问题
#includestdio.h
struct s1
{char c1;char c2;int i;
};
struct s2
{char c1;int i;char c2;
};
int main()
{printf(%zd %zd\n, sizeof(struct s1), sizeof(struct s2));return 0;
}
试着猜猜上面代码会打印什么结果呢 看看有没有出乎你的意料
是的 由于平台存在内存对齐故按我们假象所计算的结果与真实编译器实现不同。
下面讲讲对齐规则 对齐规则 1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处 2.其他成员变量要对齐到某个数字对齐数这里的对齐是成员变量元素所占地址大小比如char的对齐数是1而int的对齐数为4的整数倍的地址处。 注意对齐数min{编译器默认对齐数该成员变量大小}即两者的较小值。VS2022的默认对齐数是8而Linux中gcc没有默认对齐数即对齐数是成员变量自身大小。 3.结构体总大小为最大对齐数结构体中每个成员变量都有一个对齐数所有对齐数中最大的整数倍。 4. 如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。 根据上述的规则我们可以再来看看代码跟具规则来计算开辟的空间
先来分析第一个首先char c1由对齐规则1可知开辟到0地址处然后到了char c2char对齐到1的整数倍所以开辟在1地址处当开始开辟int类型i的内存时由于i的对齐数是4所以要对到4的整数倍上所以最后在4的位置开辟空间而地址23的内存就被浪费了所以是空。开辟结束时所占内存大小为8刚好是最大对齐数4的整数倍所以最终结构体s1所占内存为8。结果见下图左方。
再来分析第二个结构体首先还是开辟char c1在0处然后开辟int类型i的空间因为要与int所占内存大小4对齐数为4所以在4处开辟空间123被浪费。再开辟c2其对齐数为1所以在8处开辟空间在开辟完之后所占内存为9不是最大对齐数的整数倍所以还要再多开辟三个空间91011被浪费。最后开辟空间数为12。见下图右方。 到了这里大家可能又有了一些疑问为社么会存在内存对齐下面是一些参考资料给出的解答 为什么存在内存对齐 1.平台原因移植原因 不是所有的硬件平台都能访问任意地址上的任意数据某些硬件平台只能从某些特定地址处取某些特定的数据否则抛出硬件异常 2.性能原因 数据结构尤其是栈应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要做两次内存访问而对齐的内存访问仅需一次。假设一个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型数据的地址都对齐为8的倍数那么就可以用一个内存操作来读写值了。否则我们可能需要执行两次内存访问因为对象可能被分别放在8个字节内存块中。 总的来说结构体的内存对齐是拿空间换取时间的做法 修改默认对齐数
#includestdio.h
#pragma pack(1)//修改默认对齐数为1
struct S
{char c1;char c2;int i;
};//此时sizeofstruct S的值为6
#pragma pack()//取消设置的对齐数还原为默认对齐数
结构体实现位段
C语言中位段是一种数据结构允许你为结构体中的成员分配一个特定数量的位bit而不是分配完整的字节。这在需要精确控制内存分布或减少内存占用时很有用比如硬件访问和网络协议设计。对于字段的声明与结构体类似但也有不同位段通过在结构体定义中为成员后添加一个冒号和位数比特位来创建的例如
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
位段的内存分配 1.位段的成员可以是intunsigned intsigned int或者是char类型 2.空间按照四个字节int/一个字节char的方式来开辟的 3.位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段 位段的成员是顺序放置的但当当前存储单元没有足够的空间容纳下一个位段时编译器可能会不会跳转取决于编译器会跳转到下一个存储单元。这可能导致在存储单元有未使用的位。
编译器可能在位段的末尾添加填充,现在让我们假设一下VS2022的字段分配方式
现在有以下代码
#includestdio.h
struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
};
int main()
{struct S s { 0 };s.a 10;s.b 12;s.c 3;s.d 4;
}
位段中的成员在内存中从左向右还是从右向左尚未定义。
假设为从右向左分配char中首先为8个比特位首先a占三个比特位我们赋a的值为10二进制为1010经三个比特位截断得010也就是二 当赋值b的时候b占4个比特位赋值b为12二进制1100刚好取四位1100 下一个c占5个比特位由于无法放下跳转下一个位段放置的值为3二进制00011 最后d占4个比特位在第二个位段放入会超范围条赋值跳到下一个位段4二进制0100放入最终结果为下图 我们断点调试一下验证一下结果先取出结构体s的地址开始内存中存的都是0当走过s.a赋值语句时内存中的值发生了变化 当运行过s.b后 运行s.c后 运行完s.d后 经过调试验证得到最终结果假设正确注意在调试时显示的值都是以四个比特位的形式显示的故显示的值不一定等于存入的值 十六进制0x 62 03 04的二进制为01110010 000000110 00000100 位段的跨平台问题
位段的内存布局不同的编译器可能会以不同的顺序排列位段。有些编译器可能会按照声明的顺序排列位段而其他编译器可能会重新排序以优化空间或访问率
位段中的存储单元位段通常储存在整型存储单元中但不同的编译器可能会选择不同的类型作为存储单元
C中位段内存从左向右和从右向左是没有确切定义的端序影响在大端和小端中位段的物理存储顺序可能不同 注由于bit位没有地址所以位段的几个成员公用一个字节这样有些成员的起始位置并不是某些字节的起始位置那么这些位置是没有地址的所以无法通过取地址的方式为其赋值 struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
};
int main()
{struct S s { 0 };//scanf(%d,s.a);---wrong,结构体位段不可取地址//以下赋值方式均正确s.a 10;s.b 12;s.c 3;s.d 4;
}
联合体union
什么是联合体
与结构体很相似联合体也是由一个或多个成员构成这些成员可以是不同的类型来看看下面这段代码会打印什么
#includestdio.h
union u
{char c;int num;
};
int main()
{union u uu;printf(%zd\n, sizeof(uu));printf(%p\n, uu);printf(%p\n, (uu.c));printf(%p\n, (uu.num));return 0;
} 可以发现uu占用的空间只有4个字节而且后面三个地址是相同的这意味着什么呢uu.c和uu.num占用的空间在相同的位置共用一块空间当一个元素被赋值时另一个元素的值会被覆盖。因为联合体的这种特性我们还叫他共用体。
联合体与共用体比较
struct s //结构体
{char c;int i;
};
union u //联合体
{char c;int i;
}; 上图中粉色的是被浪费的空间
联合体占用内存
关于占用内存这里要严谨一点很多教课书和课程里都说联合体的大小是最大成员的大小实际上这样说是错的在联合体中也存在和结构体一样的对齐数当最大元素所需的的空间不是对齐数的倍数时会自动将其矫正为对齐数倍数见下图附上代码
#includestdio.h
union u
{char c[5];int i;
};
int main()
{union u uu;printf(%zd\n, sizeof(uu));return 0;
} 联合体的应用
也许你会问计算机内存这么大联合体节省那一点空间真的有必要吗但是联合体并不主要用于计算机中在内存极其宝贵的硬件中节省这样一些空间是很有必要的。
大小端判断共用体版
在上一篇博客讲到计算机内存时曾讲过一段判断大小端的代码链接放这里可以去看里面还有讲解大小端是什么。数据在内存中的存储-CSDN博客
今天我们要用联合体编写一段代码来判断大小端见下代码
#includestdio.h
//n和s共用一块空间
//当给n赋值后用s可以随意取每个字节上的元素
int check_sys()
{union{char c;int i;}u;u.i 1;return u.c;
}
int main()
{if (check_sys()) {printf(小端\n);}elseprintf(大端\n);return 0;
} 这样的代码是不是让人眼前一亮当一个人对代码有很强的掌控力时打代码便成了一种艺术。
枚举enum
定义枚举
通过关键字enum定义。eg
enum Sex
{//这里列举枚举类型 性别 的可能取值MALE, //男FEMALE, //女SECRET //保密
};
默认情况下枚举的第一个成员的值是0后续成员值依次递增但同时我们可以给其指定值
enum Sex
{//这里列举枚举类型 性别 的可能取值MALE 3, //男FEMALE 5, //女SECRET 9 //保密
};
枚举和#define定义常量很像枚举变量里定义的值是符号常量。
故可以这样使用
#includestdio.h
enum Sex
{//这里列举枚举类型 性别 的可能取值MALE, //男FEMALE, //女SECRET //保密
};
int main()
{enum Sex zhangsan MALE;if (zhangsan MALE) {printf(张三是男的\n);}else printf(张三是女的\n);return 0;
} 枚举的优点 1.代码可读性枚举常量的使用可以提高代码的可读性和可维护性 2.类型安全枚举提供了一个类型安全的方法来表示一组整数值 3.调试方便调试时枚举变量可同时显示其字符常量和其值便于观察 结语 到了这里结构体联合体和枚举就介绍完了一篇博客费时费力画图也是一大难点看在我这么辛辛苦苦写博客的份上给个小小的赞不过分吧。如果感觉这篇博客对你有帮助的话还请给个小小的赞再走啊比心---♥