目录
- 一、结构体struct的定义和.操作符的使用
- 二、 结构体的创建和初始化
- 三、结构体的内存对齐模式
- 四、指定结构体元素的位字段(bit)
- 五、结构数组
- 六、结构体嵌套
- 七、结构体的赋值
- 八、箭头操作符 ->的使用
- 九、指向结构体数组的指针
- 十、结构体中的数组成员和指针成员
- 十一、在堆中创建结构体
- 十二、结构体作为函数的参数
- 十三、案例-使用结构体动态接收控制台输入参数
- 十四、思考:结构体的成员到底在栈还是堆? 结构体的成员可以在结构体中初始化吗?
- 十五、如何解决数组名不能当做左值进行赋值的问题
- 十六、结构体内存申请与赋值操作
- 十七、声明结构体的时候确定结构体的名字
- 十八、结构体使用=号赋值的隐患
- 十九、结构体嵌套一级指针的内存管理
- 二十、结构体内嵌套二级指针的内存管理
- 二十一、结构体嵌套结构体的赋值
- 二十二、结构体嵌套结构体的指针偏移取值操作
- 二十三、结构体的对齐模式
一、结构体struct的定义和.操作符的使用
#include <stdio.h>
#include <string.h>
// 定义一个结构体,相当于扩展的数据类型
struct student
{
char name[100];
int age;
}; // 注意这里的;号不能少
int main()
{
// 定义一个student类型的结构体变量,变量名是st,存在栈内存中
struct student st; // 注意struct关键字不能少
// 通过.操作符给结构体的变量赋值
st.age = 20;
strcpy(st.name, "刘德华");
printf("name=%s,age=%d\n", st.name, st.age); // name=刘德华,age=20
return 0;
}
二、 结构体的创建和初始化
struct数据类型同样支持在创建的时候进行初始化,这里介绍4种初始化方式。
#include <stdio.h>
#include <string.h>
// 定义一个结构体,相当于扩展的数据类型
struct student
{
char name[100];
int age;
}; // 注意这里的;号不能少
int main()
{
// 创建结构体并初始化
// 方式1
struct student st1 = {"周星驰", 40};
printf("方式一:name=%s,age=%d\n", st1.name, st1.age);
// 方式2
struct student st2 = {"周杰伦"}; // 仅初始化名字
printf("方式二:name=%s,age=%d\n", st2.name, st2.age);
// 方式3
struct student st3 = {0}; // 重置所有变量
printf("方式三:name=%s,age=%d\n", st3.name, st3.age);
// 方式4
struct student st4 = {.age = 30, .name = "张学友"}; // 通过.操作符,可以不用按照变量在结构体中的定义顺序来赋值
printf("方式四:name=%s,age=%d\n", st4.name, st4.age);
return 0;
}
结果如下:
方式一:name=周星驰,age=40
方式二:name=周杰伦,age=0
方式三:name=,age=0
方式四:name=张学友,age=30
三、结构体的内存对齐模式
编译器在编译一个结构体的时候采用内存对齐模式
#include <stdio.h>
// 定义一个结构体,相当于扩展的数据类型
struct man{
char a;
int b;
};
int main()
{
struct man m;
printf("%lu\n", sizeof(m)) ; // 结果是 8
}
一个结构体变量的成员总是以最大的那个元素作为对齐单位的,也就是说上面man的结构体中虽然int+char总共才5个字节, 但是对齐后会以int的大小为最小单位来分配内存,那就是分配8个字节, 这样当新增变量的时候会判断结构体中空闲的空间是否能放下,如果能放下则不会去申请内存,如果放不下则又会继续申请n*4个字节,n的大小由变量个数和类型决定(假设新增的变量的数据类型长度都是小于4个字节的情况下); 如果结构体成员出现数组,对齐的时候也是看数组的类型和其他数据类型做比较,找最大的数据类型作为对齐的最小单位。
注意:C语言中结构体的内存对齐还会受到结构体中变量的定义顺序的影响.
#include<stdio.h>
struct A
{
char a;
short b;
char c;
int d;
};
int main()
{
struct A a = {1,2,3,4};
printf("%lu\n",sizeof(a)); //结果是12,而不是8, 这是因为short的摆放位置导致的
return 0;
}
用一张图来说明上面输出结果是12而不是8的原因
所以上面的代码可以优化一下
struct A
{
char a;
char c;
short b; //很简单,只需要把short移动一下, 把char c移上去即可
int d;
};
这个时候内存对齐的图示就变成这样了.
看图就知道了,这样就可以只需要分配8个字节的内存就可以了.
规律:结构体内存的对齐规律总是以2个倍数进行对齐的,如下图所示:
3.1结构体强转其他类型
如果一个结构体中所有成员都是一种类型,那么这个结构体变量在内存中就基本和一个数组类似,因为都是内存连续的一块空间。
#include<stdio.h>
struct A
{
char a[10];
char b;
};
int main()
{
struct A a = {0};
printf("%u\n",sizeof(a)); //11
//如果一个结构体中所有成员都是一种类型,那么这个结构体变量在内存中就基本和一个数组类似
char *s = (char*) &a; //强制转成char *类型,就可以把他看成char[] 了
//通过数组赋值的方式对结构体的变量赋值
s[0] = 'a';
s[1] = 'b';
s[2] = 'c';
printf("a.a=%s,a.b=%c\n",a.a,a.b); //a.a=abc, a.b还是默认值, 因为结构体的前10个字节都是变量a的空间
printf("%p,%p\n",&a,a.a); //结构体变量的地址就是这个结构体首元素的地址
return 0;
}
四、指定结构体元素的位字段(bit)
定义一个结构体的时候可以指定具体的元素的位长(bit)
struct test{
char a : 2;// 指定元素为2位长(2bit),不是2个字节长 ,注意是 : 号
};
结构体中使用位字段是有特殊用途的,比如一个char 有8个bit, 最多可以当做8个布尔变量来使用, 因为一个bit要么是0,要么是1。
#include<stdio.h>
struct A
{
unsigned char a1 : 1;
unsigned char a2 : 1;
unsigned char a3 : 1;
unsigned char a4 : 1;
unsigned char a5 : 1;
unsigned char a6 : 1;
unsigned char a7 : 1;
unsigned char a8 : 1;
};
struct B
{
unsigned char a:1;
int b;
};
struct C
{
unsigned char a:1;
int b[10];
};
int main()
{
// 结果是1, 因为8个bit 刚好用1个字节就可以搞定了. 虽然结构体A中定义了8个char 关键字,
// 但是每个变量都是用:1 表示只需要char中的1个bit
printf("%u\n",sizeof(struct A));
// 结果是8, 因为内存对齐是以int为最小单位分配的,所以分配了2个int的大小就是8个字节.
printf("%u\n",sizeof(struct B));
// 结果是44,因为变量b需要10个int, 变量a以int位最小单位对齐,也需要1个int, 所以总共 11个int, 也就是44个字节
printf("%u\n",sizeof(struct C));
return 0;
}
五、结构数组
结构体也是变量,所以也可以存在数组的形式
#include<stdio.h>
struct student //定义一个结构体
{
char name[20];
unsigned char age;
int sex;
};
int main()
{
// 定义固定大小的student结构体数组, 数据类型是struct student
// struct student st[3] = {{"刘德华",30,1},{"小明",40,0},{"hello",20,0}};
// 定义student结构体数组并初始化值 ,[]不填数字,说明数组是动态的, 可以初始化任意个数
struct student st[] = {{"abc",20,1},{"hello",10,2}};
int i;
for(i =0 ;i < sizeof(st)/sizeof(st[0]);i++)
{
//循环取出数组中的元素, 元素的数据类型是 struct student
struct student s = st[i];
printf("name=%s,age=%d,sex=%d\n",s.name,s.age,s.sex);
}
return 0;
}
5.1 结构体数组的冒泡排序
#include <stdio.h>
struct student
{
char name[10];
unsigned char age;
};
int main()
{
//定义一个student结构体数组
struct student st[] = {{"a", 5}, {"b", 10}, {"c", 8}, {"d", 2}};
int i, j;
int size = sizeof(st) / sizeof(st[0]); // 获取数组元素个数
//冒泡排序
for (i = 0; i < size; i++)
{
for (j = 0; j < size - i - 1; j++)
{
if (st[j].age > st[j + 1].age) // 对比年龄
{
// 定义临时变量, 注意类型是struct student
struct student temp = st[j];
st[j] = st[j + 1];
st[j + 1] = temp;
}
}
}
i = 0;
for (i = 0; i < size; i++)
{
printf("name=%s,age=%d\n", st[i].name, st[i].age);
}
return 0;
}
结果如下:
name=d,age=2
name=a,age=5
name=c,age=8
name=b,age=10
六、结构体嵌套
结构体是允许被嵌套的, 嵌套后的结构体在内存对齐的时候会单独看成一个整体,和它同级的变量不会存放到这个结构体的内存区域中.
#include <stdio.h>
struct A
{
int a1;
char a2;
};
struct B
{
struct A a1; // 结构体A会单独一个整体
char a2;
int a3;
};
int main()
{
struct B b; // 创建结构体B
b.a1.a1 = 10; // 给结构体B中的A的变量a1赋值
printf("%lu\n", sizeof(b)); // 长度是16
printf("a.a1=%d\n", b.a1.a1); // a.a1=10
return 0;
}
长度是16,因为结构体B中的 a2变量不会跑去结构体A的内存区域, 因为结构体A是单独一个整体,所以结构体B中的a2会单独申请一个int 的最小单元大小, 如下图示:
七、结构体的赋值
结构体本质也是变量, 所以是可以直接用=号赋值,当然前提是必须同类型的结构体之间才可以直接赋值.
#include <stdio.h>
struct Student
{
char name[10];
int age;
};
int main()
{
struct Student s1 = {"abc", 20};
struct Student s2 = s1; // 结构体的变量赋值就是内存拷贝
printf("%s,%d\n", s2.name, s2.age); // abc,20
return 0;
}
八、箭头操作符 ->的使用
当指针指向结构体时,结构体的成员赋值可以使用箭头符号->
#include <stdio.h>
#include <string.h>
struct student // 定义一个student结构体
{
char name[10];
unsigned char age;
};
int main()
{
// 创建一个student结构体
struct student s = {"hello", 20};
// 定义student结构体的指针,指向student结构体的地址
struct student *p = &s;
// 有2种方式对结构体的变量赋值
// 方式1 , 这种方式就是前面学的, 通过*p先获到student结构体 ,然后再操作结构体内的变量
strcpy((*p).name, "hi");
(*p).age = 50;
// 方式2, 使用箭头操作符, 推荐使用
strcpy(p->name, "hi");
p->age = 50;
return 0;
}
九、指向结构体数组的指针
指向结构体数组和指向普通数组的指针用法一样,把指针变量名当做数组名来使用即可.
#include <stdio.h>
#include <string.h>
struct student // 定义一个student结构体
{
char name[10];
unsigned char age;
};
int main()
{
// 创建一个student结构体数组
struct student st[3] = {{"张三", 0}, {"李四", 0}, {"王五", 0}};
// 数组名就是首元素的地址,可以用指针来接收
struct student *p = st;
p->age = 100; // 操作首元素的结构体student的age
p++; // 指针++后执行数组的第二个元素
p->age = 20; // 再次对age赋值,此时操作的是数组中的第二个元素了
p--; // 重新复位指针的位置
// 遍历数组, 操作指针即可
for (int i = 0; i < 3; i++)
{
// 操作指针和操作数组名一样
printf("操作指针--->%s,%d\n", p[i].name, p[i].age);
printf("操作数组名--->%s,%d\n", st[i].name, st[i].age);
}
return 0;
}
结果如下:
操作指针--->张三,100
操作数组名--->张三,100
操作指针--->李四,20
操作数组名--->李四,20
操作指针--->王五,0
操作数组名--->王五,0
十、结构体中的数组成员和指针成员
一个结构中可以有数组成员,也可以有指针成员,如果要对结构体内的指针变量赋值的时候就需要提前为指针成员分配内存。
struct man1
{
char name[100]; // 数组成员
};
struct man2
{
char *name; // 指针成员
};
上面2个结构体的name变量的含义是不一样的,char name[100];是一个数组,变量名name是有内存地址的,就是数组首元素的地址, 而 char *name;是一个指针,没有赋值的话是NULL,也就是没有指向任何的地址,是不能直接使用的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct man1
{
char name[100];
};
struct man2
{
char *name;
};
int main()
{
struct man1 m1 = {0};
struct man2 m2 = {0};
printf("man1.name=%p,man2.name=%p\n", m1.name, m2.name); // man1.name=0x16b43aff4,man2.name=0x0
strcpy(m1.name, "hello"); //数组可以直接使用strcpy.
// strcpy(m2.name,"hello");//这个会报错,因为man2的name变量是指针,使用前必须先指向一个内存地址, 否则就是空指针
// 先给指针指向一个地址,从堆中申请20个字节大小
m2.name = (char *)calloc(20, sizeof(char));
strcpy(m2.name, "hello world"); //char *使用strcpy必须先初始化指针
printf("man1.name=%s,man2.name=%s\n", m1.name, m2.name); // man1.name=hello,man2.name=hello world
// 释放堆内存
free(m2.name);
return 0;
}
说白了就是结构体的char *
不能直接传递给strcpy使用,必须先指向一个地址, 也就是需要先初始化, 或者在创建结构体的时候就显示赋值, 这种操作等同于char * c = "hello";
就是指向一个字符串常量的地址。
十一、在堆中创建结构体
如果结构体有指针类型成员,同时结构体在堆中创建的,那么释放堆中结构体之前需要提前释放结构体中的指针成员指向的内存,然后再释放结构体自身的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct man
{
char *name;
int age;
};
int main()
{
// 创建结构体指针
struct man *s = malloc(sizeof(struct man));
s->name = malloc(10 * sizeof(char));
// 先释放结构体的指针成员指向的堆内存
free(s[0].name);
// 然后再释放结构体指针指向的堆内存
free(s);
return 0;
}
注意:在堆中创建结构体,那么结构体中的成员都是存在堆中, 而不是栈中的. 同理在栈中创建的结构体,那么结构体的成员就存在栈中.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student
{
char name[10];
unsigned char age;
};
int main()
{
// st.name存在栈中, 因为st是在栈中创建的, 但是里面的成员如果有用到指针变量的,还是得初始化下
struct student st;
// p->name在堆中, 因为p指向的结构体是在堆中创建的
struct student *p = malloc(sizeof(struct student));
strcpy(st.name, "hello");
strcpy(p->name, "world");
printf("%s,%s\n", st.name, p->name);// hello,world
free(p);
return 0;
}
十二、结构体作为函数的参数
12.1将结构体作为函数的参数
这种方式不推荐,因为结构体作为函数参数时, 传递的实参会和函数的形参进行浅拷贝(赋值操作),这个过程是在栈中完成的,如果实参的结构体比较大, 会消耗栈的内存. 另外还有一个问题就是C语言中函数的形参的修改并不会影响到实参,也就是说如果你想对结构体的成员进行修改, 通过这种方式是行不通的,因为形参的改变并不会影响到实参。
12.2 将结构指针作为函数参数
这种方式就是用来解决12.1的问题的, 如果要修改实参,必须将实参的地址传过去, 所以函数的参数必须是指针类型。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student
{
char name[10];
unsigned char age;
};
void setStudent(struct student *s)
{
strcpy(s->name, "abc");
s->age = 20;
}
int main()
{
struct student st = {"hello", 15};
printf("修改前name=%s,age=%d\n", st.name, st.age); // 修改前name=hello,age=15
setStudent(&st); //修改结构体,需要传递结构体的地址
printf("修改后name=%s,age=%d\n", st.name, st.age); // 修改后name=abc,age=20
return 0;
}
十三、案例-使用结构体动态接收控制台输入参数
例如结构体用来保存学生的姓名和年龄, 通过控制台来录入,在不知道具体会录入多少学生的情况下就需要动态来申请堆内存了,这里会用到realloc来申请。效果图如下所示:
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student
{
char *name;
int age;
};
int main()
{
struct student *p = NULL;
int i = 1;
while (1) // 死循环
{
char tmp[1024] = {0};
printf("please input your name\n");
scanf("%s", tmp);
if (strcmp("exit", tmp) == 0)
break; // 输入exit退出
// 动态申请结构体的内存大小,第一次p=NULL,相当于malloc, 之后每次增加一个结构体的大小
p = realloc(p, sizeof(struct student) * i);
// 给name指针成员分配内存
int len = strlen(tmp);
// 因为申请的结构体内存是连续的,所以可以通过操作数组的方式来操作
p[i - 1].name = malloc(len + 1);
// 赋值name
strcpy(p[i - 1].name, tmp);
// 赋值age
printf("please input your age\n");
scanf("%d", &p[i - 1].age);
i++;
}
// 输出
int a;
for (a = 0; a < i - 1; a++)
{
printf("name=%s,age=%d\n", p[a].name, p[a].age);
free(p[a].name); // 释放name指针的内存
}
free(p); // 释放结构体的内存
return 0;
}
十四、思考:结构体的成员到底在栈还是堆? 结构体的成员可以在结构体中初始化吗?
- 结构体的成员在哪里主要看结构体在哪里创建, 如果在栈里创建,那么结构体的成员就是在栈中, 如果结构体在堆中创建,那么它的成员也在堆中,因为它们是一个整体。
- 结构体中的成员不能在结构体中初始化,因为结构体的定义只是一种数据类型, 还没有进行创建, 只有等结构体创建之后才能操作里面的成员。
如下所指示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct A
{
char array[100];
int a;
};
struct B
{
// char *p = malloc(100); 报错
int a;
};
int main()
{
struct A a; // array在栈里面
struct A *p = malloc(sizeof(struct A)); // array在堆里面
a.a = 20;
p->a = 30;
return 0;
}
十五、如何解决数组名不能当做左值进行赋值的问题
前面我们都知道数组名是一个常量,不能当做变量来使用,所以对他进行赋值会报错,也就是说下面的代码会报错。
char a[100] = "hello";
char b[100] = a; // 这样会报错
报错如下:
如何解决呢?
我们可以通过结构体来间接实现, 因为结构体是一种扩展的数据类型, 所以它定义的变量是可以当做左值使用的,代码如下所示:
struct STR
{
char a[100];
};
struct STR a = {"hello"};
strcut STR b = a; //这样是允许的, 因为 a,b都是变量, 这样操作相当于浅拷贝, 将结构体a中的字符串拷贝到结构体b中
十六、结构体内存申请与赋值操作
结构体说白了就是一种数据类型,所以它的指针操作完全可以当做普通类型的指针来操作, 需要注意的是结构体在分配堆内存的时候,它的最小内存单元是单个结构体的大小。
例如申请的的堆内存大小是 malloc(n * sizeof(结构体类型)), 这里的n表示申请多少个结构体. 假设需要申请10个结构体的空间,那么n就是10.
当给这一块空间的成员赋值和取值的时候有2种方式: 指针偏移的方式和数组名的方式, 这点和普通类型的指针操作是一样的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student
{
// char name[20];
char *name;
int age;
};
int main()
{
// 在栈中创建结构体,可以直接通过初始化的时候给name指针赋值
struct student s1 = {"a", 20};
printf("s1.name=%s,s1.age=%d\n", s1.name, s1.age);
// 在堆中创建结构体
// 分配10个结构体大小的空间,p指针初始位置是指向第一个结构体的首元素地址
struct student *p = malloc(10 * sizeof(struct student));
// 给第一个结构体赋值
p->name = (char *)malloc(20); // 结构体内的指针成员在使用前需要先初始化才能使用
strcpy(p->name, "first");
p->age = 20;
// 1.使用指针偏移的方式,必须保证p申请的空间刚好
// int i;
// for(i = 1;i< 10;i++)
// {
// (p+i)->name = (char *)malloc(20);
// strcpy((p+i)->name,"other");
// (p+i)->age= 20+i;
// }
// 2.下面当做数组名使用,同样必须保证p申请的空间刚好
int i;
for (i = 1; i < 10; i++)
{
p[i].name = (char *)malloc(20);
strcpy(p[i].name, "other");
p[i].age = 20 + i;
}
// 通过数组的方式输出结构体的值
for (i = 0; i < 10; i++)
{
printf("name=%s,age=%d\n", p[i].name, p[i].age);
}
// 通过指针偏移方式输出结构体的值
for (i = 0; i < 10; i++)
{
printf("name=%s,age=%d\n", (p + i)->name, (p + i)->age);
}
// 释放堆内存, 注意这里p的地址没有改变,还是第一个结构体的地址.
// 需要先释放结构体的成员指针
for (i = 0; i < 10; i++)
{
free(p[i].name);
}
free(p);
return 0;
}
十七、声明结构体的时候确定结构体的名字
很简单,直接在结构体{}和;直接写变量名即可. 和定义结构体类型很相似,区别就是少了前面的typedef
#include <stdio.h>
#include <string.h>
struct Person
{
char name[63];
int age;
} my_person; //my_person就是结构体的变量名,可以直接使用了.
int main()
{
strcpy(my_person.name, "hello");
my_person.age = 20;
printf("name=%s,age=%d\n", my_person.name, my_person.age);
return 0;
}
还有一种写法,直接把struct后的类型名省略了
#include <stdio.h>
struct
{
char name[64];
int age;
} tom, jack = {"jack", 18}; // 也可以进行赋值操作
int main()
{
printf("tom:name=%s,age=%d\n", tom.name, tom.age); // tom:name=,age=0
printf("jack:name=%s,age=%d\n", jack.name, jack.age); // jack:name=jack,age=18
return 0;
}
十八、结构体使用=号赋值的隐患
=号赋值是一个浅拷贝的过程,它会将结构体内的所有成员逐个字节拷贝拷贝到另一个结构体中.
如果结构体内部有指针并且指针指向堆空间,那么就会存在下面2个安全隐患:
1)同一块空间被释放两次.
因为=号赋值会使2个结构体内的指针成员指向同一个地址,那么释放这2个结构体内的指针成员的时候就有问题了,由于2个结构体内的指针成员指向的地址是一样了,所以结构体A释放完后,结构体B再去释放就会报错.
2)内存泄露.
假设赋值前,结构体A和结构体B内的指针成员已经指向了各自的堆空间,当A赋值给B后,A结构体内的指针成员原先指向的堆空间就泄露了,因为没有指针再指向这块空间了.
[解决办法]
假设赋值操作是结构体A赋值给结构体B
1)先手动释放结构体B内的指针成员指向的堆空间
2)然后根据结构体A内的指针成员的实际内容的大小分配结构体B内对应的指针成员.
3)然后开始手动拷贝.
示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person
{
char *name;
int age;
};
int main()
{
struct Person p1;
p1.name = malloc(sizeof(char) * 30);
strcpy(p1.name, "tom");
p1.age = 20;
struct Person p2;
p2.name = malloc(sizeof(char) * 30);
strcpy(p2.name, "jack tang");
p2.age = 30;
printf("before copy p1.name=%s,p1.age=%d\n", p1.name, p1.age);
printf("before copy p2.name=%s,p2.age=%d\n", p2.name, p2.age);
//开始拷贝操作,将p2赋值给p1
//1.先释放p1.name
if (p1.name)
{
free(p1.name);
p1.name = NULL;
}
//2.根据p2.name的大小分配p1.name的大小
p1.name = malloc(strlen(p2.name) + 1);
//3.开始拷贝
strcpy(p1.name, p2.name);
p1.age = p2.age;
printf("=============================================\n");
printf("after copy p1.name=%s,p1.age=%d\n", p1.name, p1.age);
printf("after copy p2.name=%s,p2.age=%d\n", p2.name, p2.age);
// 此时释放p1.name和p2.name就不会报错了
if (p1.name)
{
free(p1.name);
p1.name = NULL;
}
if (p2.name)
{
free(p2.name);
p2.name = NULL;
}
return 0;
}
结果:
before copy p1.name=tom,p1.age=20
before copy p2.name=jack tang,p2.age=30
=============================================
after copy p1.name=jack tang,p1.age=30
after copy p2.name=jack tang,p2.age=30
十九、结构体嵌套一级指针的内存管理
假设有一个结构体二级指针,应该如何管理内存.
#include <stdio.h>
#include <stdlib.h>
struct Person
{
char *name; //一级指针
int age;
};
// 内存分配
struct Person **malloc_space()
{
//二级指针内包含3个结构体一级指针
struct Person **temp = malloc(sizeof(struct Person *) * 3);
for (int i = 0; i < 3; ++i)
{
//结构体一级指针分配内存
temp[i] = malloc(sizeof(struct Person));
//name成员分配内存
temp[i]->name = malloc(sizeof(char) * 30);
//赋值
sprintf(temp[i]->name, "name:%d", i + 1);
temp[i]->age = 20 + i;
}
return temp;
}
// 取值操作
void printf_person(struct Person **person, int len)
{
if (NULL == person)
return;
for (int i = 0; i < len; ++i)
{
// person[i]得到是struct Person *类型
printf("name=%s,age=%d\n", person[i]->name, person[i]->age);
}
}
// 释放内存操作
void free_space(struct Person **person, int len)
{
if (NULL == person)
return;
for (int i = 0; i < len; ++i)
{
if (NULL == person[i])
continue;
if (NULL != person[i]->name)
{
//先释放name
free(person[i]->name);
person[i]->name = NULL;
}
//再释放struct Person *
free(person[i]);
person[i] = NULL;
}
//最后释放struct Person **
free(person);
person = NULL;
}
int main()
{
// 结构体二级指针
struct Person **p = malloc_space();
printf_person(p, 3);
free_space(p, 3);
return 0;
}
二十、结构体内嵌套二级指针的内存管理
#include <stdio.h>
#include <stdlib.h>
struct Teacher
{
char *name;
char **student; //二级指针
};
// 分配内存
int malloc_space(struct Teacher ***teacher)
{
if (NULL == teacher)
return -1;
// 结构体二级指针
struct Teacher **te = malloc(sizeof(struct Teacher *) * 3);
for (int i = 0; i < 3; ++i)
{
//给struct Teacher *分配空间
te[i] = malloc(sizeof(struct Teacher));
//给Teacher的name分配空间
te[i]->name = malloc(sizeof(char) * 30);
sprintf(te[i]->name, "te_%d", i + 1);
//给Teacher的student **分配空间,每个teacher有4个学生
te[i]->student = malloc(sizeof(char *) * 4);
//给student*分配空间
for (int j = 0; j < 4; ++j)
{
te[i]->student[j] = malloc(sizeof(char) * 30);
sprintf(te[i]->student[j], "%s_st_%d", te[i]->name, j + 1);
}
}
// 通过指针间接赋值
*teacher = te;
return 0;
}
// 取值操作
void print_teacher(struct Teacher **teacher)
{
if (NULL == teacher)
return;
for (int i = 0; i < 3; ++i)
{
printf("teacher:%s\n", teacher[i]->name);
if (NULL == teacher[i]->student)
continue;
for (int j = 0; j < 4; ++j)
{
printf("\tstudent:%s\n", teacher[i]->student[j]);
}
}
}
// 释放内存操作
void free_space(struct Teacher **teacher)
{
if (NULL == teacher)
return;
for (int i = 0; i < 3; ++i)
{
if (NULL == teacher[i])
continue;
//释放Teacher的name
if (NULL != teacher[i]->name)
{
free(teacher[i]->name);
teacher[i]->name = NULL;
}
if (NULL == teacher[i]->student)
continue;
//释放char* student
for (int j = 0; j < 4; ++j)
{
if (NULL == teacher[i]->student[j])
continue;
free(teacher[i]->student[j]);
teacher[i]->student[j] = NULL;
}
//释放char** student
free(teacher[i]->student);
teacher[i]->student = NULL;
//释放Teacher*
free(teacher[i]);
teacher[i] = NULL;
}
//释放Teacher**
free(teacher);
teacher = NULL;
}
int main()
{
struct Teacher **teacher = NULL;
malloc_space(&teacher); //输出特性,修改实参的值
print_teacher(teacher);
free_space(teacher);
return 0;
}
二十一、结构体嵌套结构体的赋值
当结构体嵌套另一个结构体时,可以直接把另一个结构体的成员当做当前结构体的成员看待,所以使用{}赋值的时候可以直接赋值,例如:
#include <stdio.h>
struct A
{
char a;
int b;
};
struct B
{
char c;
int d;
struct A a; //嵌套结构体A
};
int main()
{
struct B b = {'h', 100, 'w', 200}; //直接赋值
printf("%c,%d,%c,%d\n", b.c, b.d, b.a.a, b.a.b); // h,100,w,200
}
二十二、结构体嵌套结构体的指针偏移取值操作
获取到结构体的指针后强转成char*,让它的步长变成1,然后通过offsetof函数计算成员的偏移量,如果要查找的成员位于嵌套的结构体内,则还需要计算嵌套结构体内的成员偏移量,累计起来就定位到了指定成员的首地址了,然后根据成员的类型,再强转回对应成员的指针类型,最后取*就可以得到值了.
或者直接定位到嵌套的结构体并强转为对应的结构体指针类型,然后通过->操作符取值.
#include <stdio.h>
#include <stddef.h>
struct A
{
char a;
double b;
};
struct B
{
char c;
int d;
struct A a; //嵌套结构体A
};
int main()
{
struct B b = {'h', 100, 'w', 3.14}; //直接赋值
// 方式1.如何通过指针偏移获取结构体B内的结构体A的b成员的值
int offset1 = offsetof(struct B, a); //结构体B内的a成员的偏移量
int offset2 = offsetof(struct A, b); //结构体A内的b成员的偏移量
double a_b = *(double *)((char *)&b + offset1 + offset2);
printf("a_b=%f\n", a_b);
// 方式2:先定位到结构体B中的A结构体,然后强转成结构体A的指针,通过箭头访问b成员.
a_b = ((struct A *)((char *)&b + offset1))->b;
printf("a_b=%f\n", a_b);
return 0;
}
二十三、结构体的对齐模式
结构体对齐模式大体分为2个步骤:
步骤1是计算每个结构体内的每个成员偏移量, 偏移量取当前数据类型和魔术的最小值的最小倍数作为起始字节数.
步骤2是计算整个结构体的偏移量,偏移量取结构体内成员的最大类型的n倍和当前结构体内所有成员的字节数比较取最小值.
Windows下的默认魔数值是8,通过#pragma pack(show)查看
struct Student
{
int a; //第一个元素的偏移量是0, 占0-3字节
char b;//第二个元素的偏移量是min(1,魔数)=1, 所以是起始字节数是4,占4-7字节
double c;//min(8,魔数) = 8,所以起始字节数是8, 占8-15字节
float d;//min(4,魔数) =4, 由于前面占了15个字节,4的最小倍数只能取16了,所以偏移量是16,占16-19字节
};
//结构体内所有成员字节数总和是19, 最大成员类型是double长度是8,比19大而且是8的倍数的最小数是24,所以这个结构体的大小是24