5

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

 3 years ago
source link: https://blog.popkx.com/explanation-of-interview-questions-in-c-language-section-6/
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语言的灵魂,或者换句话说,离开了指针,C语言就只能处理 1+1 等于 2 这样的小学问题了。这么说的确有些夸张,不过读者应该能明白这只是为了说明指针在C语言中非常重要

721f8a1e9987ec6ea71e0948293ca151.png

不要神话C语言指针

“指针很重要”,相信很多C语言初学者早就有所耳闻,在学习时严阵以待。但有些朋友可能太过于紧张,在学习过程中甚至“神话了指针”,下面是一个典型的例子:

void setp(int *p)
{
    p = &global;
}
int *ptr;
setp(ptr);

请看上面的C语言代码,在之前的文章中,为了解释 setp() 函数无法改变 ptr,给出了下面这样的C语言代码,请看:

void seti(int a)
{
    a = 3;
}
int ai;
seti(ai);

seti() 函数的行为和 setp() 函数的行为是非常相似的,我曾认为:如果读者知道 seti() 函数无法修改 ai 的值,也自然就明白为什么 setp() 函数无法修改 ptr 了。可是有朋友回复说,“setp() 函数的参数不一样,这可是指针!

其实有何不同呢?把 int * 看作是C语言中和 int 一样的基础数据类型,一切不就都好理解了吗?接下来几节,将讨论几道关于指针的面试题目,希望在讨论这些题目的过程中,能够帮助读者加深对C语言指针的认识。

我们并不是为了做题而做题,而是为了巩固基础,学习知识才去做题的,对不?

3dcdb3f8f5f0b900ac7be4991d65fe19.png

来看看这道C语言面试题

下面这道C语言题目来自美国某著名软件企业M公司的面试题,这个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;
} 
423d4ebc35bf55942f947fe092bf4bc2.png

先来分析一下C语言源代码:p 是一个 long 型的指针,它指向结构体 s 的 i 成员,接着分别对 p[0] 和 p[1] 赋值 4 和 3。然后又让结构体 s 的 p 成员指向 p,对 s.p[1] 和 s.p[0] 分别赋值为 1 和 2。代码很简单,那究竟哪里会崩溃呢?

在之前几节中,我们得到答案最简单粗暴的方法是将C语言代码编译,并在计算机中运行。可是这一题的C语言程序无法正常运行,而是会崩溃:

# gcc t.c 
# ./a.out 
Segmentation fault

这就没有办法通过直接运行C语言程序的方式得到答案了吗?也不是,我们至少还有两种方法。第一种方法就是在每一行之后插入 printf() 函数,使之输出些内容,对应的C语言程序可以如下修改:

int main()
{
     struct S s;
     long *p = &s.i;
     p[0] = 4;
     printf(" -- 1\n");
     p[1] = 3;
     printf(" -- 2\n");
     s.p=p; 
     printf(" -- 3\n");
     s.p[1] = 1;
     printf(" -- 4\n");
     s.p[0] = 2;
     printf(" -- 5\n");
     return 0;
} 
cde6e9b7b5588a2d924ce0412e77d71d.png

因为用到了 printf() 函数,所以不要忘了包含 stdio.h 头文件。

这样一来,C语言程序会执行崩溃之前的 printf() 语句,通过输出的内容,我们就能推断出程序是从哪里崩溃的了,请看:

# gcc t.c 
# ./a.out 
-- 1
-- 2
-- 3
-- 4
Segmentation fault

答案已经非常明显了,上面的C语言程序在执行 s.p[0] = 2 这一行时崩溃了,那为什么会在这一行崩溃呢?请继续往下看。

“第二种方法”就是借助 gdb 的单步调试法了,不过这里讨论的是 C语言,就不啰嗦 gdb 了,感兴趣的朋友可以翻翻我之前的文章。

进一步分析

再分析一下C语言代码,p 指向的是 s.i,而 i 是结构体 s 的第一个成员,所以 p 实际上指向的也是结构体 p,那么 p+1 指向的就是 s.p 了,此时

p[0] = 4;
p[1] = 3;
// 就相当于执行
s.i = 4;
s.p = 3;
566dfd3fecf82928ecc88230bd1f260f.png

这样程序当然不会崩溃。既如此,我们继续往下分析:执行 s.p = p; 后,其实就是让 s.p 指向 s.i,也即指向结构体 s 本身,那么
s.p[1] = 1;
// 就相当于执行
s.p = 1;

这时还不会出错,只不过这一句执行完毕后,s.p 就执行地址 1 了。此时

s.p[0] = 2;
//就相当于执行
*((int*)1) = 2;

对地址 1 赋值,肯定会引发段错误,导致C语言程序崩溃。至此,我们就明白

再来看看这个C语言代码

现在将上述C语言代码的 long 修改为 int:

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

     return 0;
}
2ade1873113bdd7709c981521caa16b0.png

此时再编译运行,发现C语言程序可以正常运行,并没有崩溃。这是怎么回事?要回答这个问题,就需要了解结构体成员的字节对齐了,限于篇幅,以后有机会再说了。

我的机器是 64 位的,如果读者使用的机器是 32 位的,即使将 long 修改为 int,C语言程序可能仍然会崩溃。此时,读者可以尝试将 int 改为 short 再试一试。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK