C语言结构体
前言
随笔
1 | void main() |
其中 explain2 是变量名称。而 struct explain 和括号内的内容是类型定义。
1 | typedef struct explain1 |
将代码优化后会变成这样。意思是,使用了 typedef 将 struct explain1 和括号 内容重新定义为 explain2,这样可以简化代码。而 explain3 是变量名称。
1 | void main() |
而这样是使用了匿名的方法。其他变量不能使用这种结构体。
结构体及成员地址
首先,我们需要知道,所谓变量,其实是内存地址的一个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。
所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。
先来看一段简单的代码:
1 | struct test{ |
上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。
如果我们有这样的代码:
1 | struct test t; |
如果实例t在内存中的绝对地址是0x7fffffffe5f0,则t.i的绝对地址也是0x7fffffffe5f0,而t.p的地址是0x7fffffffe5f4。
说白了,t.i其实就是(&t + 0x0), t.p的其实就是(&t + 0x4)。
而0x0和0x4这两个偏移地址就是成员i和p在编译时被编译器给计算好了地址。于是,你就知道,不管结构体的实例是什么——访问其成员其实就是实例地址加上成员的偏移量。
结构体的内存对齐
再来看下面这段代码:
1 | struct test{ |
这段代码中指针pt的值是NULL,这代表指针指向的内存地址是0x0。其中pt->i的指向的内存地址是0x0,pt->c指向的地址是0x4,而pt->p指向的地址是0x8。
这里short类型是占2个字节,但pt->p的指向的地址确是0x8,而不是0x6。这是因为发生了内存对齐。
关于内存对齐这里不展开说明。但简单来说,如果不对齐的话,编译器就要向内存一个字节一个字节的取,这样做太浪费性能。如果一次取4个字节则高效很多。
因此来看下面这段代码:
1 |
|
这段代码会在printf(f.a->s);处导致程序崩溃。而其中的根源在于f.a->s访问了0x04的内存地址。这通常是一个受保护的内存地址,因此导致了程序崩溃。
结构体中指针和数组的差别
修改下之前的代码,将源代码中的struct str结构体中的char s[0];改成char *s;:
1 |
|
程序会在if (f.a->s)处因为无法访问内存Cannot access memory而崩溃。char *s和char s[0]到底有什么差别呢?
这个涉及到了汇编:
对于
char s[0]的f.a->s来说,汇编代码用了lea指令,lea 0x04(%rax), %rdx
对于char *s的f.a->s来说,汇编代码用了mov指令,mov 0x04(%rax), %rdx
lea全称load effective address,是把地址放进去。
mov则是把地址里的内容放进去。相当于把0x4这个地址的内容放进去。这代表访问了不该访问的内容,所以,程序就崩溃了。
从这里,我们可以看到,访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)
换句话说,对于数组char s[10]来说,数组名s和&s都是一样的。
而下面这段代码并不会崩溃:
1 | struct test{ |
执行结果是:
1 | &s = c |
零长度数组
首先要知道0长度的数组在ISO C和C++的规格说明书中是不允许的。在VC++2012下编译你会得到一个警告:“arning C4200: 使用了非标准扩展 : 结构/联合中的零大小数组”。
那么为什么gcc可以通过而连一个警告都没有?
那是因为gcc为了预先支持C99的这种玩法,所以,让“零长度数组”这种玩法合法了。参考下面这篇文章:Arrays of Length Zero。
有下面这段代码:
1 |
|
这段代码的大意是:我想分配一个不定长的数组。因此我使用一个结构体去设计这个数组,其中length代表数组长度,contents代表数组内容。通过malloc()去分配我指定的数组大小——也就是this_length的值。
这种方式叫做柔性数组。
在这个数组中,如果thisline的地址是0x601010,则contents的地址也是0x601010。但thisline->contents的值是0x601014,并且数组的是也是aaaaaaaaaa。
你如果使用sizeof()会发现返回0。这就是说,零长度的数组是存在于结构体内的,但是不占结构体的size。
你可以简单的理解为一个没有内容的占位标识,直到我们给结构体分配了内存,这个占位标识才变成了一个有长度的数组。
虽然也可以使用指针来解决上面提到的问题:
1 | struct line { |
但这个结构体的内存并不是连续的。使用零长度数组的好处有下面两个:
- 第一个意义是,方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
- 第二个原因是,可以实现一些刁钻的需求。可以结构体的地址偏移来进行定位和寻址。
这是使用char contents[]的内存地址:
1 | (gdb) x /14b thisline |
从上面的内存布局我们可以看到,前4个字节是int length,后10个字节就是char contents[]。
这是使用char* contents的内存地址:
1 | (gdb) x /16b thisline |
上面一共输出了四行内存,其中:
- 第一行前四个字节是 int length,第一行的后四个字节是对齐。
- 第二行是
char* contents,64位系统指针8个长度,他的值是0x20 0x10 0x60,也就是0x601020。 - 第三行和第四行是
char* contents指向的内容。
从这里,我们看到,其中的差别——数组的原地就是内容,而指针的那里保存的是内容的地址。