C语言面试题详解(第7节)

上一节讨论了美国某著名软件企业M公司的面试题,面试题目如下:

文章发出后,引起了一些争议:有些朋友发现以上C语言程序运行时并不会崩溃,还有些则发现程序运行时确实会崩溃。这是怎么回事?同一个C语言程序还能得到不同的结果吗?的确如此,有些C语言语句在不同架构的平台下运行结果确实可能有所差异。

一个典型的例子是,long 型在某些平台(如 x86_64平台)占用 8 字节内存空间(sizeof(long)==4),而在另外一些平台(如x86平台)仅仅占用 4 字节内存空间(sizeof(long)==4)。

C标准并没有定义 long 型占多少内存空间。

耐心看完上一篇文章的朋友应该会发现,文章在最后说,在 x86_64 机器上,“long 型问题”和 “int 型问题”会得到两个不同的结果:一个运行时崩溃,另一个则可以正常运行。那为什么会这样呢?请继续往下看。

以下均是在 x86_64 平台分析的。

先看看崩溃时的情况

会崩溃的是“long 型问题”,C语言代码如下:

struct S{
     long     i;
     long     *p;
};  
int main()
{
     struct S s;
     long *p = &s.i;
     p[0] = 4;
     p[1] = 3;
     s.p=p; 
     s.p[1] = 1;
     s.p[0] = 2;

     return 0;
} 


以上C语言程序会崩溃的原因,上一节已经较为详细的讨论过,感到陌生的朋友可以再看看上一节。关键原因就是执行

s.p[1] =1;
// 相当于执行
s.p = 1;

导致 s.p 指向了地址 1,接下来对 s.p[0] 赋值就相当于对地址 1 赋值,这当然会崩溃。不过这一切分析都是建立在 s.p[1] 等价于 s.p 之上的,为什么它俩等价呢?请看下图:

已知 s.p 指向 p,p 指向 s.i,所以 s.p 其实指向的就是结构体 s 自身。如果结构体 s 的成员 i 和成员 p 是紧密排列的,那么 s.p + 1 不就指向 s.p 了吗?此时 s.p[1] 和 s.p 自然是等价的。

请注意“紧密排列”这个词。

再来看看“int型问题”

“int型问题”对应的C语言程序如下,编译后运行不会崩溃,为什么呢?

按照上面的分析,“long型问题”的C语言程序崩溃的直接原因是 s.p[0] = 1; 向地址 1 赋值,罪魁祸首是s.p[1] = 1; 让 s.p 指向地址 1 。那如果结构体 s 的成员 i 和成员 p 不是紧密排列的,如下图:

此时 s.p+1 并不指向 s.p,执行 s.p[1] = 1; 并不会把 s.p 的值也修改了,此时执行赋值语句 s.p[0] = 2; 自然就不会崩溃了。那问题又来了,结构体 s 的成员 i 和成员 p 什么时候“紧密排列”,什么时候“不紧密排列”呢?这就涉及到C语言中数据的内存对齐问题了。

C语言中的内存对齐

那么,什么是“内存对齐”呢?在回答这个问题之前,先来看看下面这个C语言程序:

 #include <stdio.h>
struct S{
     int     i;
     int     *p;
};
int main()
 {
     struct S s;
     printf("%lu %lu %lu\n", sizeof(s.i), sizeof(s.p), sizeof(s));
     return 0;
 }


上面这个C语言程序非常简单,就是输出结构体 s 和它两个成员的 size,这个程序会输出什么呢?编译并执行之,得到如下结果:

# gcc t.c
# ./a.out 
4 8 16

s.i 和 s.p 的 size 分别是 4 字节和 8 字节,但有些奇怪的是,结构体 s 的 size 居然是大于 4+8 的 16!怎么回事?这其实就是编译器对结构体 s 的两个成员做了“内存对齐”的缘故。

之所以要做“内存对齐”,主要是为了提升访问内存数据时的效率。在 64 位地址总线的平台上,处理器访问数据是逐 8 字节进行的(0-7, 8-15, 16-23, ...),即使是想读取单字节的 char 型数据,处理器也得一次性读取 8 字节数据。

现在假设 4 字节的 int 型变量 a 存放在内存地址 6-9 处,C语言程序若想读取 a 的数值,需要分两次先后读取 0-7 和 8-15 地址的数据,这就降低了效率。(在其他某些平台,若数据没有内存对齐,程序是无法正常运行的。)对数据做内存对齐,其实就是为了尽力减少程序读取数据时,需要访问内存的次数。

现在“结构体 s 的 size 居然是大于 4+8 的 16”就好理解了,在 x86_64 平台上,s.p 占 8 字节内存空间,为了提升效率,编译器会将其放在地址 addr 处,而 addr 必须是 8 的整数倍。但是 s.i 只占 4 字节内存空间,若将 s.p 紧跟在 s.i 之后,s.p 的地址 addr 就不是 8 的整数倍了,为了解决这个问题,编译器会在 s.i 后填充 4 字节,如下图:

这就解释了为什么 sizeof(s) 不等于 sizeof(s.i)+sizeof(s.p),也对“int型问题”为什么不会崩溃的解释做了补充。

为了加深对“内存对齐”的认识,再来看个例子,请看下面的C语言代码:

#include <stdio.h>

struct S1{
     char        a;
     int         b;
     char        c;
};
struct S2{
     char        a;
     char        b;
     int         c;
};
int main()
{
     struct S1 s1;
     struct S2 s2;
     printf("%lu %lu\n", sizeof(s1), sizeof(s2));
     return 0;
 }


上面的C语言代码中,结构体 S1 和结构体 S2 的成员个数和类型都是一样的,唯一的区别就是 int 型成员的顺序不同,那么 sizeof(s1) 和 sizeof(s2) 相等吗?得到答案最简单的方法就是实际运行这个C语言程序:

# gcc t.c
# ./a.out
12 8

看来,sizeof(s1) 和 sizeof(s2) 是不相等的,那为什么呢?先来分析结构体 s1:成员 a 和 c 只占用一个字节内存空间,无论放在哪个地址都是自然对齐的。关键在于 int 型的成员 b,它占用 4 字节内存空间,为了将其放在 4 的整数倍的内存地址中,编译器需要在成员 a 后填充 3 个字节。成员 c 可以紧跟在 b 之后,但是为了兼顾结构体 s1 的内存对齐,需要在其后也填充 3 字节,即:

这么一来,sizeof(s1) 显然等于 12。

再来分析一下结构体 s2:成员 b 是 char 型的,占用 1 字节内存空间,因此可以紧跟在 a 之后,现在 a 和 b 共同占用 2 字节内存空间。而成员 c 占用 4 字节内存空间,考虑到要对其做内存对齐,需要再在 b 之后填充 2 字节,即:

这就解释了为什么 sizeof(s2) 等于 8.

小节

如果在开发C语言程序时,有内存对齐的概念,那么在定义结构体的时候就会留心成员的顺序,尽可能的减少程序对内存的占用。当然了,如果内存空间实在紧张,C语言也是有手段不让编译器做自动“内存对齐”的,至于是什么样的手段,留给读者自己思考了。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif