C和C++程序员面试秘笈
上QQ阅读APP看书,第一时间看更新

第1章 C/C++程序基础

作为程序员,你在求职时,公司会询问你的项目经验,例如你做过什么类型的项目、担任的是何种角色,以及做项目时如何与他人沟通,等等。除此之外,当然还要考查你的编程能力。这里包括你的编程风格,以及你对于赋值语句、递增语句、类型转换、数据交换等程序设计基本概念的理解。因此,最好在考试之前复习这些程序设计的基本概念,并且要特别重视那些比较细致的考点问题。本章列出了一些涉及C/C++程序设计基本概念的考题,希望读者在读完后能有所收获。

面试题1 看代码写输出——一般赋值语句

考点:一般赋值语句的概念和方法

出现频率:★★★

    1    #include <stdio.h>
    2
    3    int main(void)
    4    {
    5         int x = 3, y, z;
    6
    7        x *= (y = z = 4); printf("x = %d\n", x);
    8
    9         z = 2;
    10        x = (y = z); printf("x = %d\n", x);
    11        x = (y == z); printf("x = %d\n", x);
    12        x = (y & z); printf("x = %d\n", x);
    13        x = (y && z); printf("x = %d\n", x);
    14
    15        y = 4;
    16        x = (y | z); printf("x = %d\n", x);
    17        x = (y || z); printf("x = %d\n", x);
    18
    19        x = (y == z)? 4: 5;
    20        printf("x = %d\n", x);
    21
    22        x = (y == z)? 1: (y < z)? 2: 3;
    23        printf("x = %d\n", x);
    24
    25        return 0;
    26   }

【解析】

程序的说明如下:

❑ 程序执行至第8行时,x的值为3, y和z未被初始化。此行的执行顺序是首先执行z=4,然后执行y=z,最后执行x*=y。因此x的值为3*4=12。

❑ 程序执行至第10行时,z的值为2。此行的执行顺序是首先执行y=z,然后执行x=y。因此x的值为2。

❑ 程序执行至第11行时,y和z的值都为2。此行的执行顺序是首先执行y==z,比较y和z的值是否相等,然后将比较的结果赋给x。因此x的值为1。

❑ 程序执行至第12行时,y和z的值都为2。此行把y和z做按位与(&)运算的结果赋给变量x。y和z的二进制都是10,因此y & z的结果为二进制10。因此x的值为2。

❑ 程序执行至第13行时,y和z的值都为2。此行把y和z做逻辑与(&&)运算的结果赋给变量x。此时y和z的值都不是0,因此y && z的结果为1。因此x的值为1。

❑ 程序执行至第16行时,y的值为4, z的值为2。此行把y和z做按位或(|)运算的结果赋给变量x。此时y和z的二进制表示分别为100和010,因此y|z的结果为110。因此x的值为110,十进制表示为6。

❑ 程序执行至第17行时,y的值为4, z的值为2。此行把y和z做逻辑或(||)运算的结果赋给变量x。此时y和z的值都不是0,因此y||z的结果为1。因此x的值为1。

❑ 程序执行至第19行时,y的值为4, z的值为2。此行首先比较y和z的大小是否相等,如果相等,则将x取4和5的前者,否则x取4和5的后者。在这里,y不等于z,因此x的值为5。

❑ 程序执行至第22行时,y的值为4, z的值为2。此行首先比较y和z大小是否相等,如果相等,x取1,否则,判断y是否大于z,如果是,则取2,否则取3。在这里,y的值大于z的值,因此x的值为3。

总结:这个考题只是考查各种基本的赋值运算。这里,读者要注意位运算与逻辑运算的区别,以及三元操作符的用法。通过程序代码17行以及19行的举例,我们可以发现三元操作符有时可以代替条件判断if/else/else if的组合。

【答案】

    x = 12
    x = 2
    x = 1
    x = 2
    x = 1
    x = 6
    x = 1
    x = 5
    x = 3

面试题2 看代码写输出——C++域操作符

考点:C++域操作符的使用

出现频率:★★★

请指出下面这个程序在C和C++中的输出分别是什么。

    1    #include <stdio.h>
    2
    3    int value = 0;
    4
    5    void printvalue()
    6    {
    7         printf("value = %d\n", value);
    8    };
    9
    10   int main()
    11   {
    12        int value = 0;
    13
    14        value = 1;
    15        printf("value = %d\n", value);
    16
    17        ::value = 2;
    18        printvalue();
    19
    20        return 0;
    21   }

【解析】

如果将文件保存为后缀名为.c的文件,则在Visual C++ 6.0中不能通过编译并且提示17行有语法错误。而如果文件保存为后缀名为.cpp的文件,则在Visual C++ 6.0中就能顺利通过编译并且运行。

这段程序有两个变量,其名字都是value。不同的是,其中一个是在main函数之前就声明的全局变量,而另外一个是在main函数内部声明的局部变量。这两个变量的作用域是不一样的。

这里要注意:在函数printvalue里打印的是全局变量的值,在main函数的15行打印的是局部变量的值。这是因为在main函数里的局部变量value引用优先。在C++中可以通过域操作符“::”来直接操作全局变量(代码的17行操作的value是全局变量),但是在C中不支持这个操作符,因此会报错。注意:在C中不推荐这种局部变量与全局变量同名的设计方式。

【答案】

在C中编译不能通过,并指示17行符号错误。

在C++中的输出如下:

    value=1(局部变量value)
    value=2(全局变量value)

面试题3 看代码写输出——i++和++i的区别

考点:i++和++i的区别

出现频率:★★★★★

    1    #include <stdio.h>
    2
    3    int main(void)
    4    {
    5         int i=8;
    6
    7         printf("%d\n", ++i);
    8         printf("%d\n", --i);
    9         printf("%d\n", i++);
    10        printf("%d\n", i--);
    11        printf("%d\n", -i++);
    12        printf("%d\n", -i--);
    13        printf("------\n");
    14
    15        return 0;
    16   }

【解析】

程序的说明如下:

❑ 程序第7行,此时i的值为8。这里先i自增1,再打印i的值。因此输出9,并且i的值也变为9。

❑ 程序第8行,此时i的值为9。这里先i自减1,再打印i的值。因此输出8,并且i的值也变为8。

❑ 程序第9行,此时i的值为8。这里先打印i的值,再i自增1。因此输出8,并且i的值也变为9。

❑ 程序第10行,此时i的值为9。这里先打印i的值,再i自减1。因此输出9,并且i的值也变为8。

❑ 程序第11行,此时i的值为8。这里的“-”表示负号运算符。因此先打印-i的值,再i自增1。因此输出-8,并且i的值也变为9。

❑ 程序第12行,此时i的值为9。这里的第一个“-”表示负号运算符,后面连在一起的两个“-”表示自减运算符。因此先打印-i的值,再i自减1。因此输出-9,并且i的值也变为8。

【答案】

    9
    8
    8
    9
    -8
    -9
    ------

面试题4 i++与++i哪个效率更高

考点:i++和++i的效率比较

出现频率:★★★

【解析】

在这里声明,简单地比较前缀自增运算符和后缀自增运算符的效率是片面的,因为存在很多因素影响这个问题的答案。首先考虑内建数据类型的情况:如果自增运算表达式的结果没有被使用,而是仅仅简单地用于增加一员操作数,答案是明确的,前缀法和后缀法没有任何区别,编译器的处理都应该是相同的,很难想象得出有什么编译器实现可以别出心裁地在二者之间制造任何差异。我们看看下面这个程序。

    1    #include <stdio.h>
    2
    3    int main()
    4    {
    5         int i = 0;
    6         int x = 0;
    7
    8         i++;
    9         ++i;
    10        x = i++;
    11        x = ++i;
    12
    13        return 0;
    14   }

上面的代码在VC++ 6.0中编译,得到的汇编如下。

    ; Line 5
        mov  DWORD PTR _i$[ebp], 0
    ; Line 6
        mov  DWORD PTR _x$[ebp], 0
    ; Line 8
        mov  eax, DWORD PTR _i$[ebp]
        add  eax, 1
        mov  DWORD PTR _i$[ebp], eax
    ; Line 9
        mov  ecx, DWORD PTR _i$[ebp]
        add  ecx, 1
        mov  DWORD PTR _i$[ebp], ecx
    ; Line 10
        mov  edx, DWORD PTR _i$[ebp]
        mov  DWORD PTR _x$[ebp], edx
        mov  eax, DWORD PTR _i$[ebp]
        add  eax, 1
        mov  DWORD PTR _i$[ebp], eax
    ; Line 11
        mov  ecx, DWORD PTR _i$[ebp]
        add  ecx, 1
        mov  DWORD PTR _i$[ebp], ecx
        mov  edx, DWORD PTR _i$[ebp]
        mov  DWORD PTR _x$[ebp], edx

代码段第8行和第9行生成的汇编代码分别对应Line 8和Line 9下对应的汇编代码,可以看到3个步骤几乎完全一样。

代码段第10~11行生成的汇编代码分别对应Line 10和Line 11下对应的汇编代码,可以看到都是5个步骤,只是在加1的先后顺序上有一些区别,效率也是完全一样的。

由此说明,考虑内建数据类型时,它们的效率差别不大(去除编译器优化的影响)。所以在这种情况下,我们大可不必关心。

现在让我们再考虑自定义数据类型(主要是指类)的情况。此时我们不需要再做很多汇编代码的分析了,因为前缀式(++i)可以返回对象的引用,而后缀式(i++)必须返回对象的值,所以导致在大对象的时候产生了较大的复制开销,引起效率降低。因此处理使用者自定义类型(注意不是指内建类型)的时候,应该尽可能地使用前缀式递增/递减,因为它天生“体质”较佳。

【答案】

内建数据类型的情况,效率没有区别。

自定义数据类型的情况,++i效率较高。

面试题5 选择编程风格良好的条件比较语句

考点:良好的编程风格

出现频率:★★★★

A.假设布尔变量名字为flag,它与零值比较的标准if语句如下。

第一种:

    1    if (flag == TRUE)
    2    if (flag == FALSE)

第二种:

    1    if (flag)
    2    if (! flag)

B.假设整型变量的名字为value,它与零值比较的标准if语句如下。

第一种:

    1    if (value == 0)
    2    if (value ! = 0)

第二种:

    1    if (value)
    2    if (! value)

C.假设浮点变量的名字为x,它与0.0的比较如下。

第一种:

    1    if (x == 0.0)
    2    if (x ! = 0.0)

第二种:

    1    if ((x >= -EPSINON) && (X <= EPSINON))
    2    if ((x < -EPSINON) || (X > EPSINON))

其中,EPSINON是允许的误差(精度)。

D.指针变量p与0的比较如下。

第一种:

    1    if (p == NULL)
    2    if (p ! = NULL)

第二种:

    1    if (p == 0)
    2    if (p ! = 0)

【解析】

❑ A的第二种风格较良好。根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++将TRUE定义为1,而Visual Basic则将TRUE定义为-1。因此不可将布尔变量直接与TRUE、FALSE进行比较。

❑ B的第一种风格较良好,第二种风格会让人误解value是布尔变量,应该将整型变量用“==”或“! =”直接与0比较。

❑ C的第二种风格较良好。注意:无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“! =”与数字比较,应该设法转化成“>=”或“<=”形式。

❑ D的第一种风格较良好,指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。用p与NULL显式比较,强调p是指针变量。如用p与0比较,容易让人误解p是整型变量。

面试题6 看代码写结果——有符号变量与无符号变量的值的转换

考点:有符号变量与无符号变量的区别和联系

出现频率:★★★★

    1    #include <stdio.h>
    2
    3    char getChar(int x, int y)
    4    {
    5         char c;
    6         unsigned int a = x;
    7
    8         (a + y > 10)? (c = 1): (c = 2);
    9         return c;
    10   }
    11
    12   int main(void)
    13   {
    14        char c1 = getChar(7, 4);
    15        char c2 = getChar(7, 3);
    16        char c3 = getChar(7, -7);
    17        char c4 = getChar(7, -8);
    18
    19        printf("c1 = %d\n", c1);
    20        printf("c2 = %d\n", c2);
    21        printf("c3 = %d\n", c3);
    22        printf("c4 = %d\n", c4);
    23
    24        return 0;
    25   }

【解析】

首先说明getChar()函数的作用:它有两个输入参数,分别是整型的x和y。在函数体内,把参数x的值转换为无符号整型后再与y相加,其结果与10进行比较,如果大于10,则函数返回1,否则返回2。在这里,我们要注意:当表达式中存在有符号类型和无符号类型时,所有的操作数都自动转换成无符号类型。因此,这里由于a是无符号数,在代码第8行中,y值会首先自动转换成无符号的整数,然后与a相加,最后再与10进行比较。以下是在main函数中各调用getChar()函数的分析。

❑ 代码第14行,传入的参数分别为7和4,两个数相加后为11,因此c1返回1。

❑ 代码第15行,传入的参数分别为7和3,两个数相加后为10,因此c2返回2。

❑ 代码第16行,传入的参数分别为7和-7, -7首先被转换成一个很大的数,然后与7相加后正好溢出,其值为0,因此c3返回2。

❑ 代码第17行,传入的参数分别为7和-8, -8首先被转换成一个很大的数,然后与7相加。两个数相加后为很大的整数(差1就正好溢出了),因此c4返回1。

我们可以看到,由于无符号整数的特性,getChar()当参数x为7时,如果y等于区间[-7,3]中的任何整数值,getChar()函数都将返回2。当y的值在区间[-7,3]之外时,函数返回-1。

