拜读了陈皓大佬的《C语言结构体里的成员数组和指针》,受益匪浅
“有容乃大”,给啥都往里装
struct tag
{
member_list;
}variable_list;
看看例子:
struct fruit
{
char name[15];
float weight;
float price;
};
struct fruit
{
char name[15];
float weight;
float price;
}apple, melon;
int main()
{
struct fruit banana;
return 0;
}
一坨结构体用一对花括号初始化
struct fruit
{
char name[15];
float weight;
float price;
}apple = { "apple", 1.5, 10 }, melon = { "melon", 4, 40 };
int main()
{
struct fruit pineapple = { "pineapple", 1, 30 };
return 0;
}
struct f1
{
char buyer[20];
};
struct fruit
{
char name[15];
float weight;
float price;
struct f1;
}apple = { "apple", 1.5, 10 }, melon = { "melon", 4, 40 };
int main()
{
//struct fruit pineapple = { "pineapple", 1, 30 };
struct fruit mango = { "mango", 0.5, 10, {"bacon"} };
return 0;
}
描述复杂对象
一个芒果,单纯的用char/int 来描述,达不到我们想要的描述效果
内存对齐其实就是一种 空间换时间 的做法
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
欸?例2和例1明明成员一样,例2的总大小却更小
原来
让大小较小的成员尽量集中在一起可以节省空间
struct ss
{
char c1;
int i1;
};
struct SS
{
char c2;
int i2;
struct ss s1;
};
int main()
{
struct SS s2;
printf("%d\n", sizeof(s2));
return 0;
}
位段是什么?和结构体很类似,但是有几点要注意:
- 位段的成员必须是 整型家族的
- 位段在成员后有一个冒号和一个数字,来表示此成员所占的大小(比特位)
- 位段的开辟是按char / int 开辟的,也就是一次开辟1/4 字节(看成员)
位段本身就是很不稳定的玩意,所以我们基本不会把int 和 char成员混在一起
一定程度上节省空间
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
其实这里从右向左还是从左向右是标准未定义的,不过vs2019是右到左
跨平台问题
这一段代码来自于“左耳朵耗子”大佬的博客,我叫它“ 不是bug的‘bug’ ”
这个程序在运行起来之后会在第18行crash
#include
struct str
{
int len;
char s[0];
};
struct foo
{
struct str* a;
};
int main(int argc, char** argv)
{
struct foo f = { 0 };
if (f.a->s)
{
printf("%p\n", f.a->s);
}
return 0;
}
:
00000004
解释一下这段代码:
访问 空结构体指针 的成员,结果打印了 00000004
接下来,我们好好剖析一下结构体到底是怎么回事
首先我们要了解:任何变量的本质其实都是地址,是内存地址的抽象名字,因为机器只认地址,变量在编译的时候会被转成地址
struct test
{
int i;
char* p;
};
int main()
{
struct test t;
return 0;
}
引用陈皓大佬的代码,gdb进来:
// t实例中的p就是一个野指针
(gdb) p t
$1 = {i = 0, c = 0 '\000', d = 0 '\000', p = 0x4003e0 "1\355I\211\..."}
// 输出t的地址
(gdb) p &t
$2 = (struct test *) 0x7fffffffe5f0
//输出(t.i)的地址
(gdb) p &(t.i)
$3 = (char **) 0x7fffffffe5f0
//输出(t.p)的地址
(gdb) p &(t.p)
$4 = (char **) 0x7fffffffe5f4
可以看到:
t 这个结构体的地址是 0x7fffffffe5f0
t.i 这个结构体成员的地址是 0x7fffffffe5f0
t.p 这个结构体成员的地址是 0x7fffffffe5f4
也就是:
得出:
*偏移量通过内存对齐得到
再看一段可以体现此本质的代码
struct test
{
int i;
short c;
char* p;
};
int main()
{
struct test* t = NULL;
return 0;
}
引用陈皓大佬的代码,gdb进来:
(gdb) p pt
$1 = (struct test *) 0x0
(gdb) p pt->i
Cannot access memory at address 0x0
(gdb) p pt->c
Cannot access memory at address 0x4
(gdb) p pt->p
Cannot access memory at address 0x8
可以看到,即使 t 是 NULL,还是通过偏移量(相对地址)访问,也相当于访问 pt 的内址
现在就能够理解为什么访问空结构体指针的成员能打印个 00000004 出来了吧
通过汇编来看 “ 不是bug的’bug’ ”
lea 0x04(%rax), %rdx //对于 char s[0]
mov 0x04(%rax), %rdx//对于 char* s
lea(loading effective address): 加载有效地址(把地址放进去,不访问其中的数据)
mov:把地址里的数据放进去(访问地址中的数据)
有个很有意思的比喻,lea 好比到银行门口张望,但是不抢银行,也不犯法;mov 好比在警察眼皮子底下抢银行
到此,咱们也能解释为什么不是在第16行报错了
:我们可以拿到 00 00 00 04 这个地址,但绝对不能访问其中的数据
柔性数组:结构中最后一个未知大小的数组
struct S
{
char c;
int i[];
};
或是
struct S
{
char c;
int i[0];
};
柔性数组在结构中到底是什么存在呢?
引陈皓大佬
其实它连内存空间都不占,可以把它看作一个占位的标识,等我们通过这个标识,给其开辟的空间,它才从标识变成了有长度的数组
struct S
{
char c;
int i[0];
};
int main()
{
struct S* p = (struct S*)
malloc(sizeof(struct S) + 100 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 100; i++)
{
p->i[i] = i;
}
for (i = 0; i < 100; i++)
{
printf("%d ", p->i[i]);
}
return 0;
}
既然柔性数组在没开辟空间的时候是 标识,那我们为他开辟空间的时候就要算上结构体内其他的成员啦
所以有了 malloc(sizeof(struct S) + 10 * sizeof(int))
读到这可能很多人有疑问:我拿个指针不一样可以代替这破柔性数组吗?可以,一起看看
struct S
{
char c;
int* pi;
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));
p->pi = (int*)malloc(100 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 100; i++)
{
p->pi[i] = i;
}
for (i = 0; i < 100; i++)
{
printf("%d ", p->pi[i]);
}
free(p->pi);
p->pi = NULL;
free(p);
p = NULL;
return 0;
}
两次开辟,两次释放,也能完成柔性数组的工作
但是柔性数组就没有它的优点吗?和这种指针实现比较,有两个优点:
枚举:一一列举
enum day
{
//枚举类型day 包含的枚举常量
};
给出枚举常量
默认初始化:从第一个到最后一个枚举常量的值为 0—n-1
每个枚举常量的值都是前一个+1
enum day
{
//枚举类型day 包含的枚举常量
Mon,//0
Tues,//1
Wed = 5,//5
Thur,//6
Fri,//7
Sat,//8
Sun//9
};
int main()
{
int input = 0;
scanf("%d", &input);
switch (input)
{
case 0://不直观
case Mon://直观
printf("Mon\n");
break;
case 1://不直观
case Tues://直观
printf("Tues\n");
}(input == 8);
return 0;
}
enum day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
----------------
#define Mon 1
#define Tues 2
#define Wed 3
#define Thur 4
#define Fri 5
#define Sat 6
#define Sun 7
常量也能赋值吗?二次赋值,对于枚举常量和枚举常量是可以的
enum color
{
Red,//0
Green,//1
Blue//2
};
int main()
{
enum color clr = Red;
clr = Green;//(1)
clr = 2;//(2)
return 0;
}
(1)没问题:同类型的枚举常量可以互相赋值
(2)错误:常量2 不是和 clr 同类型的枚举常量,不能赋值(C语言是强类型的语言)
联合体的成员共用一块内存空间(都保存在同一块内存)
union un
{
char a;
char b;
};
union un
{
char a;
char b;
};
用联合体来判断机器大小端:
union un
{
int i;
char c;
};
int main()
{
union un u;
u.i = 0x11223344;
u.c = 0x00;
printf("%x\n", u.i);
return 0;
}
:
11223300
轻松判断:小端
我们在使用的时候要通过正确的途径,保证不同时使用联合体成员
节省空间
本期分享就到这啦,不足之处望请斧正
培根的blog,和你共同进步!