5

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

 3 years ago
source link: https://blog.popkx.com/explanation-of-interview-questions-in-c-language-section-7/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

ab71684529b36a8d9b2a057ff88e9868.png

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

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

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

156184f37cbcef97a37bf872eae5a28c.png

耐心看完上一篇文章的朋友应该会发现,文章在最后说,在 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;
} 
2bef879602eab5f7012926e8f37c8cad.png

以上C语言程序会崩溃的原因,上一节已经较为详细的讨论过,感到陌生的朋友可以再看看上一节。关键原因就是执行
s.p[1] =1;
// 相当于执行
s.p = 1;

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

566dfd3fecf82928ecc88230bd1f260f.png

已知 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语言程序如下,编译后运行不会崩溃,为什么呢?

60d417b82cff2372fec20854588055ff.png

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

此时 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;
 }
b74d3543adf4e93395a4214cfb804dc9.png

上面这个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 字节,如下图:

1008728284b754d64269a9fb457a5770.png

这就解释了为什么 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;
 }
6b799c5dee2f277977b80b540feb603a.png

上面的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 字节,即:

22f479ceeaf556197f69e1391b928d95.png

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

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

d98280da1e5601f350b76ff1b165d9e1.png

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK