前言
作者简介:热爱跑步的恒川,正在学习C/C++、Java、Python等。
本文收录于C语言进阶系列,本专栏主要内容为数据的存储、指针的进阶、字符串和内存函数的介绍、自定义类型结构、动态内存管理、文件操作等,持续更新!
相关专栏Python,Java等正在发展,拭目以待!
1. 结构体
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。注意:之前学过的数组里的每个成员是相同类型的变量。
1.2 结构的声明
struct tag//tag结构体的名字 { member-list; }variable-list;
例如描述一个学生:
struct Stu { //成员变量 char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; //分号不能丢
//定义学生类型 struct Stu { //成员变量 char name[20]; int age; float weight; } s4, s5, s6;//s4、5、6都是学生//全局变量 int main() { //int num = 0; //我们用类型创建变量,s1是第一个学生,s2是第二个学生,s3是第三个学生 struct Stu s1;//局部变量 struct Stu s2; struct Stu s3; return 0; }
1.3 特殊的声明
在声明结构的时候,可以不完全的声明。
比如:
//匿名结构体类型 struct { char c; int a; double d; }s1; struct { char c; int a; double d; }* ps; int main() { //ps = &s1;//err return 0; }
上面的两个结构在声明的时候省略掉了结构体标签(stu)。
那么问题来了?
//在上面代码的基础上,下面的代码合法吗? ps = &s1;
警告:
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。
1.4 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
我想将这样一个链表连接起来,那要怎么样连接呢?
struct Node { int data; struct Node n; }; //可行否? 如果可以,那sizeof(struct Node)是多少?
答案是当然不可行的
正确的自引用方式:
struct Node { int data;//4 struct Node* next;//4/8 }; int main() { struct Node n1; struct Node n2; n1.next = &n2; return 0; }
当一个结构体可以找到另外一个跟自己同类型的结构体的时候就可以用这种方法,自己的类型包含一个自己的变量是绝对不行的,而应该是自己类型包含一个自己类型的指针才是可行的
拓展一个新的知识点
typedef struct { int data; char c; } S;
typedef可以对一个匿名结构体重命名定义一个新的名字,这个S不是一个变量名而是一个类型名
在思考一个问题
typedef struct { int data; Node* next; }Node;
这样行不行?
答案是不可以,结构体是要有一个先后顺序才行,先进行大括号里面的内容,后看大括号外面的,这里的Node还没有重命名就使用时错误的,如果你硬想这样使用,那应该这样写
typedef struct Node { int data; struct Node* next; }Node;
1.5 结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单。
变量的定义:
struct S { int a; char c; }s1;//全局变量 struct S s3;//全局变量 int main() { struct S s2;//局部变量 return 0; }
这样写行不行?
当然是可行的
初始化:定义变量的同时赋初值
struct S { int a; char c; }s1; struct B { float f; struct S s; }; int main() { //int arr[10] = {1,2,3}; //int a = 0; struct S s2 = {100, 'q'}; struct S s3 = {.c = 'r', .a = 2000};//用.操作可以指定我的顺序,其他都是默认顺序 struct B sb = { 3.14f, {200, 'w'}};//结构体里面含有结构体要再用一个大括号 printf("%f,%d,%c\n", sb.f, sb.s.a, sb.s.c); return 0; }
含有指针的的结构体初始化
struct S { char name[100]; int* ptr; }; int main() { int a = 100; struct S s = {"abcdef", NULL}; return 0; }
1.6 结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
直接上代码
struct S1 { int a; char c; }; struct S2 { char c1; int a; char c2; }; struct S3 { char c1; int a; char c2; char c3; }; int main() { //探讨到底是下面三个答案的哪个 //5 6 7 //8 8 8 //8 12 12 printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); printf("%d\n", sizeof(struct S3)); return 0; }
答案却是 8 12 12,这是为什么呢?
首先得掌握结构体的对齐规则:
结构体的第一个成员永远都放在0偏移处
从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍处
这个对齐数是:成员自身大小和默认对齐数的较小值
备注:
VS环境下 没有默认对齐数,没有默认对齐数时,对齐数就是成员自身的大小
当成员全部存放进去后
结构体的总大小必须是,所以成员的对齐数中最大对齐数的整数倍
如果不够,则浪费空间对齐。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
当换一下位置时:
如果不相信可以求一下他们的偏移量
求偏移量需要用到一个offsetof宏
#include <stddef.h> struct S { char c; int a; }; int main() { struct S s = {0}; printf("%d\n", offsetof(struct S, c));//0 printf("%d\n", offsetof(struct S, a));//4 return 0; }
当了解这个规则后,我们再练习一个
struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S2)); return 0; }
图片讲解:
再练习一个
struct S3 { double d; char c; int i; };
再练习一个结构体嵌套的问题
struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { printf("%d\n", sizeof(struct S4)); return 0; }
图片讲解:
为什么存在内存对齐?
不存在内存对齐的样子:
存在内存对齐的样子:
大部分的参考资料都是如是说的:
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
//例如: struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
1.7 修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#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() { //输出的结果是什么?//结果为6 printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
1.8 结构体传参
直接上代码:
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函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。