鹤壁商城网站建设,高明网站建设哪家好,百度最新推广产品,建站宝盒站群版目录 1.简介2.struct 的复制3.struct 指针4.struct 的嵌套5.位字段6.弹性数组成员 1.简介
本篇原文为#xff1a;C语言中的struct详解#xff1a;定义、赋值、指针、嵌套与位字段。
更多C进阶、rust、python、逆向等等教程#xff0c;可点击此链接查看#xff1a;酷程网 … 目录 1.简介2.struct 的复制3.struct 指针4.struct 的嵌套5.位字段6.弹性数组成员 1.简介
本篇原文为C语言中的struct详解定义、赋值、指针、嵌套与位字段。
更多C进阶、rust、python、逆向等等教程可点击此链接查看酷程网
C 语言内置的数据类型除了最基本的几种原始类型只有数组属于复合类型可以同时包含多个值但是只能包含相同类型的数据实际使用中并不够用。
实际使用中主要有下面两种情况需要更灵活强大的复合类型。
复杂的物体需要使用多个变量描述这些变量都是相关的最好有某种机制将它们联系起来。某些函数需要传入多个参数如果一个个按照顺序传入非常麻烦最好能组合成一个复合结构传入。
为了解决这些问题C 语言提供了struct关键字允许自定义复合数据类型将不同类型的值组合在一起这样不仅为编程提供方便也有利于增强代码的可读性。
C 语言没有其他语言的对象object和类class的概念struct 结构很大程度上提供了对象和类的功能。
下面是struct自定义数据类型的一个例子
struct fraction {int numerator;int denominator;
};上面示例定义了一个分数的数据类型struct fraction包含两个属性numerator和denominator。
注意作为一个自定义的数据类型它的类型名要包括struct关键字比如上例是struct fraction单独的fraction没有任何意义甚至脚本还可以另外定义名为fraction的变量虽然这样很容易造成混淆。
另外struct语句结尾的分号不能省略否则很容易产生错误。
定义了新的数据类型以后就可以声明该类型的变量这与声明其他类型变量的写法是一样的
struct fraction f1;f1.numerator 22;
f1.denominator 7;上面示例中先声明了一个struct fraction类型的变量f1这时编译器就会为f1分配内存接着就可以为f1的不同属性赋值。
可以看到struct 结构的属性通过点.来表示比如numerator属性要写成f1.numerator。
再提醒一下声明自定义类型的变量时类型名前面不要忘记加上struct关键字。也就是说必须使用struct fraction f1声明变量不能写成fraction f1。
除了逐一对属性赋值也可以使用大括号一次性对 struct 结构的所有属性赋值
struct car {char* name;float price;int speed;
};struct car saturn {Saturn SL/2, 16000.99, 175};上面示例中变量saturn是struct car类型大括号里面同时对它的三个属性赋值如果大括号里面的值的数量少于属性的数量那么缺失的属性自动初始化为0。
注意大括号里面的值的顺序必须与 struct 类型声明时属性的顺序一致否则必须为每个值指定属性名
struct car saturn {.speed172, .nameSaturn SL/2};上面示例中初始化的属性少于声明时的属性这时剩下的那些属性都会初始化为0。
注意{}中字段名前面有一个点.不要写会报错。
声明变量以后可以修改某个属性的值
struct car saturn {.speed172, .nameSaturn SL/2};
saturn.speed 168;上面示例将speed属性的值改成168。
struct 的数据类型声明语句与变量的声明语句可以合并为一个语句。
struct book {char title[500];char author[100];float value;
} b1;上面的语句同时声明了数据类型book和该类型的变量b1。如果类型标识符book只用在这一个地方后面不再用到这里可以将类型名省略。
struct {char title[500];char author[100];float value;
} b1;上面示例中struct声明了一个匿名数据类型然后又声明了这个类型的变量b1。
与其他变量声明语句一样可以在声明变量的同时对变量赋值
struct {char title[500];char author[100];float value;
} b1 {Harry Potter, J. K. Rowling, 10.0},b2 {Cancer Ward, Aleksandr Solzhenitsyn, 7.85};上面示例中在声明变量b1和b2的同时为它们赋值。
下一章介绍的typedef命令可以为 struct 结构指定一个别名这样使用起来更简洁
typedef struct cell_phone {int cell_no;float minutes_of_charge;
} phone;phone p {5551234, 5};上面示例中phone就是struct cell_phone的别名。
指针变量也可以指向struct结构
struct book {char title[500];char author[100];float value;
}* b1;// 或者写成两个语句
struct book {char title[500];char author[100];float value;
};
struct book* b1;上面示例中变量b1是一个指针指向的数据是struct book类型的实例。
struct 结构也可以作为数组成员
struct fraction numbers[1000];numbers[0].numerator 22;
numbers[0].denominator 7;上面示例声明了一个有1000个成员的数组numbers每个成员都是自定义类型fraction的实例。
struct 结构占用的存储空间不是各个属性存储空间的总和而是最大内存占用属性的存储空间的倍数其他属性会添加空位与之对齐这样可以提高读写效率
struct foo {int a;char* b;char c;
};
printf(%d\n, sizeof(struct foo)); // 24上面示例中struct foo有三个属性在64位计算机上占用的存储空间分别是int a占4个字节指针char* b占8个字节char c占1个字节。它们加起来一共是13个字节4 8 1。
但是实际上struct foo会占用24个字节原因是它最大的内存占用属性是char* b的8个字节导致其他属性的存储空间也是8个字节这样才可以对齐导致整个struct foo就是24个字节8 * 3。
多出来的存储空间都采用空位填充所以上面的struct foo真实的结构其实是下面这样
struct foo {int a; // 4char pad1[4]; // 填充4字节char *b; // 8char c; // 1char pad2[7]; // 填充7字节
};
printf(%d\n, sizeof(struct foo)); // 24为什么浪费这么多空间进行内存对齐呢
这是为了加快读写速度把内存占用划分成等长的区块就可以快速在 Struct 结构体中定位到每个属性的起始地址。
由于这个特性在有必要的情况下定义 Struct 结构体时可以采用存储空间递增的顺序定义每个属性这样就能节省一些空间。
struct foo {char c;int a;char* b;
};
printf(%d\n, sizeof(struct foo)); // 16上面示例中占用空间最小的char c排在第一位其次是int a占用空间最大的char* b排在最后。整个strct foo的内存占用就从24字节下降到16字节。
2.struct 的复制
struct 变量可以使用赋值运算符复制给另一个变量这时会生成一个全新的副本。
系统会分配一块新的内存空间大小与原来的变量相同把每个属性都复制过去即原样生成了一份数据。这一点跟数组的复制不一样务必小心
struct cat { char name[30]; short age; } a, b;strcpy(a.name, Hula);
a.age 3;b a;
b.name[0] M;printf(%s\n, a.name); // Hula
printf(%s\n, b.name); // Mula上面示例中变量b是变量a的副本两个变量的值是各自独立的修改掉b.name不影响a.name。
上面这个示例是有前提的就是 struct 结构的属性必须定义成字符数组才能复制数据如果稍作修改属性定义成字符指针结果就不一样
struct cat { char* name; short age; } a, b;a.name Hula;
a.age 3;b a;上面示例中name属性变成了一个字符指针这时a赋值给b导致b.name也是同样的字符指针指向同一个地址也就是说两个属性共享同一个地址。
因为这时struct 结构内部保存的是一个指针而不是上一个例子的数组这时复制的就不是字符串本身而是它的指针。并且这个时候也没法修改字符串因为字符指针指向的字符串是不能修改的。
总结一下赋值运算符可以将 struct 结构每个属性的值一模一样复制一份拷贝给另一个 struct 变量。这一点跟数组完全不同使用赋值运算符复制数组不会复制数据只会共享地址。
注意这种赋值要求两个变量是同一个类型不同类型的 struct 变量无法互相赋值。
另外C 语言没有提供比较两个自定义数据结构是否相等的方法无法用比较运算符比如和!比较两个数据结构是否相等或不等。
3.struct 指针
如果将 struct 变量传入函数函数内部得到的是一个原始值的副本。
#include stdio.hstruct turtle {char* name;char* species;int age;
};void happy(struct turtle t) {t.age t.age 1;
}int main() {struct turtle myTurtle {MyTurtle, sea turtle, 99};happy(myTurtle);printf(Age is %i\n, myTurtle.age); // 输出 99return 0;
}上面示例中函数happy()传入的是一个 struct 变量myTurtle函数内部有一个自增操作。
但是执行完happy()以后函数外部的age属性值根本没变。原因就是函数内部得到的是 struct 变量的副本改变副本影响不到函数外部的原始数据。
通常情况下开发者希望传入函数的是同一份数据函数内部修改数据以后会反映在函数外部。而且传入的是同一份数据也有利于提高程序性能。
这时就需要将 struct 变量的指针传入函数通过指针来修改 struct 属性就可以影响到函数外部。
struct 指针传入函数的写法如下
void happy(struct turtle* t) {
}happy(myTurtle);上面代码中t是 struct 结构的指针调用函数时传入的是指针。struct 类型跟数组不一样类型标识符本身并不是指针所以传入时指针必须写成myTurtle。
函数内部也必须使用(*t).age的写法从指针拿到 struct 结构本身
void happy(struct turtle* t) {(*t).age (*t).age 1;
}上面示例中(*t).age不能写成*t.age因为点运算符.的优先级高于*。*t.age这种写法会将t.age看成一个指针然后取它对应的值会出现无法预料的结果。
现在重新编译执行上面的整个示例happy()内部对 struct 结构的操作就会反映到函数外部。
(*t).age这样的写法很麻烦。C 语言就引入了一个新的箭头运算符-可以从 struct 指针上直接获取属性大大增强了代码的可读性
void happy(struct turtle* t) {t-age t-age 1;
}总结一下对于 struct 变量名使用点运算符.获取属性对于 struct 变量指针使用箭头运算符-获取属性。
以变量myStruct为例假设ptr是它的指针那么下面三种写法是同一回事
// ptr myStruct
myStruct.prop (*ptr).prop ptr-prop4.struct 的嵌套
struct 结构的成员可以是另一个 struct 结构
struct species {char* name;int kinds;
};struct fish {char* name;int age;struct species breed;
};上面示例中fish的属性breed是另一个 struct 结构species。
赋值的时候有多种写法
// 写法一
struct fish shark {shark, 9, {Selachimorpha, 500}};// 写法二
struct species myBreed {Selachimorpha, 500};
struct fish shark {shark, 9, myBreed};// 写法三
struct fish shark {.nameshark,.age9,.breed{Selachimorpha, 500}
};// 写法四
struct fish shark {.nameshark,.age9,.breed.nameSelachimorpha,.breed.kinds500
};printf(Sharks species is %s, shark.breed.name);上面示例展示了嵌套 Struct 结构的四种赋值写法。另外引用breed属性的内部属性要使用两次点运算符shark.breed.name。
下面是另一个嵌套 struct 的例子
struct name {char first[50];char last[50];
};struct student {struct name name;short age;char sex;
} student1;strcpy(student1.name.first, Harry);
strcpy(student1.name.last, Potter);// or
struct name myname {Harry, Potter};
student1.name myname;上面示例中自定义类型student的name属性是另一个自定义类型如果要引用后者的属性就必须使用两个.运算符比如student1.name.first。
另外对字符数组属性赋值要使用strcpy()函数不能直接赋值因为直接改掉字符数组名的地址会报错。
struct 结构内部不仅可以引用其他结构还可以自我引用即结构内部引用当前结构比如链表结构的节点就可以写成下面这样
struct node {int data;struct node* next;
};上面示例中node结构的next属性就是指向另一个node实例的指针下面使用这个结构自定义一个数据链表
struct node {int data;struct node* next;
};struct node* head;// 生成一个三个节点的列表 (11)-(22)-(33)
head malloc(sizeof(struct node));head-data 11;
head-next malloc(sizeof(struct node));head-next-data 22;
head-next-next malloc(sizeof(struct node));head-next-next-data 33;
head-next-next-next NULL;// 遍历这个列表
for (struct node *cur head; cur ! NULL; cur cur-next) {printf(%d\n, cur-data);
}上面示例是链表结构的最简单实现通过for循环可以对其进行遍历。
5.位字段
struct 还可以用来定义二进制位组成的数据结构称为“位字段”bit field这对于操作底层的二进制数据非常有用
struct {unsigned int ab:1;unsigned int cd:1;unsigned int ef:1;unsigned int gh:1;
} synth;synth.ab 0;
synth.cd 1;上面示例中每个属性后面的:1表示指定这些属性只占用一个二进制位所以这个数据结构一共是4个二进制位。
注意定义二进制位时结构内部的各个属性只能是整数类型。
实际存储的时候C 语言会按照int类型占用的字节数存储一个位字段结构。如果有剩余的二进制位可以使用未命名属性填满那些位。
也可以使用宽度为0的属性表示占满当前字节剩余的二进制位迫使下一个属性存储在下一个字节
struct {unsigned int field1 : 1;unsigned int : 2;unsigned int field2 : 1;unsigned int : 0;unsigned int field3 : 1;
} stuff;上面示例中stuff.field1与stuff.field2之间有一个宽度为两个二进制位的未命名属性。stuff.field3将存储在下一个字节。
6.弹性数组成员
很多时候不能事先确定数组到底有多少个成员如果声明数组的时候事先给出一个很大的成员数就会很浪费空间。
C 语言提供了一个解决方法叫做弹性数组成员flexible array member。
如果不能事先确定数组成员的数量时可以定义一个 struct 结构
struct vstring {int len;char chars[];
};上面示例中struct vstring结构有两个属性。len属性用来记录数组chars的长度chars属性是一个数组但是没有给出成员数量。
chars数组到底有多少个成员可以在为vstring分配内存时确定
struct vstring* str malloc(sizeof(struct vstring) n * sizeof(char));
str-len n;上面示例中假定chars数组的成员数量是n只有在运行时才能知道n到底是多少。
然后就为struct vstring分配它需要的内存它本身占用的内存长度再加上n个数组成员占用的内存长度。最后len属性记录一下n是多少。
这样就可以让数组chars有n个成员不用事先确定可以跟运行时的需要保持一致。
弹性数组成员有一些专门的规则。首先弹性成员的数组必须是 struct 结构的最后一个属性。另外除了弹性数组成员struct 结构必须至少还有一个其他属性。