总之,我们在看表达式时要很小心地注意符号变量与无符号变量之间的转换、占用不同字节内存的变量之间的赋值等操作,否则可能会出现我们意想不到的结果。

【答案】

    c1 = 1
    c2 = 2
    c3 = 2
    c4 = 1

面试题7 不使用任何中间变量如何将a、b的值进行交换

考点:两个变量的值的交换方法

出现频率:★★★★

【解析】

请参考以下C++程序代码。

    1    #include <stdio.h>
    2
    3    void swap1(int& a, int& b)
    4    {
    5         int temp = a;  //使用局部变量temp完成交换
    6         a = b;
    7         b = temp;
    8    };
    9
    10   void swap2(int& a, int& b)
    11   {
    12        a=a+b;  //使用加减运算完成交换
    13        b=a-b;
    14        a=a-b;
    15   };
    16
    17   void swap3(int& a, int& b)
    18   {
    19        a^=b;  //使用异或运算完成交换
    20        b^=a;
    21        a^=b;
    22   };
    23
    24   int main(void)
    25   {
    26        int a1 = 1, b1 = 2;
    27        int a2 = 3, b2 = 4;
    28        int a3 = 5, b3 = 6;
    29        int a = 2147483647, b = 1;
    30
    31        swap1(a1, b1);  //测试使用临时变量进行交换的版本
    32        swap2(a2, b2);  //测试使用加减运算进行交换的版本
    33        swap3(a3, b3);  //测试使用异或运算进行交换的版本
    34
    35        printf("after swap...\n");
    36        printf("a1 = %d, b1 = %d\n", a1, b1);
    37        printf("a2 = %d, b2 = %d\n", a2, b2);
    38        printf("a3 = %d, b3 = %d\n", a3, b3);
    39
    40        swap2(a, b);
    41        printf("a = %d, b = %d\n", a, b);
    42
    43        return 0;
    44   }

以上的C++程序中有3个swap函数,都是采用引用传参的方式。

❑ swap1()采用的是我们在许多教科书里看到的方式,用一个局部变量temp保存其中一个值来达到交换目的。当然,这种方式不是本题要求的答案。

❑ swap2()采用的是一种简单的加减算法来达到交换a、b的目的。这种方式的缺点是做a+b和a-b运算时可能会导致数据溢出。

❑ swap3()采用了按位异或的方式交换a、b。按位异或运算符“^”的功能是将参与运算的两数各对应的二进制位相异或,如果对应的二进制位相同,则结果为0,否则结果为1。这样运算3次即可交换a、b的值。

代码第31~32行做了调用3种swap函数的举例,注意第40行的调用,这里在swap2函数栈中的运算会有数据溢出发生。我们知道,在32位平台下,int占4个字节内存,其范围是-2147483648~2147483647,因此2147483647加1就变成了-2147483648。不过通过运行结果我们可以看到,虽然产生了溢出,但是交换操作依然是成功的。下面是程序运行结果。

    after swap…
    a1 = 2, b1 = 1
    a1 = 4, b1 = 3
    a1 = 6, b1 = 5
    a1 = 1, b1 = 2147483647

【答案】

采用程序代码中swap2和swap3的交换方式。swap2有可能发生数据溢出的缺点。相比于swap2,推荐swap3,采用按位异或的方式。

面试题8 C++与C有什么不同

考点:C和C++的联系与区别

出现频率:★★★★

【答案】

C是一个结构化语言,它的重点在于算法和数据结构。对语言本身而言,C是C++的子集。C程序的设计首要考虑的是如何通过一个过程,对输入进行运算处理,得到输出。对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够配合对应的问题,这样就可以通过获取对象的状态信息得到输出或实现过程控制。

因此,C与C++的最大区别在于,它们用于解决问题的思想方法不一样。

C实现了C++中过程化控制及其他相关功能。而在C++中的C,相对于原来的C还有所加强,引入了重载、内联函数、异常处理等。C++更是拓展了面向对象设计的内容,如类、继承、虚函数、模板和包容器类等。

在C++中,不仅需要考虑数据封装,还需要考虑对象粒度的选择、对象接口的设计和继承、组合与继承的使用等问题。

相对于C, C++包含了更丰富的设计概念。

面试题9 如何理解C++是面向对象化的,而C是面向过程化的

考点:C++与C的区别

出现频率:★★★

【答案】

C是面向过程化的,但是C++不是完全面向对象化的。在C++中也完全可以写出与C一样过程化的程序,所以只能说C++拥有面向对象的特性。Java是真正面向对象化的。

面试题10 标准头文件的结构

为什么标准头文件都有类似以下的结构?

考点:标准头文件中一些通用结构的理解

出现频率:★★★★

    1    #ifndef __INCvxWorksh
    2    #define __INCvxWorksh
    3    #ifdef __cplusplus
    4    extern "C" {
    5    #endif
    6   /*...*/
    7    #ifdef __cplusplus
    8    }
    9    #endif
    10  #endif /* __INCvxWorksh */

【解析】

显而易见,代码第1、2、10行的作用是防止该头文件被重复引用。代码第3行的作用是表示当前使用的是C++编译器。如果要表示当前使用的是C编译器,可以这样指定:

    1    #ifdef __STDC__

那么代码第4~8行中的extern "C"有什么作用呢?

extern "C"包含双重含义。

首先,被它修饰的目标是“extern”的。也就是告诉编译器,其声明的函数和变量可以在本模块或其他模块中使用。通常,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern声明。例如,当模块B欲引用该模块A中定义的全局变量和函数时,只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

其次,被它修饰的目标是“C”的,意思是其修饰的变量和函数是按照C语言方式编译和连接的。我们来看看C++中对类似C的函数是怎样编译的。作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如下面两个函数:

    1    void foo( int x, int y );
    2    void foo( int x, float y );

这两个函数编译生成的符号是不相同的,前者可能为_foo_int_int之类,而后者可能为_foo_int_float之类。可以发现,这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。这样,如果在C中连接C++编译的符号时,就会因找不到符号问题发生连接错误。

如果加extern "C"声明后,模块编译生成foo的目标代码时,就不会对其名字进行特殊处理,采用了C语言的方式,也就是_foo之类,不会加上后面函数参数数量及类型信息相关的那一串了。因此extern "C"是C++编译器提供的与C连接交换指定的符号,用来解决名字匹配问题。

【答案】

代码第1、2、10行的作用是防止该头文件被重复引用。

代码第3行的作用是表示当前使用的是C++编译器。

代码第4~8行中的extern "C"是C++编译器提供的与C连接交换指定的符号,用来解决名字匹配问题。

面试题11 #include <head.h>和#include "head.h" 有什么区别

考点:头文件引用中<>与""的区别

出现频率:★★★★

【答案】

尖括号< >表明这个文件是一个工程或标准头文件。查找过程会首先检查预定义的目录,我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。

如果文件名用一对引号括起来,则表明该文件是用户提供的头文件,查找该文件时将从当前文件目录(或文件名指定的其他目录)中寻找文件,然后在标准位置寻找文件。

面试题12 C++中main函数执行完后还执行其他语句吗

考点:atexit()函数的使用

出现频率:★★★★

【解析】

很多时候,我们需要在程序退出的时候做一些诸如释放资源的操作,但程序退出的方式有很多种,例如main()函数运行结束,在程序的某个地方用exit()结束程序,用户通过Ctrl+C等操作发信号来终止程序,等等,因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。方法就是用atexit()函数来注册程序正常终止时要被调用的函数。

atexit()函数的参数是一个函数指针,函数指针指向一个没有参数也没有返回值的函数。atexit()的函数原型是:

    1   int atexit (void (*)(void));

在一个程序中最多可以用atexit()注册32个处理函数,这些处理函数的调用顺序与其注册的顺序相反,即最先注册的最后调用,最后注册的最先调用。请看下面的程序代码。

    1    #include<stdlib.h>                     //使用atexit()函数必须包含头文件stdlib.h
    2    #include<stdio.h>
    3
    4    void fn1(void);
    5    void fn2(void);
    6
    7    int main(void)
    8    {
    9         atexit(fn1);                      //使用atexit注册fn1()函数
    10        atexit(fn2);                      //使用atexit注册fn2()函数
    11        printf("main exit...\n");
    12        return 0;
    13   }
    14
    15   void fn1()
    16   {
    17        printf("calling fn1()...\n");    //fn1()函数打印内容
    18   }
    19
    20   void fn2()
    21   {
    22        printf("calling fn2()...\n");    //fn2()函数打印内容
    23   }

上面的程序代码在main函数中调用atexit()函数依次注册了fn1()和fn2()函数。运行这个程序,我们可以得到下面的输出。

    main exit…
    calling fn1()…
    calling fn2()…

在这里,fn2()与fn1()在main()函数结束后被依次调用,并且它们被调用的顺序与它们在main()函数被注册的顺序相反。

【答案】

可以用atexit()函数来注册程序正常终止时要被调用的函数,并且在main()函数结束时,调用这些函数的顺序与注册它们的顺序相反。