网站模板网,厦门唯一官方网站,wordpress获取父分类,动画制作网页一.前言
在C语言中#xff0c;不仅有int、char、short、long等内置类型#xff0c;C语言还有一种特殊的类型——自定义类型。该类型可以由使用者自己定义#xff0c;可以解决一些复杂的个体。
二.结构体
2.1结构体的声明
我们在利用结构体的时候一般是用于描述一些有多种…一.前言
在C语言中不仅有int、char、short、long等内置类型C语言还有一种特殊的类型——自定义类型。该类型可以由使用者自己定义可以解决一些复杂的个体。
二.结构体
2.1结构体的声明
我们在利用结构体的时候一般是用于描述一些有多种因素的对象。比如我们要描述一个学生那么学生就有他的姓名性别年龄如果我们使用内置类型的话非常麻烦我们就可以定义一个结构体类型用来描述一个学生
//定义了一个学生类型
struct student
{char name[15];//学生姓名char sex[5];//性别int age;//年龄
}; 我们创建了类型之后还得依靠此类型创建变量。
2.2结构体变量的创建和初始化
我们要时刻记住我们创建的是一个类型而不是一个变量我们要根据此类型来创建我们需要的变量。 下来我们依据上面创建的学生类型来创建变量并进行初始化
#include stdio.h//定义了一个学生类型
struct student
{char name[15];//学生姓名char sex[5];//性别int age;//年龄
};int main()
{//在创建变量的同时进行初始化//该初始化必须得按照结构体内部成员的顺序进行初始化struct student stu1 { zhangsan,nan,18 };printf(%s\n, stu1.name);printf(%s\n, stu1.sex);printf(%d\n, stu1.age);//利用访问操作符可实现自定义顺序进行初始化struct student stu2 { .age 21,.name lisi,.sex nv };printf(%s\n, stu2.name);printf(%s\n, stu2.sex);printf(%d\n, stu2.age);return 0;
}
我们除了在主函数内创建结构体变量外还可以在定义结构体的同时进行创建变量
#include stdio.hstruct student
{char name[15];char sex[5];int age;
}stu1;int main()
{struct student stu2 { 0 };return 0;
}
stu1和stu2都是结构体类型变量区别是stu1是全局变量存放在静态区而stu2是局部变量存放在栈上。
2.3结构体访问操作符
结构体访问操作符有.句点操作符和-箭头操作符。句点操作符用于结构体变量的访问而箭头操作符用于结构体指针来访问结构体变量。该操作符我在之前已经介绍过了大家可以看我之前的博客。结构体中的访问运算符-CSDN博客https://blog.csdn.net/xsc2004zyj/article/details/136722599?spm1001.2014.3001.5502 2.4结构体的特殊声明
结构体除了正常的声明外还有一种特殊的声明方式叫做匿名结构体。
匿名结构体就是在声明结构体的时候不给结构体标识符而直接创建一个变量。
//匿名结构体该结构体没有标识符
struct
{char name[15];char sex[5];int age;
}stu;
而对于匿名结构体类型来说该结构体基本上只能使用一次因为该结构体没有标识符后续没法再对此来创建变量。
下面提出一个问题
#include stdio.hstruct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}*p;int main()
{p x;return 0;
}
主函数中的p x是否合法
答案是非法因为以上两个结构体的成员变量完全一样并且都使用匿名的方式所以编译器会把两个声明当成两个完全不同的类型所以是非法的。
2.5结构体的自引用
如果我们在描述某个对象的时候需要用到结构体的自引用我们应该如何写
struct stu
{int age;struct stu s;
};
上面代码正确么如果正确那么sizeof(struct stu)的大小是多少呢上面的代码其实是错误的如果这样进行自引用那么一个结构体变量的空间将会无穷大因为一个结构体里面永远还有一个结构体。
正确的应该是存放一个该结构体的指针因为一个指针的大小不是四个字节就是八个字节。而且该指针也指向了一个该类型的变量所以也实现了结构体的自引用。
struct stu
{int age;struct stu *s;
};
2.6利用typedef重命名结构体类型
我们在创建结构体变量的时候每次都要写出struct tag x这样写起来非常麻烦并且有人可能粗心大意而忘记struct。所以我们在声明结构体的时候可以利用typedef关键字给该结构体重新起个名字用该名字来创建变量。
#include stdio.htypedef struct student
{char name[14];int age;
}stu;int main()
{stu s;struct student s2;return 0;
} 此时在声明时分号前面就不是创建变量了还是该结构体的一个新名字——stu。在创建变量的时候stu和struct student是一个意思。 我们也可以利用typedef来解决匿名结构体的问题我们只需要给匿名结构体重新起一个名字利用该名字创建变量就行了。这时该匿名结构体与正常声明的结构体没有区别。 #include stdio.htypedef struct
{char a;int b;
}X;int main()
{X x;X s;return 0;
} 这样就解决了匿名结构体只能使用一次的局限。我们可以利用重命名的结构体进行创建变量初始化等操作。 三.结构体内存对齐
我们已经基本了解了结构体的内容下来我们来讨论结构体中的热门话题结构体的大小。这也是最近热门的考点结构体内存对齐。
3.1对齐规则
在理解内存对齐之前我们得先了解结构体内存对齐的规则。 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处其他成员变量要对齐到某个数字对齐数的整数倍的地址处。对齐数 编译器默认的一个对齐数 与 该成员变量大小间的较小值。VS上的默认对齐数 8。Linux中gcc没有默认对齐数对齐数就是成员自身的大小结构体的总大小等于最大对齐数结构体的每一个成员变量都有对齐数所有对齐数中最大的一个的整数倍。如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。 下来我来一一介绍结构体的对齐规则。我们通过一个练习来引出
struct S1
{char c1;int i;char c2;
};
int main()
{printf(%d\n, sizeof(struct S1));return 0;
}
我们利用该结构体来进行解说判断该结构体的大小。有的人会想该结构体的成员两个char一个int那就占6个字节是这样么
我们看到结果是12个字节与我们的猜测不符。这就是因为结构体内部存在内存对齐规则。 在上图我借助练习题详细介绍了结构体对齐规则如何理解以及内存是如何进行对齐的。大家先仔细理解上图后进行下面几个结构体大小的练习。
3.1.1练习1
#include stdio.hstruct S2
{char c1;char c2;int i;
};int main()
{printf(%zd\n, sizeof(struct S2));return 0;
} 3.1.2练习
#include stdio.hstruct S3
{double d;char c;int i;
};int main()
{printf(%zd\n, sizeof(struct S3));return 0;
} 3.1.3练习3
#include stdio.h
//结构体嵌套struct S3
{double d;char c;int i;
};struct S4
{char c1;struct S3 s3;double d;
};int main()
{printf(%zd\n, sizeof(struct S4));return 0;
} 3.2为什么存在内存对齐
大部分的参考资料是这样说的
3.2.1平台原因移植原因
不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定 类型的数据否则抛出硬件异常。
3.2.2性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数那么就可以用⼀个内存操作来读或者写值了。否则我们可能需要执行两次内存访问因为对象可能被分放在两个8字节内存块中。 总的来说结构体的内存对齐是为了拿空间换取时间的做法。 3.2.3那么如何尽可能节省空间的浪费呢
我们来分析一下这两个结构体
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};
S1和S2的成员类型都一样只是存储的顺序不同那这两个结构体的大小是否一样大呢
我们看到S1和S2的成员虽然相同但是两者的大小却不同我们来分析一下。 那么我们在设计结构体的时候既要满足对齐又要节省空间如何做到 让占用空间小的成员尽量集中到一起。 3.2.4修改默认对齐数 #pragma这个预处理指令可以修改编译器的默认对齐数 //利用#pragma修改默认对齐数#include stdio.h
#pragma pack(1)//设置默认对⻬数为1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对⻬数还原为默认 int main()
{//输出的结果是什么 printf(%d\n, sizeof(struct S));return 0;
}
我们在上面已经分析该结构体的大小为12个字节在VS默认8为对齐数的情况下而我们利用#pragma已经将编译器的默认对齐数修改为1结果还是12个字节么 我们分析后得出修改默认对齐数后该结构体的大小变成了6字节。
四.结构体传参
我们创建好结构体变量后可能需要把该结构体变量的某个成员变量传给某个函数。而我们到现在由两种传参的方式值传递和地址传递。那我们应该选择哪一种方式对结构体变量进行传参呢
//结构体传参#include stdio.hstruct S
{char c1;char c2;int i;
};//值传递
void test1(struct S s)
{printf(%c\n, s.c1);printf(%c\n, s.c2);printf(%d\n, s.i);
}//地址传递
void test2(struct S* ps)
{printf(%c\n, ps-c1);printf(%c\n, ps-c2);printf(%d\n, ps-i);
}int main()
{struct S s { a,b,10 };test1(s);test2(s);return 0;
} 我们看到无论是值传递还是地址传递都可以达到我们的目的。那我们到底应该选那种方式呢
我们首先要知道值传递中的形参是实参的一份临时拷贝会在栈上存储其拷贝内容也就意味着会消耗内存那如果该结构体的大小非常大的话我们在栈上就会消耗很多内存。
无论是何种传参方式都会进行压栈操作而值传递在压栈过程中机会在时间和空间上有大量的系统开销。而地址传递的话指针的大小不是8个字节就是4个字节在压栈过程中不会有太多空间和时间上的开销。 所以在结构体传参的时候要传结构体的地址 五.结构体实现位段
利用结构体实现位段。位段这个概念大家可能没听过但段位大家肯定不陌生。位段是一种特殊的结构体类型位段必须得依靠结构体来实现。
5.1什么是位段
位段的声明和结构体是类似的但有两个不同 位段的成员必须是int 、unsigned int、或者signed int在C99中位段成员的类型也可以选择其他类型。位段的成员后面有一个冒号和一个数字。 比如
struct S
{int _a : 2;int _b : 5;int _c : 10;int _e : 30;
}; 通常在创建位段式结构体的时候习惯在每个变量前面加上下划线以此来说明这是位段式结构体。
我们看到结构体和位段式结构体的区别就在于位段的每个成员后面多了一个冒号和一个数字这是什么意思呢
这每个成员后面的数字表示该成员占的bit位_a占2个bit位_b占5个bit位_c占10个bit位。为什么要这样设置变量呢
我们想如果_a里面只会存123这三个数1二进制就是012二进制就是103二进制就是11所以存这三个数用两个bit位就够了。以此类推_b就是存只需5个bit位就能表示的数_c就是只存10个bit位就能表示的数。这样就可以大大减少空间的浪费。那位段式结构体是怎样储存数据的呢是如何达到节省空间的下面我们来了解位段的内存对齐规则。
5.2位段的内存对齐
位段的成员最好都是同一种类型的位段会根据成员类型来开辟空间比如成员是int型就一次开辟4个字节如果是char型就一次开辟一个字节。下面我们举一个例子。 那该位段的大小是不是跟我的结论一样呢 我们看到该位段的大小与我们的推测相同。我们再来看一下该位段是如果写入数据的。 我们分析完之后在VS上调试一下发现s的内存如下和我们分析的一样占三个字节存储的是0a 0c 05。 5.3位段的跨平台问题 int位段是被当成有符号数还是无符号数是不确定的。位段中最大位的数目是不能确定的。16位机器最大1632位机器最大32写成27在16位机器会出问题。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。在VS上默认从右向左分配当⼀个结构包含两个位段第⼆个位段成员比较大时无法容纳于第⼀个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。VS上默认舍弃 总结跟结构相比位段可以达到同样的效果并且可以很好的节省空间但是有跨平台的问题存在。 5.4位段的应用
下图是网络协议中IP数据报的格式我们可以看到其中很多的属性只需要几个bit位就能描述这里使用位段能够实现想要的效果也节省了空间这样网络传输的数据报大小也会较小⼀些对网络的畅通是有帮助的。 5.5位段使用的注意事项
位段的几个成员共有同⼀个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。内存中每个字节分配⼀个地址⼀个字节内部的bit位是没有地址的。 所以不能对位段的成员使用操作符这样就不能使用scanf直接给位段的成员输入值只能是先输入放在⼀个变量中然后赋值给位段的成员。
#include stdio.h
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{struct A sa { 0 };scanf(%d, sa._b);//这是错误的 //正确的⽰范 int b 0;scanf(%d, b);sa._b b;return 0;
}
完