10

一个“简单”的C++问题

 3 years ago
source link: https://mp.weixin.qq.com/s/AK3jLEtfhlUARhu-5wTqXQ
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

一个“简单”的C++问题

Original felix021 felix021 9/5
Image

作为一个没有正经用 C++ 写过项目的码农,在面试到 C++ 候选人时,尽管心虚,但我也只能鼓起勇气,假装自己很熟 C++ 。

不过每当在简历上看到说自己 “精通” C++ 的时候,还是免不了心里呵呵一下,然后想起早年百度 C++ 规范的开头那句话:

不要使用 C++ 。

毕竟有个我觉得挺基础的 C++ 问题,能正经答出来的人,真的不多。


= 题面 =

题目从一个简单的 struct 开始:

struct X {  int a;  char b;};

问:可以编译、运行如下代码么?

struct X *p = NULL;p++;

思考一下?


= 回答 =

在犹豫一会以后,绝大部分人的回答是:

“可以编译,但不能正常运行”。

进一步询问为什么不能正常运行时,给出的理由大概意思是,空指针是无效的(或者说,没有指向可以修改的内存空间),所以不能修改。

如果你也是这么觉得的,可以再思考一下。


= 解析 =

题面确实有一些令人迷惑,所以很多人思考后仍然给出了上述回答。

他们的问题是,混淆了【指针的值】和【指针指向的对象】。

p 的值是 NULL ,确实指向了一个无效的对象,但是在 "p++" 这个操作中,我们修改的并不是其指向的对象,而是 p 指针本身。

本质上讲,(在常见的CPU架构,如 x86/x86-64/arm 等)指针等价于一个 long 类型,对指针的值进行操作,只是修改了指针指向的对象。


= 再问 =

按照一贯的套路,针对这个问题我还准备了追问:

“执行完后,p 的值预计是多少?”

不过鉴于大部分人都认为前述代码无法正确执行,所以往往只能这么问:

“用 sizeof(struct X) 求 X 的大小,预计可以得到多少?”

因为在 C/C++ 里,对一个指针的自增操作(即++操作符),其含义是将指针指向内存中连续的下一个对象。

这其实是个很开放的问题,除了少部分奇怪的回答之外,大部分人的回答都可以是正确的。

但重要的是为什么是正确的。

比如有些人的回答是 5,因为 int 占用 4 个字节, char 占用 1 个字节。

这当然可以没错 —— 只不过默认情况下,大部分编译器的行为并非如此。


= 再解析 =

例如下图,用 gcc -S 处理左边的 C 代码,生成对应的汇编:

640?wx_fmt=png

可以看出,生成的机器码,是给 p 加上 8,而不是 5。

注:给不熟悉 gcc 汇编的同学解释一下

  • rbp 是栈寄存器,%rbp 是栈顶地址

  • -16(%rbp) 表示 p 存在栈顶 -16 个字节的位置

  • rcx 是 64 bit 寄存器(相对于 x86 的 cx 寄存器,32位)

  • $8 是常量 8

  • 右边红框的意思是,将 p 的值从“栈顶-16”内存中拷贝到 rcx 寄存器中,+8,然后再写回内存。

前面说“可以没错”,是因为我们可以用 pragma 预处理指令改变编译器的行为:

640?wx_fmt=png

可以看到,生成的汇编代码里, $8 变成了 $5 。

实际上这里还可能出现一些其他的答案,比如在 16 bit CPU 下,int 占用2个字节,或者你也可以用 pragma pack(2) 试试看。

所以重要的是为什么


= 还问 =

编译器默认 8 字节的行为被称为“alignment”(内存对齐),很多人在给出这个答案的同时,也会顺口说到这个概念。

于是我可以继续问,内存对齐的意义是什么?

但是显然大多数人都还给老师了……

简而言之,这又是一个空间换时间的 case —— 在常见的 x86 架构下,从奇数地址读取一个 int 数据,会需要读取两次内存,并且将两次读取的数据拼起来,这显然会降低执行效率。

也许有的同学会觉得,即使知道这个概念,好像也没什么用,就像屠龙之术。

其实这就像《踩坑记:go服务内存暴涨》里面提到的内存管理知识一样,看起来好像大部分情况用不到,但真的遇到相关问题时,就会束手无策。

比如像 arm、ppc 这类 CPU 架构,其部署环境往往内存较少(有时会需要用 KB 作为单位),程序员会希望通过减少内存对齐来降低内存消耗;但需要注意的是,其不支持直接读写奇数地址,即使强制生成完全不对齐代码,执行往往会出错。

再比如,通过合理安排 struct 中成员的位置,也可以显著降低对内存的消耗。

例如 X、Y 这样两个 struct,包含相同的成员变量,但只是简单兑换一下 b、c 的位置,就可以节省 25% 的内存占用:

640?wx_fmt=png

注:Y、Z 分别需要占用 16、12 字节

想想看,为什么 Z 不是只占用 10 个字节?


= 不问了不问了 =

最后得承认,这篇有点标题党了,实际上这是一个 C 问题,并不是 C++ 问题。

不过对于熟悉 C++ 的同学,没能正确回答这个问题实在让我有点意外,可能是我又陷入了知识的诅咒了(知道一个知识以后、就很难再想象不知道它的情况)。

你们怎么看这个题目呢?欢迎留言讨论。


推荐阅读:


640?wx_fmt=png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK