公司关于网站建设的通知,百度网址提交入口平台,现在都用什么软件做网站,wordpress4.6手册 chm目录
1.结构体的声明
1.1基础知识
1.2结构体的声明
1.3结构体的特殊声明 1.4结构体的自引用
1.5结构体变量的定义和初始化
1.6结构体内存对齐
那对齐这么浪费空间#xff0c;为什么要对齐 1.7修改默认对齐数
1.8结构体传参
2.位段
2.1什么是位段
2.2位段的内存分配…目录
1.结构体的声明
1.1基础知识
1.2结构体的声明
1.3结构体的特殊声明 1.4结构体的自引用
1.5结构体变量的定义和初始化
1.6结构体内存对齐
那对齐这么浪费空间为什么要对齐 1.7修改默认对齐数
1.8结构体传参
2.位段
2.1什么是位段
2.2位段的内存分配
深入研究VS环境下的位段内存分配
2.4位段的跨平台问题 首先理解什么是自定义类型我们平时所接触的char、short、int、float、double等等这写都是内置类型这些类型都是c语言所规定好的不是我们所创造出来的生来就能为我们所用的。
其实C语言还允许我们创造一些类型这就是自定义类型
那自定义类型允许我们创建哪些类型呢
结构体类型、结构体类型、枚举类型那本章就是对自定义类型的讲解。
首先我们先了解结构体
1.结构体的声明
1.1基础知识
结构体就是一些值的集合这些值被称为成员变量结构体的每个成员可以是不同的变量。
说到集合我们会想到相同元素的集合是数组而本篇要讲的是不同种类元素的集合结构体。
1.2结构体的声明 结构体类型的关键字是struct后面的tag是结构体的标签名就是结构体的名字。
大括号里的mem-list是成员列表最后的variable-list是变量列表那如下就是结构体命名的方式
struct tag
{mem - list;
}variable-list;
接下来举个例子简单使用一下结构体类型
在这里我用一个学生信息来讲解
//定义一个学生类型
struct student
{char name[20];int age;float weight;
};
一一对照着上面的结构体命名方式struct是结构体关键字tag对应着student结构体名字在这里我们将其命名为studentmem-list对应着结构体的成员变量可以有多个最后的变量列表如下和主函数讨论代码如下
struct student
{char name[20];int age;float weight;
}s4,s5,s6;//全局变量
int main()
{int num0;struct student s1;//局部变量struct student s2;struct student s3;return 0;
}
主函数中定义的struct student s1类型其实就和int num相同结构体类型可以在主函数中定义s1s2s3也可以在变量列表中定义s4s5s6区别就是s1s2s3是局部变量
而s4,s5,s5是全局变量所以变量列表可以有也可以没有。
1.3结构体的特殊声明
当我们声明结构体类型的时候将结构体名省略掉时我们将其称为匿名结构体类型
那想要用这个匿名结构体在主函数中定义局部变量可以吗
答案是不行因为没有名字 我们可以看到可以在匿名结构体类型可以在变量列表中定义全局变量但不能定义局部变量也就只能用在结构体类型的变量列表中定义全局变量。
那下面整点儿花活儿
struct
{char name[20];int age;float weight;
}s1;
struct
{char name[20];int age;float weigt;
}* pa;int main()
{pa s1;return 0;
}
我们在定义一个结构体类型的指针pa将其指向匿名结构体变量s1在主函数中将s1的地址赋给指针变量pa看到这样的代码可能就会觉得两个自定义结构体类型的成员变量都一样那把s1放到pa中去这两个匿名变量的成员类型 这就是两个同一种类型的变量。
但是当我们实现的时候编译器会报错 意思就是说呢虽然两个结构体类型的成员变量一摸一样但是在编译器看来就是两个完全不同类型的变量那这些都是匿名结构体的错误的使用方法
如下才是匿名结构体正确用法
匿名结构体只能在创建结构体的时候定义好变量不能再定义局部变量因为没有结构体名 1.4结构体的自引用
结构体中包含一个类型为该结构本身成员是否可以呢
讲解结构体自引用前先带大家简单了解一下数据结构的一些内容 我们要储存1 2 3 4 5这几个数字的时候就是找几块连续的空间将他们储存进去就像数组一样。
那我们把这样连续存储的方式就叫做顺序表。
那数据的储存也有可能是乱序的 像这样乱序的我们可以让1找到2让2找到3以此类推找到5就不需要再往后找了所以我们只需要找到1的位置就可以找到后面数字的位置这样的方式像个链条一样把1 2 3 4 5穿起来那么这样的数据储存方式就叫做链表顺序表和链表都像一条线一样将他们穿起来储存我们就称他们为线性数据结构。
我们将像这样存放1 2 3 4 5 数据的就叫做一个结点那我们每次访问节点时只需要访问一个节点就可以将他们的全部内容都访问到
那我如何访问到下一个节点呢
下一个节点和这一个节点的类型都是一样的那一个节点既要储存自己的数据又要和下一个节点建立关系
我们不妨使用结构体类型
struct Node
{int data;//大小为四个字节struct Node* next//大小为四个字节或八个字节
}; 我们先定义一个结构体类型Node在成员变量中定义一个int类型的data来存放自己的数据定义一个struct Node*类型的next这样就和下一个数据建立了
在这里要非常注意的是访问下一个数据的时候一定要使用指针的类型因为我们不知道链接到的下一个数据的大小不知道他内存的多少而使用指针时访问它的地址同样也可以做到访问数据的效果所以在成员变量内要使用指针结构体类型可以同时确定下一个数据的大小和位置
那我们接下来的操作就是将两个数据连接起来
struct Node
{int data;struct Node* next;
};
int main()
{struct Node n1;struct Node n2;n1.nextn2;return 0;
}
我们定义两个结构体变量n1和n2将n2的地址赋给n1中的next中去这样n1就有能力找到n2了就相当于一个链条将两个数据串起来了
所以当一个结构体要找到与另一个跟自己相同的结构体时就可以使用这样的方法。
1.5结构体变量的定义和初始化
非常简单直接上代码举例子
struct student
{char name[20];int age;float weight;
}s4, s5, s6;
int main()
{struct student s1;struct student s2;struct student s3;return 0;
}
其实无非就是两种方式
一种是直接定义在结构体后面的全局变量一种是定义在main函数中的局部变量
我们创建完变量后就要初始化就像我们定义别的类型如int类型时要对其进行初始化要给他赋予一个初始值
那结构体的初始化与我们所学的数组是相同
struct S
{int a;char c;
}s1;
int main()
{int arr[10] { 1,2,3};struct S s2 {100,u};return 0;
}
首先定义一个结构体成员变量有int类型的a和char类型的c再观察main函数中数组的初始化的方法需要大括号括起来那结构体初始化也要大括号括起来再往大括号中按顺序输入结构体的成员变量的初始化内容即可
那这里初始化的内容就是将100赋给a将dudu付给了char
那结构体的自引用如何初始化呢qishi struct S
{int a;char c;
}s1;
struct B
{float f;struct S s;
};
int main()
{struct B sb { 3.14,{100,u} };return 0;
} 其实也很简单我们只需要在大括号中再带上一个大括号就好了我们调试起来看一下这些值到底有没有初始化给我们的变量呢 我们可以看到确实都是我们想要初始化的值。
当然也可以不用按顺序来初始化不按顺序的初始化如下
struct S
{int a;char c;
}s1;
int main()
{struct S s3 { .c w,.a 100 };return 0;
}
我们只需要在大括号内写成 . 加上成员变量名再加上想被赋予的值就可以了调试验证 结果在意料之中
那我们如何将这些保存好的数据拿出来用呢
也非常的简单
int main()
{struct B sb { 3.14,{100,w} };printf(%f %d %c,sb.f,sb.s.a,sb.s.c);return 0;
} 结构体的操作符时一个‘ .’所以在输出时要用结构体变量名成员变量名 输出的也是我们想要的。
1.6结构体内存对齐
接下来要分享的是结构体的一个重点如何计算结构体的大小
struct S1
{int a;char c1;
};
struct S2
{char c1;int a;char c2;
};
struct S3
{char c1;int a;char c2;char c3;
};
int main()
{printf(%d\n, sizeof(struct S1));printf(%d\n, sizeof(struct S2));printf(%d\n, sizeof(struct S3));return 0;
}
给出这样一串代码预测一下输出值 你预测对了吗
实际上结构体在内存中存放时是要完成对其这样一个操作要把这些结构体放在一个对齐的边界上进行操作而不是乱存放我们利用表格来进行解释 这个图意思就是说结构体的第一个变量永远都放在0偏移处就是箭头所指向的地方那整形变量a占4个字节如下图 从第二个成员开始以后的每个成员都要对齐到某个对齐数的整数倍处
对齐数是成员自身大小和默认对齐数的较小值
这个例子中c1的大小是1
那在vs的环境下对齐数默认值是8那再别的没有默认对齐数时对齐数就是成员自身的大小。
那按照这么分析char c1自身大小是1默认对齐数是8那取较小值那他的对齐数就是1。
那c1只要对齐的1的倍数处就可以了所以我们按偏移量顺序存放并且这些偏移量都是1的倍数所以可以存放在4的位置现在的储存状况如下图 那刚刚输出的结果不是8吗为什么只占了五个位置
接下来就要说重要的第三点当成员全部存放进去后结构体总体的大小必须是所有成员对齐数中最大对齐数的整数倍如果不够则浪费空间。
int类型的对齐数是4char类型的对齐数是1这两个变量中最大的对齐数是4那结构体的大小必须是4的倍数那刚刚占了五个空间会继续浪费掉三个空间 那这样就会得出8个空间。实际上a和c只占了5个空间剩下三个字节的空间因为要对其所以浪费掉了。
那我们接下来将两个变量的位置反过来将c1定义在上面将a写在下面会是什么样呢 struct S1
{char c1;int a;
}; 当我们写成这个样子的时候
偏移量的占用如下 因为0被占用后1、2、3 都不是4的倍数所以a要从第四位开始占位
所以这种写法的情况下1、2、3号的相对偏移量就会被浪费掉
那么我们调试起来验证一下 我们在监视中取地址可以看到确实c1和a之间隔了三个地址也就是浪费了三个字节
那还有一种方法是offsetof函数他的作用就是测量偏移量 我们可以看到它其实是一个宏返回值是一个整形的值那使用时不要忘记包含头文件stdef.h,使用代码如下
#includestddef.h
struct S
{char c;int a;
};int main()
{struct S s;printf(%d\n, offsetof(struct S, c));printf(%d\n, offsetof(struct S, a));return 0;
} 我们可以看到c的偏移量是0a的偏移量是4上面的图示给大家演示过过程复杂希望大家能够理解。
那对齐这么浪费空间为什么要对齐
大部分平台都有两个原因
1.平台原因
不是所有的硬件平台都能访问任意地址上的任意数据某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常也就是说某些机器上是不能让操作者去操作一些数据的所以我们只能将一些地址对应在特定的位置边界上在边界上取数据就可以了所以要对齐
2.性能原因
数据结构尤其是栈应该尽可能在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要做两次内存访问而对齐的内存访问只需要一次就可以了也就是说对齐可以提升效率。
用图例给大家简单解释一下 先说明如果是在32位的机器上每次读取数据是四个字节所以当我们未对齐时每次读取四个字节要两次才能读取完这个数据如未对齐的紫色读取方式但是当我们数据对齐的时候访问c的地址时只需跳过浪费掉的字节数访问a的地址就可以了。
所以总的来说结构体内存对齐就是用空间换取时间的做法。
那我们在平时设计代码的时候就要注意一些创建变量时位置的问题
尽量将占用空间较小的类型集中在一起这时原本可能浪费掉的空间就会被利用上这样也就一定程度上的节省了我们的空间。 1.7修改默认对齐数
如果我们觉得我们使用的环境设置的默认对齐数不太合适的话我们当然也是可以手动修改的
修改需要使用#pragma这个预处理命令使用如下
#pragma pack(8) 后面括号的内容 就是默认对齐数我们可以对其进行修改
#pragma pack()
那我们如果又想取消设置的默认对齐数秩序将括号里的数字去掉就可以恢复为原本的默认对齐数。
我们不妨对其简单实用试一下代码如下
#pragma pack(1)
struct S
{char c1;//1int i;//4char c2;//1
};
#pragma pack()int main()
{printf(%d, sizeof(struct S));return 0;
}我们可以看到原本在默认对齐数是8的情况下我们将默认对齐数改成1此时输出为6
我们的环境提供了这样一个方式来适应自己的开发简单了解一下。
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函数。 因为函数传参的时候参数是需要压栈会有时间和空间上的系统开销。 如果传递一个结构体对象的时候结构体过大参数压栈的的系统开销比较大所以会导致性能的 下降。
那我们可能还会有疑问print1函数传的是值我们如果不小心修改掉struct S s中的s的内容也不会影响到printf1(s)中的s那这种形式是否更加安全一些呢而如果我不小心改掉print2中的指针则会修改掉print2打印函数中打印函数的内容这样不就不安全吗
其实完全可以使用const修饰指针
void print2(const struct S* ps)
{printf(%d\n, ps-num);
}
这样子加上const指针就没有能力修改指针所指向的内容。
所以在以后的代码中尽量保证结构体传参这样保证我们的效率能高一些。
2.位段
讲完结构体就得讲讲结构体实现位段的能力
2.1什么是位段
位段的声明和结构是类似的但有两个不同
1.位段的成员通常是int、unsigned int或signed int
2.位段的成员后必须有一个冒号和一个数字
举个例子
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
这就是位段的一个简单的定义可以看到和结构体有区别也有相似的地方。
那冒号后面的数字和冒号到底是什么意思呢
我们先从他所占的内存来入手了解直接上代码
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
int main()
{struct A sa { 0 };printf(%d\n, sizeof(sa));return 0;
}
可以预测一下输处结果 其实位段中的‘位’是指二进制位
也就是说冒号后面的数字表示几个二进制位
struct A
{int a : 2;//表示两个二进制位int b : 5;//表示五个二进制位int c : 10;//表示十个二进制位int d : 30;//表示三十个二进制位
};
如上述代码后的注释所示那这样的注释不是和我们之前所学的int不是占32个比特位吗
需要注意是我们在设计结构体的时候我们在成员变量a中存放的数据是非常有限的我只要存放0 1 2 3四种状态 其对应的二进制分别是00 01 10 11 我们就会发现只有两个比特位就可以表示0或1或2 或3但是如果我按正常的32位比特位给int类型开辟空间但是我只用到两个比特位所以剩下的30个比特位都浪费掉了那这时候为了节省空间让他的成员变量所占的空间小一点就有了位段的概念。
也就是说如果如果我给他两个比特位就足以表达这个成员变量想要表达的意思那就完全足够了。这就是位段做到了更加节省空间的一种方式。
2.2位段的内存分配
位段的空间上是按照需要以4个字节 int 或者1个字节 char 的方式来开辟的
那我们继续用上述代码举例
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
当这个位段放到我们们面前时我们发现它的成员是int那我们就一次开辟四个字节也就是32个比特位a先用去2个还有30个b用去5个还剩25个c用去10个还剩15个最后d要用30个那15个比特位不够给d分配怎么办呢我们重新申请int类型的空间4个字节也就是32个比特位d用去30个还剩两个问题来了那前面15剩下的空间还要不要呢
这样的事情完全取决与不同的编译器
实际上C语言标准并没有规定这样的空间要不要被利用掉。
所以位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段
不过都不影响我们最后的结果不管上面的d有没有用去15个剩下的空间我们都开辟了第二个四个字节的空间大小所以这个位段占用了8个字节的内存输出结果也能对的上上图所示了 深入研究VS环境下的位段内存分配 接下来继续研究在VS平台上数据分配的方向和空间如何使用等等问题
依然使用代码举个例子
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;return 0;
}
这段代码的意思就是先开辟char类型的空间因为char只占一个字节所以a占三个字节b占四个字节c占五个字节d占四个字节但是在进入主函数中struct S s又将他们的空间全部初始化成0所以之前不管他们的空间是多少被初始化为0后他们所占的空间就又是0了再往下就是给ab,c,d赋值了。
我们先观察并讨论位段中的内容
看到成员都是char类型就一次开辟一个字节的空间也就是八个比特位图示如下 我们先假设位段中的数据是从低地址到高地址的所以我们先给a开辟三个比特位的空间a用去3个比特位还剩5个比特位b用去4个比特位还剩1个那我们继续先假设不会占用多于出来的空间所以使用给c开辟空间时我们必须在申请一个字节的空间也就是八个比特位来存放c的内容c用去5个比特位还剩3个比特位这时不够存放d的数据继续申请一个字节的空间来存放b这时b就占用4个比特位
到现在我们一共开辟了三个字节的空间消耗了三个字节的空间
那事实是否和我们假设的一样呢
我们在观察并研究主函数中的内容
主函数中的内容就是将这写空间都初始化位0并将内容都放在这些内存空间中来验证存放的顺序。
int main()
{struct S s { 0 };s.a 10;s.b 12;s.c 3;s.d 4;return 0;
}
第一要将10存到a中10的二进制表达是1010但是a只能存放3个比特位所以就存放010
第二要将12存到b中12的二进制表达是1100b可以存放4个比特位所以存放1100
第三要将3放进c中3的二进制表达是11但是要存放五个比特位前面补三个0所以存放的是00011
第四个要将4存到d中4的二进制表达位100d可以存放4个比特位所以前面补一个0所以存放的是0100
这些存放的前提是
1.分配到的内存中的比特位是从右向左使用的
2.一个字节内分配的内存剩余的比特位不够分给下一位时浪费掉重新申请空间
为了验证以上说法我们调试起来并且为了便于观察我们将二进制数转化为十六进制数来观察计算机中的储存方式
所以转化为十六进制分别是6 2 0 3 0 4
我们调试起来 可以看到与我们假设的方式一摸一样
那么就说明VS这样的编译器就是按如上所说的方式去做的
但是
但是
但是不能说明别的编译器按这种方式储存结构体成员内容
2.4位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
这样的问题在不同平台的问题是不同的 2. 位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机 器会出问题。
也就是说我们给int 定义很大的空间时比如说int b 30
这个30的合理性有待商榷在十六位的机器上可能就会出现问题 3. 位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。 4. 当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是 舍弃剩余的位还是利用这是不确定的。
基于以上问题就可以得知位段是不跨平台的所以注重可移植的程序应该避免使用位段 总结 跟结构相比位段可以达到同样的效果但是可以很好的节省空间但是有跨平台的问题存在。
以上就是此次要分享的全部内容希望对你有所帮助最后求个三连感谢观看